trigger-authoring-chat-agent

Par triggerdotdev · skills

Créez et exécutez un agent de chat IA durable avec `chat.agent` depuis `@trigger.dev/sdk/ai` : la boucle d'exécution par tour, pourquoi vous DEVEZ étaler `...chat.toStreamTextOptions()` en premier, le renvoi d'un `StreamTextResult` versus l'appel à `chat.pipe()`, les deux server actions (`chat.createStartSessionAction` + `auth.createPublicToken`), et le câblage de `useChat` avec `useTriggerChatTransport`. Chargez ce document lors de la construction, modification ou débogage d'un backend de chat (la tâche agent ou ses lifecycle hooks) ou de son transport React, lors de la déclaration de tools typés ou de parts de données personnalisées, ou lors de la migration d'une route `streamText` classique de l'AI SDK vers `chat.agent`.

npx skills add https://github.com/triggerdotdev/skills --skill trigger-authoring-chat-agent

Créer un agent de chat

Un chat.agent exécute une conversation entière comme une tâche Trigger.dev de longue durée. Il s'active à l'arrivée d'un message, se fige quand aucun n'arrive, et l'état en mémoire persiste à travers les rafraîchissements de page, les déploiements, les interruptions et les crash. Votre code est la boucle que vous auriez écrite de toute façon : des messages en entrée, streamText en sortie. Il n'y a pas de routes API. Le frontend communique avec l'agent via un TriggerChatTransport, donc l'historique s'accumule côté serveur et le client n'envoie que le nouveau message à chaque tour.

