Loading...
Loading...
Advanced and operational chat.agent capabilities for Trigger.dev, loaded on demand. Load this when working on the raw Sessions primitive (sessions / SessionHandle), a custom chat transport or the realtime wire protocol, durable sub-agents (AgentChat, chat.stream.writer), human-in-the-loop, steering, actions, background injection (chat.defer / chat.inject), fast starts (preload, Head Start via @trigger.dev/sdk/chat-server), context resilience (compaction, recovery boot, OOM, large payloads), chat.local run-scoped state, offline testing with mockChatAgent, or prerelease/version upgrades. For the everyday chat.agent({...}) definition and the useTriggerChatTransport happy path, use the trigger-authoring-chat-agent skill instead.
npx skill4agent add triggerdotdev/skills trigger-chat-agent-advancedchat.agentexternalIdchatIdsessionsAgentChatchatchat@trigger.dev/sdk/aichat@trigger.dev/sdk/chat-serverAgentChatimport { 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()ChatStreamtext()result(){ text, toolCalls, toolResults }messages().streamsteer(text)stop()sendRaw(uiMessages)sendAction(action)preload()reconnect()sessionssessions.start(env, externalId)externalIdsession_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); // no network call; methods are lazy
await session.out.append({ kind: "message", text: "hello" });
const next = await session.in.once<MyEvent>({ timeoutMs: 30_000 });sessions.open(id).insendon(handler)peekwaittask.run()waitWithIdleTimeout.outappendpipewriterreadwriteControltrimTosessions.list({ type, tag, status, ... })for awaitsessions.updatesessions.closeAgentChattool()toModelOutputimport { 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(); // UIMessage snapshots become preliminary tool results
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." };
},
});execute: ai.toolExecute(task)chat.stream.writer({ target: "root" })target"self" | "parent" | "root" | <runId>ai.toolCallId()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();chat.defer(promise)onTurnCompletechat.inject(messages)ModelMessage[]prepareStepexport 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) }),
});compaction.shouldCompactsummarizecompactUIMessagesprepareStepchat.toStreamTextOptions()prepareStepcompaction: {
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,
},actionSchemaonActionchat.historyslicereplacerollbackToremovegetPendingToolCallsextractNewToolResultshydrateMessagesonActionrun()StreamTextResultUIMessageexport 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 }),
});transport.sendAction(chatId, { type: "undo" })agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" })chat.headStart@trigger.dev/sdk/chat-server/aiaizodimport { 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"chat.toNodeListener(handler)chat.local<T>({ id })onBootonChatStartconst 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({ /* ... */ }),
});pendingMessagesshouldInjectprepareonReceivedonInjectedprepareStepusePendingMessagespendingsteer(text)queue(text)promoteToSteering(id)transport.sendPendingMessage(chatId, uiMessage, metadata?)onRecoveryBootchat.requestUpgrade()chat.requestUpgrade()onTurnStartonValidateMessagesrun()run()chat.defer()currentRunIdclientDataconst SUPPORTED_VERSIONS = new Set(["v2", "v3"]);
onTurnStart: async ({ clientData }) => {
if (clientData?.protocolVersion && !SUPPORTED_VERSIONS.has(clientData.protocolVersion)) {
chat.requestUpgrade();
}
},oomMachinemachine@trigger.dev/sdk/ai/testsendMessagesendRegeneratesendActionsendStopsendHeadStartsendHandoverseedSnapshotseedSessionOutTailseedSessionOutPartialseedSessionInTailturn.chunksharness.allChunksimport { mockChatAgent } from "@trigger.dev/sdk/ai/test"; // BEFORE the agent module
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" }] });
// assert against turn.chunks
} finally {
await harness.close();
}mode"preload" | "submit-message" | "handover-prepare" | "continuation"preloadcontinuationpreviousRunIdsnapshottaskContextsetupLocalstaskContext.ctx.attempt.number > 1runInMockTaskContextPOST /api/v1/sessionsGET /realtime/v1/sessions/{id}/outPOST /realtime/v1/sessions/{id}/in/appendPOST /api/v1/sessions/{id}/closeChatInputChunk{ kind: "message"; payload: ChatTaskWirePayload } | { kind: "stop"; message? }ChatTaskWirePayloadchatIdtriggersubmit-message | regenerate-message | preload | close | action | handover-preparemessage?metadata?action?continuation?previousRunId?trigger-control: turn-completepublic-access-tokensession-in-event-idtrigger-control: upgrade-requiredSSEStreamSubscriptioncontrolSubtype(headers)docs/ai-chat/client-protocol.mdxPOST /api/v1/sessions// Wrong - a cached re-POST silently drops basePayload.message; basePayload is trigger config, not a channel
await fetch("/api/v1/sessions", { method: "POST", body: JSON.stringify({ ...createBody }) });
// Correct - append to the session's input channel
await fetch(`/realtime/v1/sessions/${id}/in/append`, { method: "POST", body: JSON.stringify({ kind: "message", payload }) });.in.outpublicAccessTokenx-trigger-jwtchat.localonChatStartrun()chat.local can only be modified after initializationonBootchat.defer[]awaitchat.deferexecutestreamTextaddToolOutputsendAutomaticallyWhenstreamTextchat.agent({ tools })convertToModelMessages(uiMessages, { tools })toModelOutputaizodtool-output-availableChatChunkTooLargeErrorX-Peek-Settled: 1Note on docs vocabulary: agent-side examples in some docs still use the legacychunk type. That is the agent-emit vocabulary. A custom reader must filter on thetrigger:turn-completeheader, not ontrigger-control.chunk.typeMCP-driven agent chats (,list_agents,start_agent_chat,send_agent_message) are MCP server tools used from Claude Code / Cursor, not importable SDK functions. Seeclose_agent_chat./mcp-tools#agent-chat-tools
trigger-authoring-chat-agentchat.agent({...})useTriggerChatTransporttrigger-realtimetrigger-taskstask()ctxsources:@trigger.dev/sdk/docs/ai-chat/patterns/sessions.mdxserver-chat.mdxclient-protocol.mdxpatterns/human-in-the-loop.mdxpatterns/sub-agents.mdxtrigger.config.ts@trigger.dev/sdk/docs/config/config/extensions/@trigger.dev/sdknode_modulespackage.json@trigger.dev/sdk/docs/