ツールとしての Agents を使うと、1 つのチャットエージェントが、作業の一部として別のチャット可能なサブエージェントを起動できます。子は本物のサブエージェントです。独自の Durable Object ストレージ、メッセージ、ツール、再開可能なストリーム、ドリルイン URL を持ちます。親は小さな実行レジストリを保持するので、クライアントは子のタイムラインを描画し、更新後にリプレイし、あとから片付けられます。
ツールとしての Agents は、@cloudflare/think エージェントと AIChatAgent のサブクラスに対応します。AIChatAgent の子は saveMessages() 経由でヘッドレス実行するため、サーバー側ツールを使ってください。エージェントツールのターン中は、ブラウザー提供のクライアントツールは使えません。そのやり取りをサーバー側の状態、または親が仲介する別ワークフローとしてモデル化する場合を除きます。
親コードが特定の子へ直接ストリーミング RPC し、転送、キャンセル、リプレイ方針を自前で持つときは subAgent(...).chat() を使います。
親モデルまたはワークフローが子エージェントへ作業を委譲し、保持される子の実行、イベントリプレイ、中止の橋渡し、UI のドリルインが欲しいときは agentTool() または runAgentTool() を使います。Think 固有のターン選択は ターン API を選ぶ を参照してください。
親モデルがヘルパーをいつ呼ぶかを決めるときは agentTool() を使います。
import { Think } from "@cloudflare/think";
import { agentTool } from "agents/agent-tools";
import { z } from "zod";
export class Researcher extends Think {
getSystemPrompt() {
return "Research the user's topic and end with a concise summary.";
}
}
export class Assistant extends Think {
getTools() {
return {
research: agentTool(Researcher, {
description: "Research one topic in depth.",
displayName: "Researcher",
inputSchema: z.object({
query: z.string().min(3),
}),
}),
};
}
}import { Think } from "@cloudflare/think";
import { agentTool } from "agents/agent-tools";
import { z } from "zod";
export class Researcher extends Think<Env> {
getSystemPrompt() {
return "Research the user's topic and end with a concise summary.";
}
}
export class Assistant extends Think<Env> {
getTools() {
return {
research: agentTool(Researcher, {
description: "Research one topic in depth.",
displayName: "Researcher",
inputSchema: z.object({
query: z.string().min(3),
}),
}),
};
}
}子は AIChatAgent にもできます。
import { AIChatAgent } from "@cloudflare/ai-chat";
import { agentTool } from "agents/agent-tools";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { z } from "zod";
export class Summarizer extends AIChatAgent {
formatAgentToolInput(input, request) {
return {
id: `agent-tool-${request.runId}-input`,
role: "user",
parts: [{ type: "text", text: `Summarize:\n\n${input.text}` }],
};
}
async onChatMessage() {
const result = streamText({
model: this.env.MODEL,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}
export class Assistant extends AIChatAgent {
async onChatMessage() {
const result = streamText({
model: this.env.MODEL,
messages: await convertToModelMessages(this.messages),
tools: {
summarize: agentTool(Summarizer, {
description: "Summarize long text in a separate retained agent.",
inputSchema: z.object({ text: z.string() }),
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}import { AIChatAgent } from "@cloudflare/ai-chat";
import { agentTool } from "agents/agent-tools";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { z } from "zod";
export class Summarizer extends AIChatAgent<Env> {
protected override formatAgentToolInput(input: { text: string }, request) {
return {
id: `agent-tool-${request.runId}-input`,
role: "user",
parts: [{ type: "text", text: `Summarize:\n\n${input.text}` }],
};
}
async onChatMessage() {
const result = streamText({
model: this.env.MODEL,
messages: await convertToModelMessages(this.messages),
});
return result.toUIMessageStreamResponse();
}
}
export class Assistant extends AIChatAgent<Env> {
async onChatMessage() {
const result = streamText({
model: this.env.MODEL,
messages: await convertToModelMessages(this.messages),
tools: {
summarize: agentTool(Summarizer, {
description: "Summarize long text in a separate retained agent.",
inputSchema: z.object({ text: z.string() }),
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}生成されたツールは this.runAgentTool(ChildAgent, ...) を呼び、親の WebSocket 上で agent-tool-event フレームをストリームし、子の要約を親モデルへ返します。実行が失敗、中止、中断した場合、ツールは空の成功値ではなく、構造化された AgentToolFailure を返します。
type AgentToolFailure = {
ok: false;
status: "error" | "aborted" | "interrupted";
error: string; // human-readable, safe to surface
retryable: boolean;
// Present only when `status` is "interrupted":
reason?: AgentToolInterruptedReason;
childStillRunning?: boolean;
};
type AgentToolInterruptedReason =
| "no-progress"
| "window-exceeded"
| "not-tailable"
| "inspect-timeout"
| "inspect-failed"
| "recovery-deadline"
| "budget-exceeded";retryable が true になるのは interrupted の実行だけです。デプロイまたは親のリカバリーで子がリセットまたは置き換えられ、論理的な結果に到達していないため、同じ呼び出しを再ディスパッチすれば成功することがあります。本当の error や意図的な aborted は retryable: false です。これにより、親のプロンプト規約やオーケストレーションハーネスは、一時的な中断をユーザーへ最終失敗として報告せず再実行できます。AgentToolFailure は agents からエクスポートされます。
interrupted の実行では、reason が機械可読な原因を示し、childStillRunning は親が待機をやめた時点で子がまだ動いていたか(true)またはすでに破棄されたか(false)を報告します。error の文章をパースせず、これらで分岐してください。たとえば no-progress の中断は再ディスパッチし(子が自己修復する可能性があります)、window-exceeded は再接続するか表面化します(子は破棄されています)。reason と childStillRunning は、agent-tool-event のワイヤフレームと useAgentToolEvents() の実行状態にもミラーされます。
ユーザー向けアシスタントテキストなしでワークフロー風の作業をする Think の子では、getAgentToolOutput() を上書きし、必要なら getAgentToolSummary() も上書きします。アシスタントテキストがある場合は、それがデフォルトの要約のままです。ただし Think のエージェントツール実行は、テキストチャンクを出さずに成功完了できます。
子のターンが終わる前に、構造化出力を永続化してください。getAgentToolOutput() は saveMessages() が解決した直後に読まれるためです。表示用の getAgentToolSummary() は簡潔にしてください。完全な構造化値は、ツール出力として別に保存されます。
export class Extractor extends Think {
getAgentToolOutput(runId) {
const rows = this.sql`
SELECT result_json FROM extraction_runs WHERE id = ${runId}
`;
return rows[0] ? JSON.parse(rows[0].result_json) : undefined;
}
getAgentToolSummary(_runId, output) {
return output ? "Extraction complete" : "";
}
}export class Extractor extends Think<Env> {
protected override getAgentToolOutput(runId: string) {
const rows = this.sql<{ result_json: string }>`
SELECT result_json FROM extraction_runs WHERE id = ${runId}
`;
return rows[0] ? JSON.parse(rows[0].result_json) : undefined;
}
protected override getAgentToolSummary(_runId: string, output: unknown) {
return output ? "Extraction complete" : "";
}
}決定的なワークフロー、スケジュール作業、HTTP ハンドラー、ファンアウトコードでは runAgentTool() を使います。
const [a, b] = await Promise.allSettled([
this.runAgentTool(Researcher, {
input: { query: "HTTP/3" },
parentToolCallId: toolCallId,
displayOrder: 0,
}),
this.runAgentTool(Researcher, {
input: { query: "gRPC" },
parentToolCallId: toolCallId,
displayOrder: 1,
}),
]);const [a, b] = await Promise.allSettled([
this.runAgentTool(Researcher, {
input: { query: "HTTP/3" },
parentToolCallId: toolCallId,
displayOrder: 0,
}),
this.runAgentTool(Researcher, {
input: { query: "gRPC" },
parentToolCallId: toolCallId,
displayOrder: 1,
}),
]);runAgentTool() は runId についてべき等です。同じ runId を渡しても、子のターンが重複して始まりません。完了、失敗、中止、中断した実行は、明示的に消すまで保持されます。
デフォルトでは runAgentTool() は、子が終端状態になるまで 待機してから 返します。大きなインポート、動画レンダー、深い調査など、ディスパッチするターンをブロックしたくない長時間作業では detached を渡します。実行はディスパッチされ、現在のターンは続き、runAgentTool() はすぐにハンドルを返します。
type DetachedRunAgentToolResult = {
runId: string;
agentType: string;
status: "running" | "error"; // "error" only if dispatch itself was rejected
};detached: true は投げっぱなしです。実行の観測は agent-tool-event フレーム(useAgentToolEvents() が消費するものと同じ)と、グローバルな onAgentToolFinish() フックで行います。対象を絞った耐久的な完了コールバックを接続するときは、オブジェクトを渡します。
export class Importer extends Think {
async startImport(input) {
const { runId } = await this.runAgentTool(ImportAgent, {
input,
detached: { onFinish: "onImportDone", maxBudgetMs: 60 * 60 * 1000 },
});
return runId;
}
// Fires once, even if the Durable Object was evicted and rehydrated while the
// child ran. Referenced by METHOD NAME (like schedule()) — never a closure,
// which cannot survive eviction.
async onImportDone(run, result) {
switch (result.status) {
case "completed":
await this.markImportReady(run.runId, result.summary);
break;
case "error":
await this.markImportFailed(run.runId, result.error);
break;
case "interrupted":
// reason "budget-exceeded" ⇒ the run hit its maxBudgetMs ceiling.
// interrupted is soft: a child that finishes anyway re-fires this
// hook with "completed", so make the handler idempotent.
break;
}
}
}export class Importer extends Think<Env> {
async startImport(input: ImportInput) {
const { runId } = await this.runAgentTool(ImportAgent, {
input,
detached: { onFinish: "onImportDone", maxBudgetMs: 60 * 60 * 1000 },
});
return runId;
}
// Fires once, even if the Durable Object was evicted and rehydrated while the
// child ran. Referenced by METHOD NAME (like schedule()) — never a closure,
// which cannot survive eviction.
async onImportDone(run: AgentToolRunInfo, result: AgentToolLifecycleResult) {
switch (result.status) {
case "completed":
await this.markImportReady(run.runId, result.summary);
break;
case "error":
await this.markImportFailed(run.runId, result.error);
break;
case "interrupted":
// reason "budget-exceeded" ⇒ the run hit its maxBudgetMs ceiling.
// interrupted is soft: a child that finishes anyway re-fires this
// hook with "completed", so make the handler idempotent.
break;
}
}
}主な動作:
- 耐久的な完了。 配信は退避とデプロイを生き延びます。isolate が生きているあいだはウォームな高速経路が低遅延で届け、高速経路が見逃したものを自己スケジュールのリコンサイル基盤が確定します。ハッピーパスでは exactly-once、クラッシュ時は at-least-once なので、
onFinishハンドラーはべき等にしてください。 - 監視放棄と完了は独立です。 予算による監視放棄は
status: "interrupted"、reason: "budget-exceeded"として届きます。interruptedはソフトなので、監視放棄のあとで子が完了しても、本当の結果でonFinishが再発火します。早すぎる監視放棄が、遅い完了を隠すことはありません。 - 上限があります。 すべてのデタッチ実行には絶対的な
maxBudgetMs上限があります(実行ごと、またはdetachedMaxBudgetMs静的オプション。デフォルト 24 時間)。期限切れになると、親は監視をやめ、子を破棄します。放置された実行がmaxConcurrentAgentToolsスロットを永久に占有しないようにするためです。 - シグナルは継承しません。 デタッチ実行は起動したターンより長く生きる必要があるため、
options.signalを 継承しません。明示的にキャンセルします。
await this.cancelAgentTool(runId); // idempotent; delivers onFinish "aborted"await this.cancelAgentTool(runId); // idempotent; delivers onFinish "aborted"チャットエージェント(@cloudflare/think または AIChatAgent)では、終わったバックグラウンド実行にモデルが反応してほしいことがほとんどです。onFinish を手でつなぐ代わりに notify: true を渡します。実行が終わると、エージェントはチャットへメッセージを注入し(実行 + ステータスごとにべき等なので、exactly-once の完了が重複しません)、モデルは結果を文脈に入れて次のターンを取ります。
await this.runAgentTool(ResearchAgent, { input, detached: { notify: true } });await this.runAgentTool(ResearchAgent, { input, detached: { notify: true } });アプリが metadata.source で合成メッセージを振り分けたり隠したりする場合は、独自の source を渡します。
await this.runAgentTool(ResearchAgent, {
input,
detached: { notify: { source: "research-background" } },
});await this.runAgentTool(ResearchAgent, {
input,
detached: { notify: { source: "research-background" } },
});注入テキストをカスタムするには formatDetachedCompletion(run, result) を上書きします。空文字を返すと、その結果の通知を抑制します。明示的な onFinish は notify より優先されます。
子の inspectAgentToolRun(runId) は、実行の現在のステータススナップショット、または null を返します。null は「失敗」ではありません。その実行の記録が子にまだない、という意味です。ディスパッチ直後は普通です(子が最初の行をまだ永続化していることがあります)。また、古くなった running 行を遅延リコンサイルする前の、再ハイドレート直後の子も同じ値を返します。呼び出し側と、フレームワーク自身のリコンサイル基盤は、null を「終端ではない。予算内で監視を続ける」として扱い、終端失敗としては扱いません。終端の status(completed / error / aborted)を持つ非 null の検査だけが、実行を確定します。
エージェントツールとして動いているサブエージェント(待機でもデタッチでも)は、実行の途中で進捗を報告できます。親はライブのステータス行を描画したり、サーバー側で計測したり、実行が終わる前に名前付きチェックポイントへ反応したりできます。子の内側(たとえばツールの execute)から reportProgress() を呼びます。
export class ImportAgent extends Think {
getTools() {
return {
ingest: tool({
inputSchema: z.object({ url: z.string() }),
execute: async ({ url }) => {
// Ephemeral progress: drives a generic bar / phase / status line.
await this.reportProgress({
fraction: 0.6,
phase: "ingesting",
message: "Ingested 40k/80k rows",
});
// ...
},
}),
};
}
}export class ImportAgent extends Think<Env> {
getTools() {
return {
ingest: tool({
inputSchema: z.object({ url: z.string() }),
execute: async ({ url }) => {
// Ephemeral progress: drives a generic bar / phase / status line.
await this.reportProgress({
fraction: 0.6,
phase: "ingesting",
message: "Ingested 40k/80k rows",
});
// ...
},
}),
};
}
}reportProgress() はチャットエージェント(@cloudflare/think と AIChatAgent)で使えます。基底の Agent クラス、およびアクティブなエージェントツール実行の外では no-op になり、開発時の警告が出ます。同じ子コードを単体実行しても安全です。フレームワークは現在のターンからアクティブな実行を解決します。run ID を渡す必要はありません。
reportProgress<T>(
progress: {
fraction?: number; // 0..1 — drives a progress bar
message?: string; // human-readable status line
phase?: string; // coarse phase label, e.g. "ingesting"
milestone?: string; // present ⇒ a durable milestone (see below)
data?: T; // app-specific payload; live-only unless persisted
},
options?: { persist?: boolean },
): Promise<void>;一時的なシグナルは、子自身のターンストリーム上を一時的な data-agent-progress 部品として流れます。親の接続中クライアントへ再ブロードキャストされ、useAgentToolEvents() 経由で AgentToolRunState.progress に現れます。バックグラウンド実行トレイは、ドリルインなしでライブのバー、フェーズ、ステータス行を描画できます。バーストはまとめられます(最新優先。fraction >= 1 のフレームは必ずフラッシュされます)。data フィールドは、{ persist: true } を渡さない限りライブのみです。
計測、操舵、サーバー側での表示には onProgress() を上書きします。子の進捗シグナルが親経由で転送されるたびにベストエフォートで発火します。待機実行とデタッチ実行の両方です。
export class Assistant extends Think {
async onProgress(run, progress) {
if (progress.milestone) {
// A durable milestone landed — branch on it.
}
console.log(run.runId, progress.phase, progress.fraction);
}
}export class Assistant extends Think<Env> {
override async onProgress(
run: AgentToolRunInfo,
progress: AgentToolProgressSnapshot,
) {
if (progress.milestone) {
// A durable milestone landed — branch on it.
}
console.log(run.runId, progress.phase, progress.fraction);
}
}onProgress() は耐久的ではありません。退避後、デタッチ実行の最新スナップショットは、フックを再発火するのではなく、リコンサイル時に inspectAgentToolRun().progress から再構築されます。最新スナップショットは子の実行行にも永続化されるので、再ハイドレートした親は、ライブストリームを追わなくても「この実行はどこか」に答えられます。
milestone に名前を付けると、シグナルは一時的な層から 耐久的 な層へ上がります。emit メソッドは 1 つだけです。
await this.reportProgress({
milestone: "sources-gathered",
data: { sources: 2 },
});await this.reportProgress({
milestone: "sources-gathered",
data: { sources: 2 },
});マイルストーンは、実行ごとに単調増加する sequence 付きで子の 1 行として永続化され、ストリーム上は 永続化された data-agent-milestone 部品として流れます(一時的な進捗とは異なります)。そのため退避を生き延び、ドリルイン時にリプレイされ、sequence で重複排除されたうえで AgentToolRunState.milestones と inspectAgentToolRun().milestones に現れます。マイルストーンでも onProgress() は発火し、progress.milestone がセットされるので、マイルストーンと一時的な進捗で分岐できます。
チャットエージェント上のデタッチ実行では、detached: { onMilestones } により、設定したマイルストーンが着地したとき、実行が終わる前にチャットメッセージを出します。各 (runId, name) は最大 1 回発火します。ライブ観測でも退避後のリコンサイルでも同じです。決定的な ID がウォーム配信とコールド配信を at-most-once にまとめます。
// "narrate" (default): inject a synthetic assistant status line — no model turn.
await this.runAgentTool(Researcher, {
input,
detached: { onMilestones: ["sources-gathered"] },
});
// "react": post a user-role turn so the model responds (steer, start dependent
// work). Costs a model turn.
await this.runAgentTool(Researcher, {
input,
detached: { onMilestones: { names: ["needs-approval"], mode: "react" } },
});// "narrate" (default): inject a synthetic assistant status line — no model turn.
await this.runAgentTool(Researcher, {
input,
detached: { onMilestones: ["sources-gathered"] },
});
// "react": post a user-role turn so the model responds (steer, start dependent
// work). Costs a model turn.
await this.runAgentTool(Researcher, {
input,
detached: { onMilestones: { names: ["needs-approval"], mode: "react" } },
});文言をカスタムするには formatDetachedMilestone(run, milestone) を上書きします。空文字を返すとそのマイルストーンを抑制します。合成の narrate メッセージは metadata.source を持つので、クライアントは人間のターンではなくエージェントイベントとして描画できます。
デタッチした子が少なくとも 1 つのシグナルを報告したあと、実行がその後 detachedNoProgressBudgetMs(デフォルト 1 時間。実行ごとの上書きは detached: { noProgressBudgetMs })のあいだ無音だと、リコンサイル基盤は監視をやめます。これは status: "interrupted"、reason: "no-progress" として現れます。一度も報告しない子は、絶対上限の detachedMaxBudgetMs だけで縛られます。遅いという理由だけで監視放棄されることはありません。リセットする窓を無効にするには、noProgressBudgetMs を 0 または Infinity にします。
useAgentToolEvents() はヘッドレスフックです。既存の親接続を購読し、リプレイとライブの競合を重複排除し、子の UIMessageChunk 本体をメッセージ部品へ適用し、親のツール呼び出し ID で兄弟実行をグループ化します。各実行状態は progress と milestones を持つので、バックグラウンド実行トレイはドリルインなしでライブのバー、フェーズ、マイルストーンチップを描画できます。
import { useAgent, useAgentToolEvents } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });
const { messages } = useAgentChat({ agent });
const agentTools = useAgentToolEvents({ agent });
for (const message of messages) {
for (const part of message.parts) {
if (part.type === "tool-call") {
const runs = agentTools.getRunsForToolCall(part.toolCallId);
// Render the child runs beside this tool call.
}
}
}import { useAgent, useAgentToolEvents } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });
const { messages } = useAgentChat({ agent });
const agentTools = useAgentToolEvents({ agent });
for (const message of messages) {
for (const part of message.parts) {
if (part.type === "tool-call") {
const runs = agentTools.getRunsForToolCall(part.toolCallId);
// Render the child runs beside this tool call.
}
}
}親のツール呼び出しがない命令的な実行は agentTools.unboundRuns として取れます。
ツールとしての Agents は通常のサブエージェントです。保持された子へは、親ルート経由で接続します。
useAgent({
agent: "Assistant",
name: userId,
sub: [{ agent: "Researcher", name: runId }],
});useAgent({
agent: "Assistant",
name: userId,
sub: [{ agent: "Researcher", name: runId }],
});推測された run ID が新しい子 facet を作らないよう、親レジストリで外部アクセスをゲートします。
override async onBeforeSubAgent(_request, child) {
if (!this.hasAgentToolRun(child.className, child.name)) {
return new Response("Not found", { status: 404 });
}
}実行と子 facet は、更新、ドリルイン、あとからの検査のためにデフォルトで保持されます。チャット履歴の消去や独自の保持ポリシー適用時に、明示的に削除します。
await this.clearAgentToolRuns();
await this.clearAgentToolRuns({
status: ["completed", "error", "aborted", "interrupted"],
});
await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });await this.clearAgentToolRuns();
await this.clearAgentToolRuns({
status: ["completed", "error", "aborted", "interrupted"],
});
await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });保持された実行がまだ starting または running の場合、クリーンアップは facet を消す前に子をキャンセルします。
エージェントツールの実行は親に保持されます。子の実行がまだ starting または running のあいだに親が再起動する(デプロイまたは退避)と、子を見捨てません。起動時リカバリーはライブの子へ再接続し、子の終端結果までストリームを追います。子は独自の chatRecovery を持つサブエージェントなので、自分の中断ターンを自己修復し、親はその出力を転送します。完了した子は、終わった作業を再実行せずに確定されます。
再接続の待ちは 進捗キー付き であり、固定の壁時計ではありません。2 つの静的 options で調整します。
| オプション | デフォルト | 動作 |
|---|---|---|
agentToolReattachNoProgressTimeoutMs |
120000(2 分) |
進捗なし の状態で親が諦めるまでの待ち時間。転送されたチャンクごとにリセットされるので、ストリーミング中の子は終端まで追います。 |
agentToolReattachMaxWindowMs |
Infinity |
1 回の再接続に対する任意の壁時計上限。デフォルトは無制限(チャットリカバリーの maxRecoveryWork に合わせています)。健全で長時間動く子が切られないようにします。上限を課すときは有限値を設定します。 |
監視放棄の結果は AgentToolFailure のフィールドに対応します。
- 進捗なし窓いっぱいで無音になった子は、
reason: "no-progress"、childStillRunning: trueで確定されます。この確定はソフトです。子は動かしたままなので、同じrunIdを再ディスパッチすれば再接続し、自己修復していれば回収できます。 - 有限の
agentToolReattachMaxWindowMsを設定して発火した場合、実行はreason: "window-exceeded"、childStillRunning: falseで確定され、子は破棄されます(十分な窓を使い切ったとみなします)。 - ストリームを追えない、または検査できない子、あるいは全体のリカバリー期限を超えた子は、対応する
reasonで確定されます。親のツール呼び出しは無限にハングせず、構造化された失敗を返します。
ハングした子がリカバリーを永久にブロックすることはありません。進捗なし予算が無音の子を縛ります。コンテンツの暴走は、親だけのタイマーではなく、子自身の chatRecovery(maxRecoveryWork と shouldKeepRecovering)で縛られます。
親のリコンシリエーションは agentTool の可観測性チャネルで監視します。
import { subscribe } from "agents/observability";
const unsubscribe = subscribe("agentTool", (event) => {
if (event.type === "agent_tool:recovery:row") {
console.log("Recovered agent-tool row", event.payload);
}
});import { subscribe } from "agents/observability";
const unsubscribe = subscribe("agentTool", (event) => {
if (event.type === "agent_tool:recovery:row") {
console.log("Recovered agent-tool row", event.payload);
}
});生の diagnostics_channel 購読者は、チャネル名 agents:agent_tool を使ってください。