mcpField
T
trigger-authoring-chat-agent

trigger-authoring-chat-agent

github.com/triggerdotdevAgent 工作流35242 次安装1 周前更新
代码仓库

关于 trigger-authoring-chat-agent

面向开发者的技能,介绍如何将对话 Agent 构建为长期运行的 Trigger.dev 任务,涵盖服务端对话状态、流式响应、类型化工具、生命周期钩子及无需 API 路由的前端传输。

主要功能

  1. 01长期运行的聊天 Agent 任务
  2. 02模型响应流式传输
  3. 03类型化工具与消息数据
  4. 04对话轮次生命周期钩子
  5. 05前端传输与服务端操作

适用场景

  1. 01使用 Vercel AI SDK 构建聊天 Agent
  2. 02无需 API 路由实现聊天流
  3. 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 获取。
  • 在客户端,将 api URL 替换为 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/ 下。

评论

支持 Markdown