Loading...
Loading...
Author and run a durable AI chat agent with chat.agent from @trigger.dev/sdk/ai: the per-turn run loop, why you MUST spread ...chat.toStreamTextOptions() first, returning a StreamTextResult vs calling chat.pipe(), the two server actions (chat.createStartSessionAction + auth.createPublicToken), and wiring useChat to useTriggerChatTransport. Load this when building, modifying, or debugging a chat backend (the agent task or its lifecycle hooks) or its React transport, when declaring typed tools or custom data parts, or when migrating a plain AI SDK streamText route to chat.agent.
npx skill4agent add triggerdotdev/skills trigger-authoring-chat-agentchat.agentstreamTextTriggerChatTransport@ai-sdk/otelimport { 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),
}),
});runmessagesModelMessage[]UIMessage[]signalStreamTextResult"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",
});
}"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".
}typeof myChatuseChatstreamTextrunstreamTextawait chat.pipe(result)runvoidexport 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
}chat.agent({ tools })run()chat.toStreamTextOptions({ tools })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(event) => ToolSeteventchatIdturncontinuationclientDatadata-*chat.response.write()run()writer.write()responseMessage.partsonTurnCompletetransient: truechat.stream// 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 });data-*chat.withUIMessage<T>()chat.withClientData({ schema })streamOptionsuiMessageStreamOptionsexport 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 }),
});MyChatUIMessageUIMessage<unknown, MyDataTypes, InferUITools<typeof tools>>InferChatUIMessageFromTools<typeof tools>@trigger.dev/sdk/aiuseChatInferChatUIMessage<typeof myChat>@trigger.dev/sdk/chat/reactchat.agentonValidateMessages -> hydrateMessages -> onChatStart (chat's first message only)
-> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnCompleteonBootchat.localonChatStartonChatSuspendonChatResumetoolsclientDataSchemamaxTurnsturnTimeoutidleTimeoutInSecondsuiMessageStreamOptionsexitAfterPreloadIdleretrychat.agentmaxAttempts: 1signalrunabortSignalstreamTextrun: async ({ messages, signal }) =>
streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });streamTextstreamTextrunModelMessage[]StreamTextResult...chat.toStreamTextOptions()apiuseTriggerChatTransportuseChat...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 });prepareStepchat.prompt()streamTextchat.agent({ tools })runchat.toStreamTextOptions({ tools })toModelOutputsignalabortSignal: signalchat.localonChatStartonBootonChatStartchat.local can only be modified after initializationonBootlastEventIdchat.endRun()seq_num=0turn-completeuiMessageStreamOptions.onErrortrigger-chat-agent-advancedchat.createSessionchat.customAgentchat.streamtrigger-realtimetrigger-taskstask()ctxsources:@trigger.dev/sdk/docs/ai-chat/quick-start.mdxbackend.mdxtools.mdxtypes.mdxfrontend.mdxchat.agenttrigger.config.ts@trigger.dev/sdk/docs/config/config/extensions/overview.mdx@trigger.dev/sdknode_modulespackage.json@trigger.dev/sdk/docs/