Fonctionne avec Vercel AI SDK v5, v6 ou v7. Sur v7, installez aussi @ai-sdk/otel pour que les appels de modèle soient tracés (le SDK l'enregistre pour vous).

Configuration

Trois éléments : la tâche agent, deux server actions, et le transport frontend.

1. Définir l'agent

import { chat } from "@trigger.dev/sdk/ai";
import { streamText, stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export const myChat = chat.agent({
  id: "my-chat",
  run: async ({ messages, signal }) =>
    streamText({
      // Spread this FIRST. See "Common mistakes".
      ...chat.toStreamTextOptions(),
      model: anthropic("claude-sonnet-4-5"),
      messages,
      abortSignal: signal,
      stopWhen: stepCountIs(15),
    }),
});

run reçoit messages déjà converties en ModelMessage[] (le SDK convertit le UIMessage[] du frontend pour vous) plus un signal qui abandonne à l'arrêt ou l'annulation. Le retour du StreamTextResult le dirige automatiquement vers le frontend.

2. Ajouter deux server actions

Les deux s'exécutent sur votre serveur, donc le navigateur ne détient jamais votre clé secrète d'environnement. C'est aussi ici que vivent l'autorisation par utilisateur / par plan et toute écriture DB associée.

"use server";
import { auth } from "@trigger.dev/sdk";
import { chat } from "@trigger.dev/sdk/ai";

// Creates the Session + first run, returns a session PAT. Idempotent on (env, chatId).
export const startChatSession = chat.createStartSessionAction("my-chat");

// Pure mint. The transport calls this on 401/403 to refresh an expired token.
export async function mintChatAccessToken(chatId: string) {
  return auth.createPublicToken({
    scopes: { read: { sessions: chatId }, write: { sessions: chatId } },
    expirationTime: "1h",
  });
}

3. Câbler le frontend

"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import type { myChat } from "@/trigger/chat";
import { mintChatAccessToken, startChatSession } from "@/app/actions";

export function Chat() {
  const transport = useTriggerChatTransport<typeof myChat>({
    task: "my-chat", // typeof myChat gives compile-time task-id validation
    accessToken: ({ chatId }) => mintChatAccessToken(chatId),
    startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
  });

  const { messages, sendMessage, stop, status } = useChat({ transport });
  const [input, setInput] = useState("");
  // render messages, a form that calls sendMessage({ text: input }),
  // and a Stop button (onClick={stop}) while status === "streaming".
}

Le transport est mémoïsé (créé une fois, réutilisé à travers les rendus). Passer typeof myChat fait circuler le type de message de l'agent à travers useChat.

Motifs fondamentaux

1. Retour vs pipe

Retournez le résultat de streamText depuis run pour le cas simple. Quand streamText est appelé profondément dans des helpers imbriqués, appelez await chat.pipe(result) depuis n'importe où dans la tâche, et laissez run se résoudre en void.

export const agentChat = chat.agent({
  id: "agent-chat",
  run: async ({ messages }) => {
    await runAgentLoop(messages); // don't return; pipe inside
  },
});

async function runAgentLoop(messages: ModelMessage[]) {
  const result = streamText({
    ...chat.toStreamTextOptions(),
    model: anthropic("claude-sonnet-4-5"),
    messages,
  });
  await chat.pipe(result); // works from anywhere in the task
}

2. Outils typés (déclarez sur config ET répartissez en arrière)

Déclarez les outils sur chat.agent({ tools }), relisez-les dactylographiés depuis la charge utile run(), et passez ce set à chat.toStreamTextOptions({ tools }). Une déclaration circule partout.

import { tool, stepCountIs } from "ai";
import { z } from "zod";

const tools = {
  searchDocs: tool({
    description: "Search the docs.",
    inputSchema: z.object({ query: z.string() }),
    execute: async ({ query }) => searchIndex(query),
  }),
};

export const myChat = chat.agent({
  id: "my-chat",
  tools, // so toModelOutput survives across turns
  run: async ({ messages, tools, signal }) =>
    streamText({
      ...chat.toStreamTextOptions({ tools }), // same set, handed back typed
      model: anthropic("claude-sonnet-4-5"),
      messages,
      abortSignal: signal,
      stopWhen: stepCountIs(15),
    }),
});

tools accepte aussi une fonction (event) => ToolSet résolue par tour, où event porte chatId, turn, continuation, et clientData.

3. Parties de données personnalisées (persistées vs transitoires)

Les parties data-* écrites via chat.response.write() dans run() (ou writer.write() dans les hooks) persistent dans responseMessage.parts et s'affichent dans onTurnComplete. Ajoutez transient: true pour les diffuser sans persister. Les écritures via chat.stream sont toujours éphémères.

// In run() - persists, surfaces in onTurnComplete's responseMessage
chat.response.write({ type: "data-context", data: { searchResults } });

// In a hook via writer - streams but does NOT persist
writer.write({ type: "data-progress", id: "search", data: { percent: 50 }, transient: true });

4. Type UIMessage personnalisé, données client, et hooks de builder

Pour les parties data-* typées ou une carte d'outils, construisez l'agent via chat.withUIMessage<T>() et chat.withClientData({ schema }). Les méthodes de builder se chaînent dans n'importe quel ordre ; les hooks de builder s'exécutent avant le hook de tâche correspondant. streamOptions devient la uiMessageStreamOptions par défaut (fusion superficielle, l'agent gagne).

export const myChat = chat
  .withUIMessage<MyChatUIMessage>({ streamOptions: { sendReasoning: true } })
  .withClientData({ schema: z.object({ userId: z.string() }) })
  .agent({
    id: "my-chat",
    tools: myTools,
    onTurnStart: async ({ uiMessages, writer }) => {
      writer.write({ type: "data-turn-status", data: { status: "preparing" } });
    },
    run: async ({ messages, tools, signal }) =>
      streamText({ ...chat.toStreamTextOptions({ tools }), model, messages, abortSignal: signal }),
  });

Construisez MyChatUIMessage comme UIMessage<unknown, MyDataTypes, InferUITools<typeof tools>> (ou, pour les outils uniquement, InferChatUIMessageFromTools<typeof tools> depuis @trigger.dev/sdk/ai). Sur le frontend, affinez useChat avec InferChatUIMessage<typeof myChat> depuis @trigger.dev/sdk/chat/react.

5. Hooks de cycle de vie et arrêt

chat.agent accepte des hooks qui se déclenchent dans un ordre fixe par tour :

onValidateMessages -> hydrateMessages -> onChatStart (chat's first message only)
  -> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnComplete

onBoot se déclenche une fois par processus worker (à chaque nouveau démarrage, y compris les exécutions de continuation) et c'est où résident chat.local, les connexions BD, et l'état par processus. onChatStart se déclenche uniquement au premier message du chat. Suspension/reprise utilisent onChatSuspend / onChatResume. Les options de config incluent tools, clientDataSchema, maxTurns (100), turnTimeout ("1h"), idleTimeoutInSeconds (30), uiMessageStreamOptions, et exitAfterPreloadIdle. Il n'y a pas de retry générique ; chat.agent s'exécute avec maxAttempts: 1 en interne.

Stop est crucial : le signal passé à run abandonne à l'arrêt ou l'annulation. Transmettez-le comme abortSignal à streamText, sinon le bouton Stop met à jour l'UI tandis que le modèle continue de générer côté serveur.

run: async ({ messages, signal }) =>
  streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });

6. Migration depuis une route streamText simple du SDK IA

Il n'y a pas de route API dans ce modèle. Le transport remplace l'aller-retour de la route, donc :

  • Supprimez le gestionnaire de route. Déplacez l'authentification par requête dans les deux server actions de l'étape Configuration 2.
  • Déplacez l'appel streamText dans run. Il reçoit déjà ModelMessage[] pré-convertie.
  • Retournez le StreamTextResult (il se pipe automatiquement) et ajoutez ...chat.toStreamTextOptions() en premier.
  • Sur le client, remplacez l'URL api par useTriggerChatTransport ; useChat conserve la même forme.

Erreurs courantes

  • CRITIQUE : oublier ...chat.toStreamTextOptions().

    // Wrong - compaction / steering / background injection silently no-op
    return streamText({ model, messages, abortSignal: signal });
    // Correct - spread FIRST so explicit overrides win
    return streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal });

    Cela câble le callback prepareStep derrière la compaction, la direction mi-tour, et l'injection en arrière-plan, injecte le prompt système depuis chat.prompt(), résout le modèle du registre, et ajoute la télémétrie. L'omettre rend tous ceux-ci silencieusement sans effet sans erreur.

  • Déclarer les outils uniquement sur streamText. Déclarez-les aussi sur chat.agent({ tools }), relisez-les depuis run, et passez chat.toStreamTextOptions({ tools }). Sinon, le toModelOutput de chaque outil s'exécute au tour 1 mais est ignoré quand l'historique est reconverti aux tours suivants.

  • Ne pas transmettre signal pour l'arrêt. Sans abortSignal: signal, Stop met à jour l'UI mais le modèle continue de générer côté serveur.

  • Initialiser chat.local dans onChatStart. Initialisez-le dans onBoot. onChatStart se déclenche une fois par chat, donc les exécutions de continuation la sautent et crash avec chat.local can only be modified after initialization. onBoot se déclenche à chaque nouveau worker.

  • Minter des tokens dans le navigateur. Ne jamais exposer la clé secrète d'environnement côté client. Mintez via les deux server actions ; le transport les appelle.

  • Effacer lastEventId sur chat.endRun(). Gardez le curseur pendant la durée de vie de la Session ; n'effacez-le que quand la Session elle-même se ferme. C'est étiqueté avec sessionId, donc l'effacer force une réabonnement depuis seq_num=0 qui peut atteindre le turn-complete obsolète du tour précédent et fermer le flux vide.

  • Retourner l'erreur brute depuis uiMessageStreamOptions.onError. Cela fuit les internals (clés, stack traces). Retournez plutôt une chaîne assainie.

