エージェント実行をキャンセルする¶
エージェント実行に時間がかかりすぎる場合、状況が変わった場合、または不要になった場合、 すでに完了した作業を失わずにキャンセルしたいことがあります。ADK のキャンセルは非破壊的です。 セッションにすでにコミットされたイベントは保持されます。
ADK は AbortController と AbortSignal を使った正常なキャンセルをサポートします。
runner.runAsync() に AbortSignal を渡すことで、エージェント実行、LLM 生成、
ツール実行、プラグインコールバックを含む実行スタックの任意の地点で、呼び出し全体を
キャンセルできます。
はじめに¶
AbortController を作成し、その signal を runner.runAsync() に渡して、実行を
キャンセルしたいタイミングで controller.abort() を呼び出します。
import { Runner, InMemorySessionService, LlmAgent, FunctionTool } from '@google/adk';
import { z } from 'zod';
const getInfo = new FunctionTool({
name: 'get_info',
description: 'Gets information about a topic.',
parameters: z.object({ topic: z.string() }),
execute: (args) => ({ result: `Info about ${args.topic}` }),
});
const agent = new LlmAgent({
name: 'my_agent',
model: 'gemini-flash-latest',
instruction: 'Always use the get_info tool before answering.',
tools: [getInfo],
});
const sessionService = new InMemorySessionService();
const runner = new Runner({ agent, appName: 'my_app', sessionService });
const session = await sessionService.createSession({ appName: 'my_app', userId: 'user_1' });
const controller = new AbortController();
const run = runner.runAsync({
userId: session.userId,
sessionId: session.id,
newMessage: { role: 'user', parts: [{ text: 'Tell me about quantum computing.' }] },
abortSignal: controller.signal,
});
let count = 0;
for await (const event of run) {
count++;
console.log('Event:', event.author);
controller.abort(); // Without this, 3+ events; with it, only 1.
}
console.log(`Done. Received ${count} event(s).`);
キャンセルの伝播方法¶
signal を abort すると、キャンセルは実行スタック全体に伝播します。各コンポーネントは
重要なライフサイクル地点で abortSignal.aborted を確認し、キャンセルを検出すると早期に
終了します。
| コンポーネント | abort 時の動作 |
|---|---|
| Runner | セッション取得前、プラグインコールバック後、イベントストリーミングループ内で停止します。 |
| LlmAgent | 実行ステップ間、モデルコールバックの前後、レスポンスストリーミング中に停止します。 |
| LoopAgent | ループ反復間およびサブエージェント実行間で停止します。 |
| ParallelAgent | 並行して実行されたサブエージェントの結果をマージするときに停止します。 |
| Models (Gemini) | signal が config.abortSignal 経由で基盤となる Google GenAI SDK に渡され、進行中の HTTP リクエストをキャンセルします。 |
| AgentTool | signal をサブエージェント runner に渡し、セッション作成後に abort を確認します。 |
| MCPTool | signal を MCP クライアントの callTool メソッドに渡します。 |
InvocationContext も signal に listener を登録します。signal がトリガーされると
endInvocation = true が自動的に設定され、すべてのコンポーネントに終了処理へ移るよう
通知します。
キャンセル時の動作¶
AbortSignal がトリガーされると、次の動作が適用されます。
- 正常終了:
runner.runAsync()が返す async generator はエラーを投げずに完了します。 つまり、イベントの yield を停止します。 - コミット済みイベントは保持: abort 前に Runner がすでに yield して処理したイベントは、 セッション履歴にコミットされたまま残ります。
- 部分イベントなし: 進行中だったがまだ yield されていないイベントは破棄されます。
- リソースのクリーンアップ: Gemini API への進行中の LLM リクエストは、SDK のネイティブな
AbortSignalサポートによりキャンセルされ、ネットワークリソースが解放されます。
高度な例¶
次の例では、基本的な AbortController の使い方を超えた追加のキャンセルパターンを
示します。
タイムアウトによるキャンセル¶
AbortSignal.timeout() を使用すると、指定した時間の後にエージェント実行を自動的に
キャンセルできます。これはエージェント実行に時間制限を強制したい場合に便利です。
はじめにの例と同じエージェントおよび runner 設定を使い、const controller 以降を
すべて次のコードに置き換えます。
const run = runner.runAsync({
userId: session.userId,
sessionId: session.id,
newMessage: { role: 'user', parts: [{ text: 'Tell me about quantum computing.' }] },
abortSignal: AbortSignal.timeout(2_000), // Cancel after 2 seconds
});
let count = 0;
for await (const event of run) {
count++;
console.log('Event:', event.author);
}
console.log(`Done. Received ${count} event(s).`);
AbortSignal.any() を使って、タイムアウトとプログラムによるキャンセルを組み合わせる
こともできます。同じ設定で、const controller 以降をすべて次のコードに置き換えます。
const controller = new AbortController();
// Cancel on timeout OR programmatically via controller.abort()
// e.g.: cancelButton.addEventListener('click', () => controller.abort());
const combinedSignal = AbortSignal.any([
controller.signal,
AbortSignal.timeout(60_000),
]);
const run = runner.runAsync({
userId: session.userId,
sessionId: session.id,
newMessage: { role: 'user', parts: [{ text: 'Tell me about quantum computing.' }] },
abortSignal: combinedSignal,
});
カスタムツール内の AbortSignal¶
runner.runAsync() に AbortSignal を渡すと、カスタムツール内の
toolContext.abortSignal でも利用できます。次の例では、カスタムツール内で abort
signal を確認するパターンを示します。
import { FunctionTool } from '@google/adk';
import { z } from 'zod';
const fetchItems = async (id: string) => ['item1', 'item2', 'item3'];
const processItem = async (item: string) => ({ processed: item });
const longRunningTool = new FunctionTool({
name: 'process_data',
description: 'Processes data in multiple steps.',
parameters: z.object({
dataId: z.string(),
}),
execute: async (args, toolContext) => {
const items = await fetchItems(args.dataId);
const results = [];
for (const item of items) {
// Check the abort signal before each step
if (toolContext?.abortSignal?.aborted) {
return { status: 'cancelled', processed: results.length };
}
results.push(await processItem(item));
}
return { status: 'complete', processed: results.length };
},
});