mcpField
T
trigger-chat-agent-advanced

trigger-chat-agent-advanced

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

trigger-chat-agent-advanced について

Trigger.dev のチャットエージェントを運用する開発者向けの上級スキルです。Sessions、サーバー側の AgentChat、永続的なサブエージェント、アクション、圧縮、復旧、カスタムトランスポートと低レベル API を扱います。

主な機能

  1. 01低レベル Sessions API と AgentChat
  2. 02永続的なストリーミング型サブエージェント
  3. 03バックグラウンド注入と圧縮
  4. 04会話アクションと高速起動
  5. 05復旧、アップグレード、オフラインテスト

活用シーン

  1. 01サーバー側タスクやスクリプトからのチャットエージェント実行
  2. 02承認フローやエージェント受信箱の構築
  3. 03永続的なサブエージェントへの作業委任
  4. 04会話ターンと復旧処理のオフラインテスト

インストール

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

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

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

詳細

chat.agent:高度な機能と運用

chat.agent は セッションを基盤としています。安定した externalId(例:chatId)をキーとする、永続的でタスクに紐づいた双方向 I/O チャネルのペアであり、単一の実行が終了しても存続します。このスキルでは、日常的なエージェントを支えるレイヤーとその周辺の機能、すなわち低レベルの sessions API、サーバー側の AgentChat、永続的なサブエージェント、アクション/バックグラウンドからの注入、高速起動、圧縮と復旧、カスタムトランスポート用のワイヤープロトコルについて説明します。

混同しやすい chat 名前空間が 2 つあります。エージェント定義では @trigger.dev/sdk/ai から chat をインポートし、Head Start/Node-listener のサーバーエントリーでは @trigger.dev/sdk/chat-server から chat をインポートします。

セットアップ

基本的な方法は、AgentChat を使ってサーバー側のコード(タスク、Webhook、スクリプト)からエージェントを動かすことです。

import { AgentChat } from "@trigger.dev/sdk/chat";
import type { myAgent } from "./trigger/my-agent";

const chat = new AgentChat<typeof myAgent>({ agent: "my-chat", clientData: { userId: "user_123" } });
const stream = await chat.sendMessage("Review PR #42");
const text = await stream.text();
await chat.close();

sendMessage() は、最初の呼び出しでは実行を開始し、その後は入力ストリームを介して同じ実行を再利用します。ChatStream は text()、result()({ text, toolCalls, toolResults })、messages()(UIMessage のスナップショット)、および生の .stream を公開します。その他のメソッド:steer(text)、stop()、sendRaw(uiMessages)、sendAction(action)、preload()、reconnect()。

コアパターン

1. チャット以外の双方向 I/O に使う低レベルの Sessions

チャットの抽象化が合わない場合は、sessions を直接使います。エージェントの受信トレイ、承認フロー、サーバー間のパイプラインなどが該当します。sessions.start は (env, externalId) に対して冪等です。externalId の先頭に session_ は使用できません。

import { sessions } from "@trigger.dev/sdk";

const { id, publicAccessToken } = await sessions.start({
  type: "chat.agent",
  externalId: chatId,
  taskIdentifier: "my-chat",
  triggerConfig: { tags: [`chat:${chatId}`], basePayload: { chatId, trigger: "preload" } },
});

const session = sessions.open(chatId); // no network call; methods are lazy
await session.out.append({ kind: "message", text: "hello" });
const next = await session.in.once<MyEvent>({ timeoutMs: 30_000 });

sessions.open(id).in には send、on(handler)、peek、wait(実行を一時停止します。task.run() 内でのみ使用可能)、waitWithIdleTimeout もあります。.out には append、pipe、writer、read、writeControl、trimTo があります。sessions.list({ type, tag, status, ... })(for await)で一覧表示し、sessions.update で変更し、sessions.close で終了します(終端状態となり、冪等です)。

2. ストリーミングツールとしての永続的なサブエージェント

AI SDK の tool() 内で AgentChat を使うと、永続的なサブエージェントに処理を委譲できます。その応答は予備的なツール結果としてストリーミングされます。モデルに簡潔な要約を提示するため、ツールに toModelOutput を指定してください。

import { tool } from "ai";
import { AgentChat } from "@trigger.dev/sdk/chat";
import { z } from "zod";

