trigger-chat-agent-advanced

Par triggerdotdev · skills

Capacités avancées et opérationnelles de `chat.agent` pour Trigger.dev, chargées à la demande. Chargez ce skill lorsque vous travaillez sur la primitive Sessions brute (sessions / `SessionHandle`), un transport de chat personnalisé ou le protocole wire realtime, les sous-agents durables (`AgentChat`, `chat.stream.writer`), le human-in-the-loop, le steering, les actions, l'injection en arrière-plan (`chat.defer` / `chat.inject`), les démarrages rapides (preload, Head Start via `@trigger.dev/sdk/chat-server`), la résilience du contexte (compaction, recovery boot, OOM, large payloads), l'état scoped à l'exécution `chat.local`, les tests hors ligne avec `mockChatAgent`, ou les mises à niveau de version/préreleases. Pour la définition quotidienne de `chat.agent({...})` et le happy path `useTriggerChatTransport`, utilisez plutôt le skill `trigger-authoring-chat-agent`.

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

chat.agent : avancé et opérationnel

chat.agent repose sur Sessions : une paire de canaux I/O bidirectionnels durables et liés à une tâche, identifiés par un externalId stable (ex. chatId) qui survit à tout exécution unique. Cette compétence couvre les couches en dessous et autour de l'agent quotidien : l'API brute sessions, le AgentChat côté serveur, les sous-agents durables, les actions / injection en arrière-plan, les démarrages rapides, la compaction et la récupération, ainsi que le protocole filaire pour les transports personnalisés.

Deux espaces de noms chat sont faciles à confondre : la définition de l'agent importe chat depuis @trigger.dev/sdk/ai ; les entrées du serveur Head Start / Node-listener importent chat depuis @trigger.dev/sdk/chat-server.

Configuration

Chemin heureux : piloter un agent à partir du code côté serveur (tâche, webhook ou script) avec AgentChat.

import { AgentChat } from "@trigger.dev/sdk/chat";
import type { myAgent } from "./trigger/my-agent";

const chat = new AgentChat<typeof myAgent>({ agent: "my-chat", clientData: { userId: "user_123" } });
const stream = await chat.sendMessage("Review PR #42");
const text = await stream.text();
await chat.close();

sendMessage() déclenche une exécution au premier appel, puis la réutilise via des flux d'entrée. ChatStream expose text(), result() ({ text, toolCalls, toolResults }), messages() (captures UIMessage), et le flux brut .stream. Autres méthodes : steer(text), stop(), sendRaw(uiMessages), sendAction(action), preload(), reconnect().

Motifs essentiels

1. Sessions brutes pour I/O bidirectionnel non-chat

Utilisez sessions directement quand l'abstraction chat ne convient pas : boîtes de réception d'agents, flux d'approbation, pipelines serveur-à-serveur. sessions.start est idempotent sur (env, externalId) ; externalId ne peut pas commencer par session_.

import { sessions } from "@trigger.dev/sdk";

const { id, publicAccessToken } = await sessions.start({
  type: "chat.agent",
  externalId: chatId,
  taskIdentifier: "my-chat",
  triggerConfig: { tags: [`chat:${chatId}`], basePayload: { chatId, trigger: "preload" } },
});

const session = sessions.open(chatId); // pas d'appel réseau ; les méthodes sont lazy
await session.out.append({ kind: "message", text: "hello" });
const next = await session.in.once<MyEvent>({ timeoutMs: 30_000 });

