trigger-authoring-chat-agent
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuthoring a chat agent
编写聊天代理
A runs an entire conversation as one long-lived Trigger.dev task. It wakes when a
message arrives, freezes when none do, and in-memory state survives page refreshes, deploys, idle
gaps, and crashes. Your code is the loop you would write anyway: messages in, out.
There are no API routes. The frontend talks to the agent through a , so
history accumulates server-side and the client ships only the new message each turn.
chat.agentstreamTextTriggerChatTransportWorks with Vercel AI SDK v5, v6, or v7. On v7 also install so model calls are traced
(the SDK registers it for you).
@ai-sdk/otelchat.agentstreamTextTriggerChatTransport兼容 Vercel AI SDK v5、v6 或 v7。使用 v7 时还需安装 ,以便追踪模型调用(SDK会自动注册该工具)。
@ai-sdk/otelSetup
配置步骤
Three pieces: the agent task, two server actions, and the frontend transport.
分为三部分:代理任务、两个服务器操作,以及前端传输层。
1. Define the agent
1. 定义代理
ts
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),
}),
});runmessagesModelMessage[]UIMessage[]signalStreamTextResultts
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({
// 必须首先展开此内容。详见「常见错误」。
...chat.toStreamTextOptions(),
model: anthropic("claude-sonnet-4-5"),
messages,
abortSignal: signal,
stopWhen: stepCountIs(15),
}),
});runModelMessage[]messagesUIMessage[]signalStreamTextResult2. Add two server actions
2. 添加两个服务器操作
Both run on your server, so the browser never holds your environment secret key. This is also
where per-user / per-plan authorization and any paired DB writes live.
ts
"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",
});
}这两个操作均在服务器端运行,因此浏览器不会持有你的环境密钥。你也可以在此处添加按用户/按计划的授权逻辑,以及相关的数据库写入操作。
ts
"use server";
import { auth } from "@trigger.dev/sdk";
import { chat } from "@trigger.dev/sdk/ai";
// 创建会话和首次运行,返回会话PAT。基于(env, chatId)实现幂等。
export const startChatSession = chat.createStartSessionAction("my-chat");
// 纯令牌生成逻辑。当传输层遇到401/403错误时会调用此方法刷新过期令牌。
export async function mintChatAccessToken(chatId: string) {
return auth.createPublicToken({
scopes: { read: { sessions: chatId }, write: { sessions: chatId } },
expirationTime: "1h",
});
}3. Wire the frontend
3. 关联前端
tsx
"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".
}The transport is memoized (created once, reused across renders). Passing flows the
agent's message type through .
typeof myChatuseChattsx
"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 提供编译期任务ID验证
accessToken: ({ chatId }) => mintChatAccessToken(chatId),
startSession: ({ chatId, clientData }) => startChatSession({ chatId, clientData }),
});
const { messages, sendMessage, stop, status } = useChat({ transport });
const [input, setInput] = useState("");
// 渲染消息列表、调用 sendMessage({ text: input }) 的表单,
// 以及在 status === "streaming" 时显示的「停止」按钮(点击时触发 stop)。
}传输层会被缓存(仅创建一次,在多次渲染中复用)。传入 会将代理的消息类型传递给 。
typeof myChatuseChatCore patterns
核心模式
1. Return vs pipe
1. 返回结果 vs 管道传输
Return the result from for the simple case. When is called deep
inside nested helpers, call from anywhere in the task instead, and let
resolve .
streamTextrunstreamTextawait chat.pipe(result)runvoidts
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
}简单场景下,直接从 返回 的结果即可。当 在嵌套工具内部调用时,可在任务的任意位置调用 ,并让 返回 。
runstreamTextstreamTextawait chat.pipe(result)runvoidts
export const agentChat = chat.agent({
id: "agent-chat",
run: async ({ messages }) => {
await runAgentLoop(messages); // 不返回结果,在内部进行管道传输
},
});
async function runAgentLoop(messages: ModelMessage[]) {
const result = streamText({
...chat.toStreamTextOptions(),
model: anthropic("claude-sonnet-4-5"),
messages,
});
await chat.pipe(result); // 在任务的任意位置均可生效
}2. Typed tools (declare on config AND spread back)
2. 类型化工具(需在配置中声明并回传)
Declare tools on , read them back typed from the payload, and pass
that set to . One declaration flows everywhere.
chat.agent({ tools })run()chat.toStreamTextOptions({ tools })ts
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) => ToolSeteventchatIdturncontinuationclientData在 中声明工具,从 的参数中读取已类型化的工具集,再将其传入 。一次声明即可在全流程复用。
chat.agent({ tools })run()chat.toStreamTextOptions({ tools })ts
import { tool, stepCountIs } from "ai";
import { z } from "zod";
const tools = {
searchDocs: tool({
description: "搜索文档。",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => searchIndex(query),
}),
};
export const myChat = chat.agent({
id: "my-chat",
tools, // 确保 toModelOutput 在多轮对话中持续生效
run: async ({ messages, tools, signal }) =>
streamText({
...chat.toStreamTextOptions({ tools }), // 复用同一工具集,且已类型化
model: anthropic("claude-sonnet-4-5"),
messages,
abortSignal: signal,
stopWhen: stepCountIs(15),
}),
});tools(event) => ToolSeteventchatIdturncontinuationclientData3. Custom data parts (persisted vs transient)
3. 自定义数据部分(持久化 vs 临时)
data-*chat.response.write()run()writer.write()responseMessage.partsonTurnCompletetransient: truechat.streamts
// 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 });在 中通过 (或在钩子中通过 )写入的 部分会持久化到 中,并在 中暴露。添加 可实现流式传输但不持久化。通过 写入的内容始终是临时的。
run()chat.response.write()writer.write()data-*responseMessage.partsonTurnCompletetransient: truechat.streamts
// 在 run() 中 - 持久化,会在 onTurnComplete 的 responseMessage 中暴露
chat.response.write({ type: "data-context", data: { searchResults } });
// 在钩子中通过 writer - 流式传输但不持久化
writer.write({ type: "data-progress", id: "search", data: { percent: 50 }, transient: true });4. Custom UIMessage type, client data, and builder hooks
4. 自定义 UIMessage 类型、客户端数据和构建器钩子
For typed parts or a tool map, build the agent through and
. Builder methods chain in any order; builder hooks run before the
matching task hook. becomes the default (shallow-merged,
agent wins).
data-*chat.withUIMessage<T>()chat.withClientData({ schema })streamOptionsuiMessageStreamOptionsts
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 }),
});Build as (or, for
tools only, from ). On the
frontend, narrow with from .
MyChatUIMessageUIMessage<unknown, MyDataTypes, InferUITools<typeof tools>>InferChatUIMessageFromTools<typeof tools>@trigger.dev/sdk/aiuseChatInferChatUIMessage<typeof myChat>@trigger.dev/sdk/chat/react如需类型化的 部分或工具映射,可通过 和 构建代理。构建器方法可按任意顺序链式调用;构建器钩子会在对应的任务钩子之前执行。 会成为默认的 (浅合并,代理配置优先级更高)。
data-*chat.withUIMessage<T>()chat.withClientData({ schema })streamOptionsuiMessageStreamOptionsts
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 }),
});将 定义为 (若仅需工具类型,可使用 中的 )。在前端,通过 中的 缩小 的类型范围。
MyChatUIMessageUIMessage<unknown, MyDataTypes, InferUITools<typeof tools>>@trigger.dev/sdk/aiInferChatUIMessageFromTools<typeof tools>@trigger.dev/sdk/chat/reactInferChatUIMessage<typeof myChat>useChat5. Lifecycle hooks and stop
5. 生命周期钩子与停止操作
chat.agenttext
onValidateMessages -> hydrateMessages -> onChatStart (chat's first message only)
-> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnCompleteonBootchat.localonChatStartonChatSuspendonChatResumetoolsclientDataSchemamaxTurnsturnTimeoutidleTimeoutInSecondsuiMessageStreamOptionsexitAfterPreloadIdleretrychat.agentmaxAttempts: 1Stop is load-bearing: the passed to aborts on stop or cancel. Forward it as
to , or the Stop button updates the UI while the model keeps generating
server-side.
signalrunabortSignalstreamTextts
run: async ({ messages, signal }) =>
streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });chat.agenttext
onValidateMessages -> hydrateMessages -> onChatStart(仅对话第一条消息触发)
-> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnCompleteonBootchat.localonChatStartonChatSuspendonChatResumetoolsclientDataSchemamaxTurnsturnTimeoutidleTimeoutInSecondsuiMessageStreamOptionsexitAfterPreloadIdleretrychat.agentmaxAttempts: 1停止操作至关重要:传入 的 会在停止或取消时触发中止。需将其作为 传入 ,否则「停止」按钮仅会更新UI,但服务器端模型仍会继续生成内容。
runsignalabortSignalstreamTextts
run: async ({ messages, signal }) =>
streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });6. Migrating from a plain AI SDK streamText
route
streamText6. 从普通AI SDK的 streamText
路由迁移
streamTextThere is no API route in this model. The transport replaces the route round-trip, so:
- Delete the route handler. Move per-request auth into the two server actions from Setup step 2.
- Move the call into
streamText. It already receives pre-convertedrun.ModelMessage[] - Return the (it auto-pipes) and add
StreamTextResultfirst....chat.toStreamTextOptions() - On the client, swap the URL for
api;useTriggerChatTransportstays the same shape.useChat
此模型中无需API路由,传输层替代了路由往返,因此:
- 删除路由处理程序。将每请求的授权逻辑移至配置步骤2中的两个服务器操作中。
- 将 调用移至
streamText函数中。它会自动接收已转换的run。ModelMessage[] - 返回 (会自动进行管道传输),并首先添加
StreamTextResult。...chat.toStreamTextOptions() - 在客户端,将 URL替换为
api;useTriggerChatTransport的使用方式保持不变。useChat
Common mistakes
常见错误
-
CRITICAL: forgetting.
...chat.toStreamTextOptions()ts// 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 });It wires thecallback behind compaction, mid-turn steering, and background injection, injects the system prompt fromprepareStep, resolves the registry model, and adds telemetry. Omitting it makes all of those silently no-op with no error.chat.prompt() -
Declaring tools only on. Also declare them on
streamText, read them back fromchat.agent({ tools }), and passrun. Otherwise each tool'schat.toStreamTextOptions({ tools })runs on turn 1 but is dropped when history is re-converted on later turns.toModelOutput -
Not forwardingfor stop. Without
signal, Stop updates the UI but the model keeps generating server-side.abortSignal: signal -
Initializingin
chat.local. Initialize it inonChatStart.onBootfires once per chat, so continuation runs skip it and crash withonChatStart.chat.local can only be modified after initializationfires on every fresh worker.onBoot -
Minting tokens in the browser. Never expose the environment secret key client-side. Mint via the two server actions; the transport calls them.
-
Clearingon
lastEventId. Keep the cursor for the Session lifetime; clear it only when the Session itself closes. It is sessionId-keyed, so clearing forces a resubscribe fromchat.endRun()that can hit the prior turn's staleseq_num=0and close the stream empty.turn-complete -
Returning the raw error from. It leaks internals (keys, stack traces). Return a sanitized string instead.
uiMessageStreamOptions.onError
-
严重错误:忘记添加。
...chat.toStreamTextOptions()ts// 错误 - 压缩/引导/后台注入会静默失效 return streamText({ model, messages, abortSignal: signal }); // 正确 - 首先展开,显式覆盖项优先级更高 return streamText({ ...chat.toStreamTextOptions(), model, messages, abortSignal: signal });此操作会在压缩、轮中引导和后台注入背后关联回调,注入来自prepareStep的系统提示,解析注册模型,并添加遥测。省略此操作会导致所有这些功能静默失效且无错误提示。chat.prompt() -
仅在中声明工具。 还需在
streamText中声明工具,从chat.agent({ tools })中读取工具集,并传入run。否则每个工具的chat.toStreamTextOptions({ tools })仅在第一轮生效,后续轮次转换历史时会被丢弃。toModelOutput -
未转发用于停止操作。 若未传入
signal,「停止」按钮仅会更新UI,但服务器端模型仍会继续生成内容。abortSignal: signal -
在中初始化
onChatStart。 应在chat.local中初始化。onBoot仅在每个对话触发一次,因此续跑会跳过此步骤并因onChatStart崩溃。chat.local can only be modified after initialization会在每个新工作进程启动时触发。onBoot -
在浏览器中生成令牌。 切勿在客户端暴露环境密钥。应通过两个服务器操作生成令牌;传输层会自动调用它们。
-
在时清除
chat.endRun()。 应在会话生命周期内保留游标;仅当会话本身关闭时才清除。它以sessionId为键,因此清除会强制从lastEventId重新订阅,可能会命中上一轮过期的seq_num=0并导致流空关闭。turn-complete -
从返回原始错误。 这会泄露内部信息(密钥、堆栈跟踪)。应返回经过清理的字符串。
uiMessageStreamOptions.onError
References
参考资料
- skill - lifecycle hooks in depth, sessions, raw-task primitives (
trigger-chat-agent-advanced,chat.createSession,chat.customAgent), compaction, HITL approvals, recovery.chat.stream - skill - Realtime hooks and frontend streaming beyond the chat transport.
trigger-realtime - skill - base
trigger-taskssemantics,task(), and standard lifecycle hooks.ctx
Reference docs ship beside this skill in the same package, read them locally (no network), pinned to your installed version. The frontmatter above lists every doc this skill draws from, all under . Start with , , , , .
sources:@trigger.dev/sdk/docs/ai-chat/quick-start.mdxbackend.mdxtools.mdxtypes.mdxfrontend.mdxA is a Trigger.dev task, so it builds and deploys like any other. For and build extensions (Prisma, Playwright, Python, FFmpeg, etc. — e.g. when a tool needs them), read the bundled config docs under (extensions are in , starting with ).
chat.agenttrigger.config.ts@trigger.dev/sdk/docs/config/config/extensions/overview.mdx- 技能 - 深入讲解生命周期钩子、会话、原始任务原语(
trigger-chat-agent-advanced、chat.createSession、chat.customAgent)、压缩、人工介入审批、恢复等功能。chat.stream - 技能 - 聊天传输层之外的实时钩子和前端流式传输。
trigger-realtime - 技能 - 基础
trigger-tasks语义、task()和标准生命周期钩子。ctx
参考文档与本技能捆绑在同一包中,可本地阅读(无需网络),且与你安装的SDK版本一致。上方的 前置元数据列出了本技能引用的所有文档,均位于 下。建议从 、、、、 开始阅读。
sources:@trigger.dev/sdk/docs/ai-chat/quick-start.mdxbackend.mdxtools.mdxtypes.mdxfrontend.mdxchat.agenttrigger.config.ts@trigger.dev/sdk/docs/config/config/extensions/overview.mdxVersion
版本
This skill is bundled inside and read directly from , so it always matches your installed SDK version (see the adjacent ). The full documentation for these APIs ships alongside it under .
@trigger.dev/sdknode_modulespackage.json@trigger.dev/sdk/docs/本技能捆绑在 中,直接从 读取,因此始终与你安装的SDK版本一致(可查看相邻的 )。这些API的完整文档位于 下。
@trigger.dev/sdknode_modulespackage.json@trigger.dev/sdk/docs/