const researchTool = tool({
  description: "Delegate research to a specialist agent.",
  inputSchema: z.object({ topic: z.string() }),
  execute: async function* ({ topic }, { abortSignal }) {
    const chat = new AgentChat({ agent: "research-agent" });
    const stream = await chat.sendMessage(topic, { abortSignal });
    yield* stream.messages(); // UIMessage snapshots become preliminary tool results
    await chat.close();
  },
  toModelOutput: ({ output: message }) => {
    const lastText = message?.parts?.findLast((p: { type: string }) => p.type === "text") as
      | { text?: string }
      | undefined;
    return { type: "text", value: lastText?.text ?? "Done." };
  },
});

execute: ai.toolExecute(task) を介して公開するサブタスクの場合は、chat.stream.writer({ target: "root" }) を使ってエージェントの実行に進捗をストリーミングします。target が受け付ける値は "self" | "parent" | "root" | <runId> です。サブタスク内で ai.toolCallId() と ai.chatContextOrThrow<typeof myChat>() を使ってコンテキストを読み取ります({ chatId, turn, continuation, clientData })。

import { chat, ai } from "@trigger.dev/sdk/ai";

const { waitUntilComplete } = chat.stream.writer({
  target: "root",
  execute: ({ write }) =>
    write({ type: "data-research-status", id: partId, data: { query, status: "in-progress" } }),
});
await waitUntilComplete();
3. バックグラウンドからの注入:遅延実行+注入

chat.defer(promise) は、ストリーミングと並行して処理を実行します(遅延されたすべてのプロミスは、onTurnComplete の前に 5 秒のタイムアウト付きで待機されます)。chat.inject(messages) は、次のターン開始時または prepareStep の境界で処理される ModelMessage[] をキューに追加します。

注入先はロールによって決まる 2 つのレーンに分かれます。role: "system" のメッセージはモデルへの指示に加わり、システムプロンプトと同様に信頼され、次のターンにのみ適用されます。それ以外のロールは会話に加わり、構造上、信頼されないものとして扱われます。そのため、検証可能な事実はそちらに置き、指示はシステムレーンに置いてください。指示レーンがモデルに届くのは、管理対象の streamText(または chat.toStreamTextOptions() の展開)を介する場合のみです。SDK が指示を設定できるのはその箇所だからです。

export const myChat = chat.agent({
  id: "my-chat",
  onTurnComplete: async ({ messages }) => {
    chat.defer(
      (async () => {
        const analysis = await analyzeConversation(messages);
        chat.inject([{ role: "system", content: `[Analysis]\n\n${analysis}` }]);
      })()
    );
  },
  registry,
  run: async ({ messages, signal, streamText }) =>
    streamText({ messages, abortSignal: signal, stopWhen: stepCountIs(15) }),
});
4. 圧縮(しきい値ベース)

compaction.shouldCompact が圧縮するタイミングを判定し、summarize がモデルメッセージを置き換える要約を生成します。UI メッセージはデフォルトで保持されます(compactUIMessages でカスタマイズ可能です)。内部ループの圧縮を実行する prepareStep は、管理対象の streamText を介して適用され、後から渡す prepareStep と合成されます。chat.toStreamTextOptions() を展開した後に独自の prepareStep を渡すと置き換えられ、圧縮は無効になります。

compaction: {
  shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > 80_000,
  summarize: async ({ messages }) =>
    (await generateText({
      model: anthropic("claude-haiku-4-5"),
      messages: [...messages, { role: "user", content: "Summarize concisely." }],
    })).text,
},
5. アクション:状態を編集し、必要に応じて回答する