sessions.open(id).in a aussi send, on(handler), peek, wait (suspend l'exécution, seulement dans task.run()), et waitWithIdleTimeout. .out a append, pipe, writer, read, writeControl, et trimTo. Listez avec sessions.list({ type, tag, status, ... }) (for await), mutez avec sessions.update, terminez avec sessions.close (terminal, idempotent).

2. Sous-agent durable comme outil en streaming

AgentChat à l'intérieur d'un tool() du SDK AI délègue à un sous-agent durable ; sa réponse fait stream comme résultats d'outils préliminaires. Donnez au tool un toModelOutput pour que le modèle voit un résumé compact.

import { tool } from "ai";
import { AgentChat } from "@trigger.dev/sdk/chat";
import { z } from "zod";

const researchTool = tool({
  description: "Delegate research to a specialist agent.",
  inputSchema: z.object({ topic: z.string() }),
  execute: async function* ({ topic }, { abortSignal }) {
    const chat = new AgentChat({ agent: "research-agent" });
    const stream = await chat.sendMessage(topic, { abortSignal });
    yield* stream.messages(); // les snapshots UIMessage deviennent des résultats d'outils préliminaires
    await chat.close();
  },
  toModelOutput: ({ output: message }) => {
    const lastText = message?.parts?.findLast((p: { type: string }) => p.type === "text") as
      | { text?: string }
      | undefined;
    return { type: "text", value: lastText?.text ?? "Done." };
  },
});

Pour une sous-tâche exposée via execute: ai.toolExecute(task), streamez la progression à l'exécution de l'agent avec chat.stream.writer({ target: "root" }). target accepte "self" | "parent" | "root" | <runId>. À l'intérieur de la sous-tâche, lisez le contexte avec ai.toolCallId() et ai.chatContextOrThrow<typeof myChat>() ({ chatId, turn, continuation, clientData }).

import { chat, ai } from "@trigger.dev/sdk/ai";

const { waitUntilComplete } = chat.stream.writer({
  target: "root",
  execute: ({ write }) =>
    write({ type: "data-research-status", id: partId, data: { query, status: "in-progress" } }),
});
await waitUntilComplete();

3. Injection en arrière-plan : defer + inject

chat.defer(promise) exécute du travail en parallèle du streaming (toutes les promesses différées sont attendues, avec un timeout de 5 s, avant onTurnComplete). chat.inject(messages) met en queue des ModelMessage[] qui se vident au prochain démarrage de tour ou limite prepareStep.

export const myChat = chat.agent({
  id: "my-chat",
  onTurnComplete: async ({ messages }) => {
    chat.defer(
      (async () => {
        const analysis = await analyzeConversation(messages);
        chat.inject([{ role: "system", content: `[Analysis]\n\n${analysis}` }]);
      })()
    );
  },
  run: async ({ messages, signal }) =>
    streamText({ ...chat.toStreamTextOptions({ registry }), messages, abortSignal: signal, stopWhen: stepCountIs(15) }),
});

4. Compaction (basée sur seuil)

compaction.shouldCompact décide quand, summarize produit le résumé qui remplace les messages du modèle. Les messages UI sont préservés par défaut (personnalisez via compactUIMessages). Le prepareStep qui effectue la compaction de boucle interne est auto-injecté par chat.toStreamTextOptions() ; un prepareStep que vous passez après le spread gagne.

compaction: {
  shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
  summarize: async ({ messages }) =>
    (await generateText({
      model: anthropic("claude-haiku-4-5"),
      messages: [...messages, { role: "user", content: "Summarize concisely." }],
    })).text,
},

5. Actions : muter l'état sans un tour

actionSchema valide ; onAction mute via chat.history (slice, replace, rollbackTo, remove, getPendingToolCalls, extractNewToolResults). Les actions déclenchent hydrateMessages et onAction seulement, jamais run() ou les hooks de tour. Retournez un StreamTextResult, string, ou UIMessage pour également émettre une réponse du modèle.

export const myChat = chat.agent({
  id: "my-chat",
  actionSchema: z.discriminatedUnion("type", [
    z.object({ type: z.literal("undo") }),
    z.object({ type: z.literal("rollback"), targetMessageId: z.string() }),
  ]),
  onAction: async ({ action }) => {
    if (action.type === "undo") chat.history.slice(0, -2);
    if (action.type === "rollback") chat.history.rollbackTo(action.targetMessageId);
  },
  run: async ({ messages, signal }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
});

Envoyez depuis le navigateur avec transport.sendAction(chatId, { type: "undo" }), ou côté serveur avec agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" }).

6. Démarrages rapides : Head Start

chat.headStart (depuis @trigger.dev/sdk/chat-server, PAS /ai) retourne un gestionnaire Web Fetch qui sert le tour 1 depuis votre propre processus chaud, puis remet à l'agent sur les tours 2+. Les tools passés ici doivent être schéma-uniquement (un module important ai + zod seulement) ; les executes lourds restent dans la tâche.

import { chat } from "@trigger.dev/sdk/chat-server";
import { streamText, stepCountIs } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { headStartTools } from "@/lib/chat-tools/schemas";

export const chatHandler = chat.headStart({
  agentId: "my-chat",
  run: async ({ chat: helper }) =>
    streamText({
      ...helper.toStreamTextOptions({ tools: headStartTools }),
      model: anthropic("claude-sonnet-4-6"),
      system: "You are helpful.",
      stopWhen: stepCountIs(15),
    }),
});
// Next.js: export const POST = chatHandler;  Transport: headStart: "/api/chat"

Les frameworks Node-only enveloppent un gestionnaire Web Fetch avec chat.toNodeListener(handler). Utilisez le même modèle des deux côtés pour éviter un changement de ton entre le tour 1 et les tours 2+.

7. chat.local : initialiser dans onBoot, pas onChatStart

chat.local<T>({ id }) est au niveau du module, proxy peu profond, état limité à l'exécution. Initialisez-le dans onBoot (s'exécute sur tout worker frais, y compris les exécutions de continuation), jamais dans onChatStart.

const userContext = chat.local<{ name: string; plan: "free" | "pro" }>({ id: "userContext" });

export const myChat = chat.agent({
  id: "my-chat",
  onBoot: async ({ clientData }) => userContext.init({ name: "Alice", plan: "pro" }),
  run: async ({ messages, signal }) => streamText({ /* ... */ }),
});

8. Messages en attente (entrée utilisateur en cours de streaming)

Un message envoyé pendant qu'un tour est en streaming ne doit PAS annuler le flux. Configurez pendingMessages (shouldInject, prepare, onReceived, onInjected) sur l'agent pour que le prepareStep auto-injecté du SDK les replie à la prochaine limite. En frontend, usePendingMessages retourne pending, steer(text), queue(text), et promoteToSteering(id) ; envoyez via transport.sendPendingMessage(chatId, uiMessage, metadata?).

9. Récupération et mises à niveau de version

onRecoveryBoot s'exécute uniquement quand un message assistant partiel existe à la queue (déploiement interrompu, crash, OOM retry). Il ne s'exécute PAS sur chat.requestUpgrade(), qui est une sortie gracieuse sans partiel. chat.requestUpgrade() (appelé dans onTurnStart / onValidateMessages pour ignorer run(), ou dans run() / chat.defer() pour quitter après le tour) fait tourner le currentRunId de la Session à une exécution sur le dernier déploiement sans reconnexion client. Appariez-le avec une version de contrat sur clientData.

const SUPPORTED_VERSIONS = new Set(["v2", "v3"]);
onTurnStart: async ({ clientData }) => {
  if (clientData?.protocolVersion && !SUPPORTED_VERSIONS.has(clientData.protocolVersion)) {
    chat.requestUpgrade();
  }
},

Pour la résilience OOM, définissez oomMachine (et machine) sur l'agent pour que les retries se posent sur un préréglage plus grand.

10. Tests hors ligne avec mockChatAgent

@trigger.dev/sdk/ai/test exécute la vraie boucle de tour en mémoire. Importez-le avant le module d'agent pour que le catalogue de ressources soit installé. Pilotez avec sendMessage, sendRegenerate, sendAction, sendStop, sendHeadStart, sendHandover ; semez l'état avec seedSnapshot / seedSessionOutTail / seedSessionOutPartial / seedSessionInTail ; affirmez contre turn.chunks et harness.allChunks.

import { mockChatAgent } from "@trigger.dev/sdk/ai/test"; // AVANT le module d'agent
import { myChatAgent } from "./my-chat.js";

const harness = mockChatAgent(myChatAgent, { chatId: "test-1", clientData: { model } });
try {
  const turn = await harness.sendMessage({ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] });
  // affirmez contre turn.chunks
} finally {
  await harness.close();
}