Références

  • Skill trigger-chat-agent-advanced - hooks de cycle de vie en profondeur, sessions, primitives brutes (chat.createSession, chat.customAgent, chat.stream), compaction, approbations HITL, récupération.
  • Skill trigger-realtime - hooks Realtime et diffusion frontend au-delà du transport de chat.
  • Skill trigger-tasks - sémantique de base task(), ctx, et hooks de cycle de vie standard.

Les docs de référence livrent à côté de ce skill dans le même package, lisez-les localement (pas de réseau), épinglées à votre version SDK installée. Le frontmatter sources: ci-dessus liste chaque doc que ce skill exploite, tous sous @trigger.dev/sdk/docs/ai-chat/. Commencez par quick-start.mdx, backend.mdx, tools.mdx, types.mdx, frontend.mdx.

Un chat.agent est une tâche Trigger.dev, donc il se construit et se déploie comme n'importe quelle autre. Pour trigger.config.ts et les extensions de build (Prisma, Playwright, Python, FFmpeg, etc. — par ex. quand un outil les nécessite), lisez les docs de config groupées sous @trigger.dev/sdk/docs/config/ (les extensions se trouvent dans config/extensions/, commençant par overview.mdx).

Version

Ce skill est groupé à l'intérieur de @trigger.dev/sdk et lu directement depuis node_modules, donc il correspond toujours à votre version SDK installée (voir le package.json adjacent). La documentation complète de ces APIs se livre à côté sous @trigger.dev/sdk/docs/.

Skills similaires