关于 trigger-authoring-chat-agent
面向开发者的技能,介绍如何将对话 Agent 构建为长期运行的 Trigger.dev 任务,涵盖服务端对话状态、流式响应、类型化工具、生命周期钩子及无需 API 路由的前端传输。
主要功能
- 01长期运行的聊天 Agent 任务
- 02模型响应流式传输
- 03类型化工具与消息数据
- 04对话轮次生命周期钩子
- 05前端传输与服务端操作
适用场景
- 01使用 Vercel AI SDK 构建聊天 Agent
- 02无需 API 路由实现聊天流
- 03为聊天添加类型化工具和自定义数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ triggerdotdev/ skills --skill trigger-authoring-chat-agent安装后在 Claude Code 中输入/trigger-authoring-chat-agent确认已加载。
详细介绍
编写聊天 Agent
chat.agent 会将一整段对话作为一个长期运行的 Trigger.dev 任务。收到消息时,它会启动;没有新消息时,它会冻结;内存中的状态会在页面刷新、部署、空闲间隔和崩溃后继续保留。你只需编写原本就会写的循环:输入消息,输出 streamText。无需 API 路由。前端通过 TriggerChatTransport 与 Agent 通信,因此对话历史会在服务端累积,而客户端每轮只发送新消息。
支持 Vercel AI SDK v5、v6 或 v7。使用 v7 时,还需安装 @ai-sdk/otel,以追踪模型调用(SDK 会为你注册它)。
设置
由三部分组成:Agent 任务、两个服务端操作和前端传输层。
1. 定义 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",
// `streamText` below is the SDK's, from the run argument. See "Common mistakes".
run: async ({ messages, signal, streamText }) =>
streamText({
model: anthropic("claude-sonnet-4-5"),
messages,
abortSignal: signal,
stopWhen: stepCountIs(15),
}),
});
run 接收已经转换为 ModelMessage[] 的 messages(SDK 会为你将前端的 UIMessage[] 转换为此类型),以及在停止或取消时会触发中止的 signal。返回 StreamTextResult 会自动将其传输到前端。
2. 添加两个服务端操作
两者都在你的服务端运行,因此浏览器不会持有你的环境密钥。这里也可以处理按用户或套餐进行的授权,以及任何配套的数据库写入。
"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. 接入前端
"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 myChat,即可让 Agent 的消息类型传递到 useChat。
核心模式
1. 返回还是传输
简单情况下,从 run 返回 streamText 结果。如果在多层嵌套的辅助函数中调用 streamText,则可在任务中的任意位置调用 await chat.pipe(result),并让 run 返回 void。
import { chat, type ChatStreamText } from "@trigger.dev/sdk/ai";
import { anthropic } from "@ai-sdk/anthropic";
import type { ModelMessage } from "ai";
export const agentChat = chat.agent({
id: "agent-chat",
run: async ({ messages, streamText }) => {
await runAgentLoop(messages, streamText); // don't return; pipe inside
},
});
// A loop factored out of `run` takes `streamText` as an argument, so it keeps the
// managed options. `ChatStreamText` (from `@trigger.dev/sdk/ai`) types the parameter.
// `chat.toStreamTextOptions()` is the alternative when threading it down is impractical.
async function runAgentLoop(messages: ModelMessage[], streamText: ChatStreamText) {
const result = streamText({
model: anthropic("claude-sonnet-4-5"),
messages,
});
await chat.pipe(result); // works from anywhere in the task
}
2. 类型化工具(在配置中声明并传入)
在 chat.agent({ tools }) 中声明工具,从 run() 参数中以类型化方式读取它们,并将这组工具传入 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 }) =>
streamText({
model: anthropic("claude-sonnet-4-5"),
messages,
tools, // same set, handed back typed
abortSignal: signal,
stopWhen: stepCountIs(15),
}),
});
tools 也接受函数 (event) => ToolSet,并按轮次解析;其中 event 包含 chatId、turn、continuation 和 clientData。
3. 自定义数据部分(持久化与临时数据)
在 run() 中通过 chat.response.write() 写入的 data-* 部分(或在钩子中通过 writer.write() 写入的部分)会持久化到 responseMessage.parts,并显示在 onTurnComplete 中。添加 transient: true 可让其流式传输但不持久化。通过 chat.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 });
4. 自定义 UIMessage 类型、客户端数据和构建器钩子
对于类型化的 data-* 部分或工具映射,请通过 chat.withUIMessage<T>() 和 chat.withClientData({ schema }) 构建 Agent。构建器方法可以任意顺序链式调用;构建器钩子会在对应的任务钩子之前运行。streamOptions 会成为默认的 uiMessageStreamOptions(进行浅合并,Agent 优先)。
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 }) =>
streamText({ model, messages, tools, abortSignal: signal }),
});
将 MyChatUIMessage 构建为 UIMessage<unknown, MyDataTypes, InferUITools<typeof tools>>(或者仅针对工具,使用 @trigger.dev/sdk/ai 中的 InferChatUIMessageFromTools<typeof tools>)。在前端,使用 @trigger.dev/sdk/chat/react 中的 InferChatUIMessage<typeof myChat> 为 useChat 缩窄类型。
5. 生命周期钩子与停止
chat.agent 接受按固定的每轮顺序触发的钩子:
onValidateMessages -> storage.loadContext (or the deprecated hydrateMessages)
-> onChatStart (chat's first message only)
-> onTurnStart -> run() -> onBeforeTurnComplete -> onTurnComplete -> storage.save
onBoot 在每个工作进程启动时触发一次(每次全新启动都会触发,包括继续运行的情况),可用于初始化 chat.local、数据库连接和进程级状态。onChatStart 仅在聊天的第一条消息到达时触发。挂起和恢复分别使用 onChatSuspend / onChatResume。配置选项包括 tools、clientDataSchema、maxTurns(100)、turnTimeout("1h")、idleTimeoutInSeconds(30)、uiMessageStreamOptions 和 exitAfterPreloadIdle。没有通用的 retry;chat.agent 内部以 maxAttempts: 1 运行。
停止功能取决于此设置:传给 run 的 signal 会在停止或取消时触发中止。将它转发为 streamText 的 abortSignal,否则即使停止按钮更新了界面,模型仍会在服务端继续生成。
run: async ({ messages, signal, streamText }) =>
streamText({ model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });
6. 从普通 AI SDK streamText 路由迁移
此模型中没有 API 路由。传输层会取代路由往返,因此:
- 删除路由处理程序。将每个请求的身份验证移到 Setup 第 2 步中的两个服务端操作。
- 将
streamText调用移到run中。它已接收预先转换的ModelMessage[]。 - 返回
StreamTextResult(它会自动管道传输),并从run的参数中获取streamText,而不是从ai获取。 - 在客户端,将
apiURL 替换为useTriggerChatTransport;useChat的形式保持不变。
常见错误
-
严重:调用从
ai导入的streamText。// Wrong - compaction / steering / background injection silently no-op import { streamText } from "ai"; run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }); // Correct - the run argument's streamText carries the managed options run: async ({ messages, signal, streamText }) => streamText({ model, messages, abortSignal: signal });SDK 自带的
streamText包含由压缩、轮次中途引导和后台注入触发的prepareStep、来自chat.prompt()或chat.agent({ system })的系统提示词、由注册表解析的模型,以及遥测。导入的streamText不包含其中任何内容,且不会报错。...chat.toStreamTextOptions()可以手动完成相同的工作;由于自定义 Agent 没有run参数,因此必须使用它。chat.headStart路由也会获得绑定的streamText,且该函数还负责管理messages、prompt、stopWhen和abortSignal。展开它之后再重新设置tools或prepareStep会替换已托管的值;而run参数中的streamText会合并tools,并组合prepareStep。 -
只在
streamText上声明工具。 也要在chat.agent({ tools })上声明工具,从run中读取它们,并将该工具集作为tools传入。否则,每个工具的toModelOutput会在第 1 轮运行,但在后续轮次重新转换历史记录时会被丢弃。 -
未传递用于停止的
signal。 如果没有abortSignal: signal,停止操作会更新 UI,但模型仍会在服务端继续生成。 -
在
onChatStart中初始化chat.local。 应在onBoot中初始化。onChatStart每个聊天只会触发一次,因此后续运行会跳过它,并因chat.local can only be modified after initialization而崩溃。每个新的 worker 都会触发onBoot。 -
在浏览器中创建令牌。 切勿在客户端暴露环境密钥。通过两个服务端操作创建令牌;传输层会调用它们。
-
在
chat.endRun()时清除lastEventId。 在 Session 的整个生命周期内保留游标;只有在 Session 本身关闭时才清除。它以 sessionId 为键,因此清除后会从seq_num=0重新订阅,可能碰到上一轮过期的turn-complete事件,导致流以空内容结束。 -
从
uiMessageStreamOptions.onError返回原始错误。 这会泄露内部信息(密钥、堆栈跟踪)。请改为返回已清理的字符串。
参考资料
trigger-chat-agent-advanced技能——深入了解生命周期钩子、会话、原始任务基元(chat.createSession、chat.customAgent、chat.stream)、压缩、HITL 审批和恢复。trigger-realtime技能——聊天传输之外的 Realtime 钩子和前端流式传输。trigger-tasks技能——基础task()语义、ctx和标准生命周期钩子。
参考文档与此技能一同打包在同一个包中,请在本地阅读(无需联网),文档版本与已安装版本一致。上方的 sources: frontmatter 列出了此技能所依据的所有文档,它们都位于 @trigger.dev/sdk/docs/ai-chat/ 下。建议从 quick-start.mdx、backend.mdx、tools.mdx、types.mdx 和 frontend.mdx 开始阅读。
chat.agent 是一个 Trigger.dev 任务,因此其构建和部署方式与其他任务相同。有关 trigger.config.ts 和构建扩展(Prisma、Playwright、Python、FFmpeg 等,例如工具需要这些依赖时),请阅读 @trigger.dev/sdk/docs/config/ 下打包的配置文档(扩展位于 config/extensions/,请从 overview.mdx 开始)。
版本
此技能打包在 @trigger.dev/sdk 中,并直接从 node_modules 读取,因此始终与已安装的 SDK 版本一致(参见相邻的 package.json)。这些 API 的完整文档也与此技能一同打包,位于 @trigger.dev/sdk/docs/ 下。