Les options incluent mode ("preload" | "submit-message" | "handover-prepare" | "continuation"), preload, continuation, previousRunId, snapshot, taskContext, et setupLocals. Définissez taskContext.ctx.attempt.number > 1 pour simuler une tentative OOM-retry. runInMockTaskContext pilote une tâche non-chat hors ligne.

11. Transport personnalisé : le protocole filaire

Points de terminaison : POST /api/v1/sessions (créer), GET /realtime/v1/sessions/{id}/out (SSE), POST /realtime/v1/sessions/{id}/in/append, POST /api/v1/sessions/{id}/close. ChatInputChunk est { kind: "message"; payload: ChatTaskWirePayload } | { kind: "stop"; message? }. Le ChatTaskWirePayload porte chatId, trigger (submit-message | regenerate-message | preload | close | action | handover-prepare), message?, metadata?, action?, continuation?, previousRunId?, et plus. Les enregistrements de contrôle sont en forme d'en-tête : trigger-control: turn-complete (avec public-access-token, session-in-event-id optionnels) et trigger-control: upgrade-required. Les aides TS SSEStreamSubscription et controlSubtype(headers) (documentés dans docs/ai-chat/client-protocol.mdx) gèrent le décodage par lots et le filtrage des enregistrements de contrôle pour vous.