actionSchema が検証を行い、onAction が chat.history(slice、replace、rollbackTo、 remove、getPendingToolCalls、extractNewToolResults)を使って編集します。アクションでは hydrateMessages と onAction のみが実行されます。編集のみのアクションでは何も返さないでください。モデル呼び出しは行われず、ターンカウンターも進みません。編集後に回答するには chat.turn() を返します。ターンでは編集後の履歴を使い、ターンに含まれるすべての機能(エージェントのシステムプロンプトとツール、ステアリング、コンパクション、挿入された指示、onTurnStart と onTurnComplete、永続化)が実行され、run() には trigger: "action-turn"`` が渡されます。onActionにstreamText引数はなく、そこからStreamTextResult、文字列、または UIMessage` を返すと例外が発生します。

永続化の処理はモデルによって異なります。トランスクリプトストレージ(chat.agent の storage。デフォルトではプラットフォームのスナップショット)を使う場合、会話が変更されたアクションの後、ランタイムは reason: "action" を含む変更セットをストレージに渡します。取り消しは 1 回の truncateAfter、再生成は truncateAfter に続いてターン完了時に新しい回答の put、編集は編集後の ID に対する put です。非推奨の hydrateMessages を使う場合、ストアが信頼できるデータソースとなり、ランタイムは書き込みを行いません。そのため、すべての変更を自分で反映してください。再生成では削除と挿入が行われ、chat.turn() に続く回答は、他のターンの回答と同様に onTurnComplete を通じて届きます。

export const myChat = chat.agent({
  id: "my-chat",
  actionSchema: z.discriminatedUnion("type", [
    z.object({ type: z.literal("undo") }),
    z.object({ type: z.literal("regenerate") }),
    z.object({ type: z.literal("rollback"), targetMessageId: z.string() }),
  ]),
  onAction: async ({ action }) => {
    if (action.type === "undo") chat.history.slice(0, -2); // edit only
    if (action.type === "rollback") chat.history.rollbackTo(action.targetMessageId);
    if (action.type === "regenerate") {
      chat.history.slice(0, -1);
      return chat.turn(); // answer the edited history
    }
  },
  run: async ({ messages, signal, streamText }) =>
    streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }),
});

回答が通常のターンと同じように描画されるよう、useChat を通じてブラウザーから送信します。sendMessage(undefined, { body: { action: { type: "regenerate" } } })、regenerate({ body: { action } })、 または @trigger.dev/sdk/chat/react の useChatActions({ sendMessage }) を使います。再生成には、useChat(Vercel AI SDK)の regenerate() を使ってください。これは新しい回答をストリーミングする前に最後のアシスタントメッセージを削除します。再生成アクションを指定して sendMessage を呼び出すと、削除せずに追加するため、両方の回答が表示されたままになります。transport.sendAction(chatId, action) は生のストリームを返すため、呼び出し側で読み取って適用する必要があります。サーバー側では、 agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" }) を使います。

6. クイックスタート:Head Start

chat.headStart(@trigger.dev/sdk/chat-server から。/ai ではありません)は Web Fetch ハンドラーを返します。このハンドラーは、最初のターンを独自のウォームプロセスから提供し、2 ターン目以降はエージェントに引き継ぎます。ここで渡すツールは スキーマのみ にしてください(ai と zod のみをインポートするモジュール)。処理の重い実行部分はタスク内に置きます。

import { chat } from "@trigger.dev/sdk/chat-server";
import { anthropic } from "@ai-sdk/anthropic";
import { headStartTools } from "@/lib/chat-tools/schemas";

export const chatHandler = chat.headStart({
  agentId: "my-chat",
  // `streamText` from the run argument owns `messages`, `prompt`, `stopWhen`
  // and `abortSignal`: the handover needs `stopWhen: stepCountIs(1)` so the agent,
  // not this handler, runs step 2 onward. Passing any of them is a type error.
  run: async ({ streamText }) =>
    streamText({
      model: anthropic("claude-sonnet-4-6"),
      system: "You are helpful.",
      tools: headStartTools,
    }),
});
// Next.js: export const POST = chatHandler;  Transport: headStart: "/api/chat"

Node 専用のフレームワークでは、chat.toNodeListener(handler) を使って Web Fetch ハンドラーをラップします。1 ターン目と 2 ターン目以降で口調が変わらないよう、両側で 同じモデル を使用してください。

7. chat.local:onChatStart ではなく onBoot で初期化する

chat.local<T>({ id }) はモジュールレベルの浅いプロキシで、実行単位の状態です。onBoot(継続実行を含む、新しいワーカーが起動するたびに呼び出されます)で初期化し、onChatStart では決して初期化しないでください。

const userContext = chat.local<{ name: string; plan: "free" | "pro" }>({ id: "userContext" });

export const myChat = chat.agent({
  id: "my-chat",
  onBoot: async ({ clientData }) => userContext.init({ name: "Alice", plan: "pro" }),
  run: async ({ messages, signal }) => streamText({ /* ... */ }),
});
8. 保留中のメッセージ(ストリーミング中のユーザー入力)

ターンのストリーミング中にメッセージが送信されても、ストリームはキャンセルされません。エージェントに pendingMessages(shouldInject、prepare、onReceived、onInjected)を設定すると、管理対象の streamText の prepareStep が次の境界でメッセージを組み込みます。挿入されたステアリングメッセージはフックから見える会話の一部となるため、onTurnComplete では uiMessages と newUIMessages に含まれ、そこから永続化するアプリでは追加の作業なしに保存されます。フロントエンドでは、usePendingMessages が pending、steer(text)、queue(text)、promoteToSteering(id) を返します。transport.sendPendingMessage(chatId, uiMessage, metadata?) で送信します。

9. リカバリーとバージョンアップグレード

onRecoveryBoot は、末尾に部分的なアシスタントメッセージが存在する場合にのみ呼び出されます(デプロイの中断、クラッシュ、OOM による再試行)。chat.requestUpgrade() では呼び出されません。これは部分的なメッセージを伴わない正常な終了です。chat.requestUpgrade()(run() をスキップするために onTurnStart / onValidateMessages で呼び出すか、ターン終了後に終了するために run() / chat.defer() で呼び出します)は、クライアントの再接続なしに、Session の currentRunId を最新のデプロイ上の実行に切り替えます。clientData のコントラクトバージョンと組み合わせて使用します。

const SUPPORTED_VERSIONS = new Set(["v2", "v3"]);
onTurnStart: async ({ clientData }) => {
  if (clientData?.protocolVersion && !SUPPORTED_VERSIONS.has(clientData.protocolVersion)) {
    chat.requestUpgrade();
  }
},

OOM への耐性を高めるには、エージェントに oomMachine(および machine)を設定し、再試行時により大きなプリセットが使用されるようにします。

10. mockChatAgent によるオフラインテスト

@trigger.dev/sdk/ai/test は、実際のターンループをメモリ内で実行します。リソースカタログがインストールされるよう、エージェントモジュールより前にインポートしてください。sendMessage、sendRegenerate、sendAction、sendStop、sendHeadStart、sendHandover を使って操作し、seedSnapshot / seedSessionOutTail / seedSessionOutPartial / seedSessionInTail で状態を初期化します。turn.chunks と harness.allChunks に対してアサーションを行います。

import { mockChatAgent } from "@trigger.dev/sdk/ai/test"; // BEFORE the agent module
import { myChatAgent } from "./my-chat.js";

const harness = mockChatAgent(myChatAgent, { chatId: "test-1", clientData: { model } });
try {
  const turn = await harness.sendMessage({ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] });
  // assert against turn.chunks
} finally {
  await harness.close();
}

オプションには、mode("preload" | "submit-message" | "handover-prepare" | "continuation")、preload、continuation、previousRunId、snapshot、taskContext、setupLocals があります。OOM による再試行をシミュレートするには、taskContext.ctx.attempt.number > 1 を設定します。runInMockTaskContext は、チャット以外のタスクをオフラインで実行します。

11. カスタムトランスポート:ワイヤープロトコル

エンドポイント:POST /api/v1/sessions(作成)、GET /realtime/v1/sessions/{id}/out(SSE)、POST /realtime/v1/sessions/{id}/in/append、POST /api/v1/sessions/{id}/close。ChatInputChunk は { kind: "message"; payload: ChatTaskWirePayload } | { kind: "stop"; message? } です。ChatTaskWirePayload には、chatId、trigger(submit-message | regenerate-message | preload | close | action | handover-prepare)、message?、metadata?、action?、continuation?、previousRunId? などが含まれます。制御レコードはヘッダー形式です:trigger-control: turn-complete(任意の public-access-token、session-in-event-id を含みます)および trigger-control: upgrade-required。TS ヘルパーの SSEStreamSubscription と controlSubtype(headers)(docs/ai-chat/client-protocol.mdx に記載)が、バッチのデコードと制御レコードのフィルタリングを処理します。

よくある間違い

  • 重大:POST /api/v1/sessions を再度 POST してフォローアップを送信すること。

    // Wrong - a cached re-POST silently drops basePayload.message; basePayload is trigger config, not a channel
    await fetch("/api/v1/sessions", { method: "POST", body: JSON.stringify({ ...createBody }) });
    // Correct - append to the session's input channel
    await fetch(`/realtime/v1/sessions/${id}/in/append`, { method: "POST", body: JSON.stringify({ kind: "message", payload }) });
    
  • .in / .out に誤ったトークンを使用すること。 作成レスポンスの body に含まれる publicAccessToken(セッションスコープ)を使用してください。x-trigger-jwt レスポンスヘッダーは実行スコープのため、サブスクライブには使用できません。

  • onChatStart で chat.local を初期化すること。 継続実行時にはスキップされるため、run() が chat.local can only be modified after initialization でクラッシュします。onBoot で初期化してください。

  • メッセージ履歴の書き込みに chat.defer を使用すること。 ストリームの途中で更新が発生すると、[] が読み込まれます。モデルのストリーミング開始前に、その書き込みをインラインで await してください。chat.defer は分析、監査、キャッシュのウォームアップ用に取っておきます。

  • HITL ツールに execute を指定すること。 streamText が即座に実行します。execute は指定せず、フロントエンドから addToolOutput と sendAutomaticallyWhen を使って回答を渡してください。

  • サブエージェント/負荷の高いツールを streamText にしか宣言しないこと。 chat.agent({ tools }) にも宣言するか、カスタムエージェントで convertToModelMessages(uiMessages, { tools }) に渡してください。これにより、毎ターン toModelOutput が再適用されます。

  • 負荷の高い execute を含むツールを Head Start のルートモジュールにインポートすること。 これはビルド時のインポートチェーンの問題であり、実行時の strip ヘルパーでは解決できません。スキーマは ai と zod のみを使用するモジュールに置いてください。

  • ストリームでメガバイト単位のツール出力を返すこと。 約 1 MiB を超える tool-output-available レコードが 1 つあると、ChatChunkTooLargeError がスローされます。ストアに保存し、先に行を書き込んでから、ID だけを送信してください。

  • アクティブな送信パスで X-Peek-Settled: 1 を設定すること。 新しいターンの最初のチャンクと競合し、ストリームを早期に閉じてしまいます。再読み込み時の再接続パスでのみ使用してください。

ドキュメントの用語に関する注意:一部のドキュメントにあるエージェント側の例では、引き続き従来の trigger:turn-complete チャンクタイプが使われています。これはエージェントが出力する語彙です。カスタムの リーダー は、 chunk.type ではなく trigger-control ヘッダーでフィルタリングする必要があります。

MCP を使用するエージェントチャット(list_agents、start_agent_chat、send_agent_message、 close_agent_chat)は、Claude Code / Cursor から使用する MCP サーバーのツールであり、インポート可能な SDK 関数ではありません。/mcp-tools#agent-chat-tools を参照してください。

参考資料

  • trigger-authoring-chat-agent スキル:日常的に使用する chat.agent({...}) の定義、ライフサイクルフック、 useTriggerChatTransport の基本的な使い方について説明します。このスキルを利用する前に、まずこちらを参照してください。
  • trigger-realtime スキル:チャットトランスポート以外の Realtime フックとフロントエンドのストリーミングについて説明します。
  • trigger-tasks スキル:基本的な task() のセマンティクス、ctx、標準的なライフサイクルフックについて説明します。

リファレンスドキュメントはこのスキルと同じパッケージ内に同梱されています。ネットワークを使わず、インストール済みバージョンに合わせてローカルで参照してください。上記の sources: フロントマターには、このスキルが参照するすべてのドキュメントが記載されており、すべて @trigger.dev/sdk/docs/ai-chat/ 以下にあります(patterns/ を含む)。HITL、セッション、サブエージェントについては、sessions.mdx、server-chat.mdx、client-protocol.mdx、patterns/human-in-the-loop.mdx、patterns/sub-agents.mdx から読み始めてください。

チャットエージェントタスクで必要となる trigger.config.ts とビルド拡張機能(Prisma、Playwright、Python など)については、同梱されている @trigger.dev/sdk/docs/config/ 以下の設定ドキュメントを参照してください(拡張機能ごとの設定については config/extensions/)。

バージョン

このスキルは @trigger.dev/sdk に同梱され、node_modules から直接読み込まれるため、インストール済みの SDK バージョンと常に一致します(隣接する package.json を参照してください)。これらの API に関する完全なドキュメントも、@trigger.dev/sdk/docs/ 以下に同梱されています。

コメント