mcpField
T
trigger-authoring-chat-agent

trigger-authoring-chat-agent

github.com/triggerdotdevエージェントワークフロー35242 回インストール1 週間前に更新
リポジトリ

trigger-authoring-chat-agent について

会話型エージェントを長時間実行する Trigger.dev タスクとして構築するためのスキルです。サーバー側の会話状態、ストリーミング応答、型付きツール、ライフサイクルフック、API ルートを使わないフロントエンド接続を解説します。

主な機能

  1. 01長時間実行するチャットエージェント
  2. 02モデル応答のストリーミング
  3. 03型付きツールとメッセージデータ
  4. 04ターンのライフサイクルフック
  5. 05フロントエンド接続とサーバーアクション

活用シーン

  1. 01Vercel AI SDK を使ったチャットエージェントの構築
  2. 02API ルートなしのチャットストリーミング
  3. 03型付きツールと独自データの追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/triggerdotdev/skills --skill trigger-authoring-chat-agent

インストール後、Claude Code で/trigger-authoring-chat-agentを入力して読み込まれたか確認します。

詳細

チャットエージェントの作成

chat.agent は、会話全体を1つの長時間実行される Trigger.dev タスクとして実行します。メッセージが届くと起動し、届かなくなると停止します。メモリ内の状態は、ページの再読み込み、デプロイ、アイドル状態の期間、クラッシュを越えて保持されます。コードは、普段書くループそのものです。メッセージを受け取り、streamText を返します。API ルートはありません。フロントエンドは TriggerChatTransport を通じてエージェントと通信するため、履歴はサーバー側に蓄積され、クライアントは各ターンで新しいメッセージだけを送信します。

Vercel AI SDK v5、v6、v7 に対応しています。v7 ではモデル呼び出しをトレースできるよう、@ai-sdk/otel もインストールしてください(SDK が自動的に登録します)。

セットアップ

必要な要素は3つです。エージェントタスク、2つのサーバーアクション、フロントエンドのトランスポートです。

1. エージェントを定義する
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. 2つのサーバーアクションを追加する

どちらもサーバー上で実行されるため、ブラウザーが環境シークレットキーを保持することはありません。ユーザーごと、プランごとの認可や、関連する DB 書き込みもここで行います。

"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".
}

トランスポートはメモ化されます(1度作成され、レンダリングをまたいで再利用されます)。typeof myChat を渡すことで、エージェントのメッセージ型が 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 も指定できます。この関数には、chatId、turn、continuation、clientData を含む event が渡されます。

3. カスタムデータパーツ(永続化と一時的なデータ)

run() 内の chat.response.write()(またはフック内の writer.write())で書き込まれた data-* パーツは、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 }) を通じてエージェントを構築します。ビルダーメソッドは任意の順序で連結でき、ビルダーフックは対応するタスクフックの前に実行されます。streamOptions は uiMessageStreamOptions のデフォルトになり、浅くマージされます(エージェント側の値が優先されます)。

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 はワーカープロセスごとに1度(継続実行を含む、新しい起動ごとに)実行されます。chat.local、DB 接続、プロセスごとの状態はここに置きます。onChatStart はチャットの最初のメッセージでのみ実行されます。中断と再開には onChatSuspend/onChatResume を使用します。設定オプションには tools、clientDataSchema、maxTurns(100)、turnTimeout("1h")、idleTimeoutInSeconds(30)、uiMessageStreamOptions、exitAfterPreloadIdle があります。汎用の retry はありません。内部では chat.agent は maxAttempts: 1 で実行されます。

停止の挙動は、これに依存します。run に渡される signal は、停止またはキャンセル時に中断します。これを streamText の abortSignal として渡さないと、モデルがサーバー側で生成を続けているのに、停止ボタンを押すと UI だけが更新されてしまいます。

run: async ({ messages, signal, streamText }) =>
  streamText({ model, messages, abortSignal: signal, stopWhen: stepCountIs(15) });
6. プレーンな AI SDK streamText ルートからの移行

このモデルには API ルートがありません。トランスポートがルートへの往復を置き換えるため、次のようにします。

  • ルートハンドラーを削除します。リクエストごとの認証は、Setup のステップ 2 にある 2 つのサーバーアクションに移します。
  • streamText の呼び出しを run に移します。すでに変換済みの ModelMessage[] を受け取ります。
  • StreamTextResult を返します(自動的にパイプ処理されます)。また、streamText は ai からではなく、run の引数から取得します。
  • クライアントでは、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() でも同じことを手動で行えます。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 というエラーが発生します。onBoot は新しいワーカーの起動ごとに実行されます。

  • ブラウザーでトークンを発行する。 環境シークレットキーをクライアント側に公開してはいけません。2 つのサーバーアクションを通じて発行します。トランスポートがそれらを呼び出します。

  • chat.endRun() で lastEventId をクリアする。 セッションの存続期間中はカーソルを保持し、クリアするのはセッション自体を閉じるときだけです。カーソルは 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/ 以下に同梱されています。

コメント