Erreurs courantes

  • CRITIQUE : envoyer un suivi en re-POSTant POST /api/v1/sessions.

    // Mauvais - un re-POST en cache abandonne silencieusement basePayload.message ; basePayload est la config de trigger, pas un canal
    await fetch("/api/v1/sessions", { method: "POST", body: JSON.stringify({ ...createBody }) });
    // Correct - ajouter au canal d'entrée de la session
    await fetch(`/realtime/v1/sessions/${id}/in/append`, { method: "POST", body: JSON.stringify({ kind: "message", payload }) });
  • Utiliser le mauvais token pour .in / .out. Utilisez publicAccessToken du corps de réponse create (limité à session). L'en-tête de réponse x-trigger-jwt est limité à l'exécution et ne peut pas s'abonner.

  • Initialiser chat.local dans onChatStart. Il est ignoré sur les exécutions de continuation, donc run() plante avec chat.local can only be modified after initialization. Initialisez dans onBoot.

  • chat.defer pour l'écriture d'historique de messages. Une actualisation en cours de streaming lirait []. await cette écriture en ligne avant le streaming du modèle ; réservez chat.defer pour l'analytique, l'audit, le préchauffage du cache.

  • Donner au tool HITL un execute. streamText l'appelle immédiatement. Laissez-le sans execute ; le frontend fournit la réponse via addToolOutput + sendAutomaticallyWhen.

  • Déclarer les sous-agents / tools lourds seulement sur streamText. Déclarez-les aussi sur chat.agent({ tools }) (ou passez à convertToModelMessages(uiMessages, { tools }) dans un agent personnalisé) pour que toModelOutput se réapplique à chaque tour.

  • Importer des tools à execute lourd dans le module route Head Start. C'est un problème de chaîne d'import au moment de la construction ; les aides de strip runtime ne le corrigent pas. Gardez les schémas dans un module ai + zod-uniquement.

  • Retourner une sortie d'outil de plusieurs mégaoctets sur le flux. Un enregistrement tool-output-available sur ~1 MiB lève ChatChunkTooLargeError. Persistez dans votre store, écrivez la ligne d'abord, puis émettez seulement un id.

  • Définir X-Peek-Settled: 1 sur le chemin d'envoi actif. Il court-circuite le premier chunk du prochain tour et ferme le flux tôt. Utilisez-le seulement sur les chemins de reconnexion-en-rechargement.

Note sur le vocabulaire des docs : les exemples côté agent dans certaines docs utilisent toujours le type de chunk trigger:turn-complete hérité. C'est le vocabulaire d'émission agent. Un lecteur personnalisé doit filtrer sur l'en-tête trigger-control, pas sur chunk.type.

Les chats agents pilotés par MCP (list_agents, start_agent_chat, send_agent_message, close_agent_chat) sont des tools serveur MCP utilisés depuis Claude Code / Cursor, non des fonctions SDK importables. Voir /mcp-tools#agent-chat-tools.

Références

  • Compétence trigger-authoring-chat-agent - la définition quotidienne chat.agent({...}), les hooks du cycle de vie, et le chemin heureux useTriggerChatTransport. Commencez là avant d'atteindre cette compétence.
  • Compétence trigger-realtime - Realtime hooks et streaming frontend au-delà du transport chat.
  • Compétence trigger-tasks - sémantique base task(), ctx, et hooks du cycle de vie standard.

Les docs de référence sont expédiées à côté de cette compétence dans le même paquet, lisez-les localement (pas de réseau), épinglées à votre version installée. Le frontmatter sources: ci-dessus liste chaque doc que cette compétence utilise, tous sous @trigger.dev/sdk/docs/ai-chat/ (y compris patterns/). Pour HITL, sessions et sous-agents, commencez par sessions.mdx, server-chat.mdx, client-protocol.mdx, patterns/human-in-the-loop.mdx, patterns/sub-agents.mdx.

Pour trigger.config.ts et les extensions de build qu'une tâche chat-agent peut nécessiter (Prisma, Playwright, Python, etc.), lisez les docs de config fournis sous @trigger.dev/sdk/docs/config/ (config/extensions/ pour la configuration par extension).

Version

Cette compétence est fournie à l'intérieur de @trigger.dev/sdk et lue directement depuis node_modules, elle correspond toujours à votre version SDK installée (voir le package.json adjacent). La documentation complète de ces APIs est expédiée avec sous @trigger.dev/sdk/docs/.

Skills similaires