ADK 実行ループ¶
ADKでサポートPython v0.1.0TypeScript v0.2.0Kotlin v0.7.0Go v0.1.0Java v0.1.0
ADK (Agent Development Kit) の中心には、非同期イベント駆動型アーキテクチャがあります。上位レベルでは、Runner がエージェントとツールで構成される実行ロジックと連携して、会話をターンごとに進めます。このフローを理解することで、状態の変更、ストリーミング出力、非同期操作がどのようにシームレスにオーケストレーションされるかを把握できます。
コア概念: Yield-and-Resume イベント ループ¶
ADK 実行ループの基本的なメカニズムは、協調型ジェネレータ/ストリーム (cooperative generator/stream) モデルです。エージェントが実行される際、一度にすべてを実行して単一の結果を返すことはありません。代わりに、一連の Event オブジェクトを段階的に生成 (yield/emit) します。
ループの仕組み:¶
Runnerがループを開始します:Runnerはユーザーのクエリを受信してセッション履歴に記録し、エージェントのメイン実行メソッド (run_async) を呼び出してプロセスを開始します。- エージェントがイベントを生成 (Yield) して一時停止します: エージェントが出力を生成したり (思考、テキスト チャンクなど)、ツールを呼び出す必要があったり、状態を変更したりするたびに、それらのアクションを含む
Eventオブジェクトを生成 (yield) します。イベントを生成した直後に、エージェントの実行は一時停止されます。 Runnerがイベントを処理します:Runnerは一時停止したエージェントから生成されたイベントを受け取ります。次の操作を実行します。- イベントをセッションの
event historyに記録します。 - イベントで指定されたアクション (セッション状態への
state_deltaの適用、アーティファクトの保存確認など) をコミットします。 - 処理されたイベントをアップストリーム (UI へのストリーミングなど) に転送します。
- イベントをセッションの
Runnerがエージェントを再開 (Resume) します: イベント処理が完了すると、Runnerはエージェントの実行を再開するようにシグナルを送ります。- エージェントが続行します: エージェントは中断したところから再開し、前のイベントで要求された状態の変更やアクションが
SessionServiceによって正常にコミットされたことを確認できます。 - 繰り返し: この生成、一時停止、処理、再開のサイクルは、エージェントが現在のユーザー クエリに対する作業を完了するまで続きます。
概念的なコード例¶
次の簡略化された疑似コードは、Runner とエージェントの実行ロジック間の相互作用を示しています。
# Runner のメイン ループ ロジックの簡略化
async def run_async(new_query, ...) -> AsyncGenerator[Event, None]:
# 1. 新しいクエリをセッション イベント履歴に追加 (SessionService 経由)
await session_service.append_event(session, Event(author='user', content=new_query))
# 2. エージェントを呼び出してイベント ループを開始
agent_event_generator = agent_to_run.run_async(context)
async for event in agent_event_generator:
# 3. 生成されたイベントを処理し、変更をコミット
await session_service.append_event(session, event) # 状態/アーティファクト デルタなどをコミット
# memory_service.update_memory(...) # 該当する場合
# artifact_service はエージェント実行中にコンテキスト経由ですでに呼び出されている可能性があります
# 4. アップストリーム処理 (UI レンダリングなど) のためにイベントを yield
yield event
# Runner は yield 後にエージェント ジェネレータが続行できることを暗黙的にシグナル送信
// Runner のメイン ループ ロジックの簡略化
async * runAsync(newQuery: Content, ...): AsyncGenerator<Event, void, void> {
// 1. 新しいクエリをセッション イベント履歴に追加 (SessionService 経由)
await sessionService.appendEvent({
session,
event: createEvent({author: 'user', content: newQuery})
});
// 2. エージェントを呼び出してイベント ループを開始
const agentEventGenerator = agentToRun.runAsync(context);
for await (const event of agentEventGenerator) {
// 3. 生成されたイベントを処理し、変更をコミット
await sessionService.appendEvent({session, event}); // 状態/アーティファクト デルタなどをコミット
// memoryService.updateMemory(...) // 該当する場合
// 4. アップストリーム処理のためにイベントを yield
yield event;
// Runner は yield 後にエージェント ジェネレータが続行できることを暗黙的にシグナル送信
}
}
/**
* Simplified view of Runner's main loop logic in Kotlin
*/
fun runAsync(
userId: String,
sessionId: String,
newMessage: Content,
runner: InMemoryRunner,
sessionService: InMemorySessionService,
): Flow<Event> {
// 1. Append newMessage to session event history (via SessionService)
// 2. Kick off event loop by calling the agent
// 3. Process generated events, commit changes, and yield upstream
return runner
.runAsync(
userId = userId,
sessionId = sessionId,
newMessage = newMessage,
).onEach { event ->
// Process the event and commit changes to services (done internally by Runner)
// sessionService.appendEvent(...)
}
}
// Go では、エージェント ランタイムはチャネルまたはイテレータを利用します
func (r *Runner) RunAsync(ctx context.Context, session *session.Session, query *session.Content) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
userEvent := session.NewEvent(ctx, r.invocationID)
userEvent.Author = "user"
userEvent.Content = query
r.sessionService.AppendEvent(ctx, session, userEvent)
for event, err := range r.agent.Run(ctx) {
if err != nil {
yield(nil, err)
return
}
r.sessionService.AppendEvent(ctx, session, event)
if !yield(event, nil) {
return
}
}
}
}
public Flowable<Event> runAsync(Session session, Content newQuery) {
Event userEvent = Event.builder().author("user").content(newQuery).build();
return sessionService.appendEvent(session, userEvent)
.andThen(Flowable.defer(() -> agentToRun.runAsync(context)))
.concatMap(event -> sessionService.appendEvent(session, event).andThen(Flowable.just(event)));
}
実行ロジックの観点¶
エージェントの実装内では、イベントのストリームを生成します。
# エージェント実行ロジック内部
async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:
# 中間の思考または部分的なテキストを yield
yield Event(author=self.name, content=Content(parts=[Part.from_text("思考中...")]), partial=True)
# --- 一時停止 --- Runner が処理後に再開 ---
# 状態変更の準備
ctx.session.state['my_key'] = 'new_value'
# 状態デルタとともにイベントを yield
yield Event(author=self.name, actions=EventActions(state_delta={'my_key': 'new_value'}))
# --- 一時停止 --- Runner/SessionService が 'my_key' をコミット後に再開 ---
# これで 'my_key' は確実にコミット済み
yield Event(author=self.name, content=Content(parts=[Part.from_text("完了!")]))
// TypeScript エージェント実行ロジック内部
async * _runAsyncImpl(ctx: InvocationContext): AsyncGenerator<Event, void, void> {
yield createEvent({
author: this.name,
content: createContent({parts: [createPart({text: "思考中..."})]}),
partial: true
});
ctx.state.set('my_key', 'new_value');
yield createEvent({
author: this.name,
actions: createEventActions({stateDelta: {'my_key': 'new_value'}})
});
yield createEvent({
author: this.name,
content: createContent({parts: [createPart({text: "完了!"})]})
});
}
/**
* Simplified view of logic inside Agent.runAsync, callbacks, or tools in Kotlin
*/
suspend fun executionLogic(ctx: InvocationContext) {
// ... previous code runs based on current state ...
// 1. Determine a change or output is needed, construct the event
val updateData = mapOf("field_1" to "value_2")
val eventWithStateChange =
Event(
author = "my_agent",
actions = EventActions(stateDelta = updateData.toMutableMap()),
content = Content.fromText(Role.MODEL, "State updated."),
)
// 2. Yield the event to the Runner for processing & commit
// In Kotlin, this is done by emitting to the Flow
// emit(eventWithStateChange)
// <<<<<<<<<<<< EXECUTION PAUSES HERE >>>>>>>>>>>>
// (Implicitly, when the Flow consumer collects the event and processes it)
// <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>>
// 3. Resume execution ONLY after Runner is done processing.
// Now, the state committed by the Runner is reliably reflected.
val val1 = ctx.session.state["field_1"]
println("Resumed execution. Value of field_1 is now: $val1")
}
Event オブジェクトを介した Runner と実行ロジックの間の協調的な yield/一時停止/再開サイクルが、ADK ランタイムのコアを形成します。
ランタイムの主要コンポーネント¶
ADK ランタイム内でエージェント呼び出しを実行するために、いくつかのコンポーネントが連携して機能します。
-
Runner¶- 役割: 単一のユーザー クエリに対するメイン エントリ ポイントおよびオーケストレーター (
run_async)。 - 機能: 全体的なイベント ループを管理し、実行ロジックによって生成されたイベントを受信し、サービスと連携してイベント アクション (状態/アーティファクトの変更) を処理およびコミットし、処理されたイベントをアップストリーム (UI など) に転送します。生成されたイベントに基づいて会話をターンごとに駆動します (
google.adk.runnersで定義)。
- 役割: 単一のユーザー クエリに対するメイン エントリ ポイントおよびオーケストレーター (
-
実行ロジック コンポーネント (Execution Logic Components)¶
- 役割: カスタム コードとコア エージェント機能を含む部分。
- コンポーネント:
Agent(BaseAgent、LlmAgentなど): 情報を処理しアクションを決定するプライマリ ロジック ユニット。イベントを生成する_run_async_implメソッドを実装します。Tools(BaseTool、FunctionTool、AgentToolなど): エージェント (主にLlmAgent) が外部世界と対話したり、特定のタスクを実行したりするために使用する外部関数または機能。実行されて結果を返し、イベントにラップされます。Callbacks(関数): 実行フローの特定のポイントにフックし、動作や状態を変更する可能性のあるエージェントにアタッチされたユーザー定義関数 (before_agent_callback、after_model_callbackなど)。その効果はイベントにキャプチャされます。
- 機能: 実際の思考、計算、または外部との対話を実行します。
Eventオブジェクトを生成 (yield) し、Runner が処理するまで一時停止することで、結果やニーズを伝達します。
-
Event¶- 役割:
Runnerと実行ロジックの間でやり取りされるメッセージ。 - 機能: アトミックな発生 (ユーザー入力、エージェント テキスト、ツール呼び出し/結果、状態変更要求、制御信号) を表します。発生のコンテンツと意図された副作用 (
state_deltaなどのactions) の両方を保持します。
- 役割:
-
Services¶- 役割: 永続リソースまたは共有リソースの管理を担当するバックエンド コンポーネント。イベント処理中に主に
Runnerによって使用されます。 - コンポーネント:
SessionService(BaseSessionService、InMemorySessionServiceなど):Sessionオブジェクトの保存/読み込み、セッション状態へのstate_deltaの適用、event historyへのイベントの追加など、Sessionを管理します。ArtifactService(BaseArtifactService、InMemoryArtifactService、GcsArtifactServiceなど): バイナリ アーティファクト データの保存と取得を管理します。実行ロジック中にコンテキストを介してsave_artifactが呼び出されますが、イベント内のartifact_deltaは Runner/SessionService のアクションを確認します。MemoryService(BaseMemoryServiceなど): (オプション) ユーザーのセッション間にわたる長期的なセマンティック メモリを管理します。
- 機能: 永続化層を提供します。
Runnerはそれらと連携して、実行ロジックが再開される前に、event.actionsで通知された変更が確実に保存されるようにします。
- 役割: 永続リソースまたは共有リソースの管理を担当するバックエンド コンポーネント。イベント処理中に主に
-
Session¶- 役割: ユーザーとアプリケーション間の特定の 1 回の会話の状態と履歴を保持するデータ コンテナ。
- 機能: 現在の
state辞書、過去のすべてのeventsのリスト (event history)、および関連するアーティファクトへの参照を保存します。SessionServiceによって管理される対話のプライマリ レコードです。
-
Invocation¶- 役割:
Runnerがユーザー クエリを受信した瞬間から、エージェント ロジックがそのクエリに対するイベントの生成を終了するまで、単一のクエリに応じて発生するすべてを表す概念的な用語。 - 機能: 呼び出しには、単一の
InvocationContext内のinvocation_idによって結び付けられた複数のエージェントの実行 (エージェント転送またはAgentToolを使用する場合)、複数の LLM 呼び出し、ツール実行、およびコールバック実行が含まれる場合があります。プレフィックスtemp:が付いた状態変数は、単一の呼び出しに厳密にスコープされ、その後破棄されます。
- 役割:
仕組み: 簡略化された呼び出しフロー¶
LLM エージェントがツールを呼び出す一般的なユーザー クエリの簡略化されたフローを追跡してみましょう。

ステップバイステップの分析¶
- ユーザー入力: ユーザーがクエリを送信します (例: 「フランスの首都はどこですか?」)。
- Runner の開始:
Runner.run_asyncが始まります。SessionServiceと対話して関連するSessionを読み込み、ユーザー クエリをセッション履歴の最初のEventとして追加します。InvocationContext(ctx) が準備されます。 - エージェントの実行:
Runnerは、指定されたルート エージェント (LlmAgentなど) でagent.run_async(ctx)を呼び出します。 - LLM 呼び出し (例):
Agent_Llmは、ツールを呼び出すことによって情報を取得する必要があると判断します。LLMへのリクエストを準備します。LLM がMyToolを呼び出すことを決定したと仮定します。 - FunctionCall イベントの Yield:
Agent_Llmは LLM からFunctionCall応答を受信し、それをEvent(author='Agent_Llm', content=Content(parts=[Part(function_call=...)]))にラップし、このイベントをyieldまたはemitします。 - エージェントの一時停止:
Agent_Llmの実行はyieldの直後に一時停止します。 - Runner の処理:
Runnerは FunctionCall イベントを受信します。それを履歴に記録するためにSessionServiceに渡します。次に、Runnerはイベントをアップストリーム (Userまたはアプリケーション) に yield します。 - エージェントの再開:
Runnerはイベントが処理されたことを通知し、Agent_Llmは実行を再開します。 - ツールの実行:
Agent_Llmの内部フローは、要求されたMyToolの実行に進みます。tool.run_async(...)を呼び出します。 - ツールの結果の返却:
MyToolが実行され、結果を返します (例:{'result': 'Paris'})。 - FunctionResponse イベントの Yield: エージェント (
Agent_Llm) はツールの結果をFunctionResponseパートを含むEventにラップします。ツールが状態を変更した場合 (state_delta) やアーティファクトを保存した場合 (artifact_delta)、このイベントにactionsが含まれる場合があります。エージェントはこのイベントをyieldします。 - エージェントの一時停止:
Agent_Llmが再び一時停止します。 - Runner の処理:
Runnerは FunctionResponse イベントを受信します。すべてのstate_delta/artifact_deltaを適用し、イベントを履歴に追加するSessionServiceに渡します。Runnerはイベントをアップストリームに yield します。 - エージェントの再開:
Agent_Llmが再開し、ツールの結果とすべての状態変更がコミットされたことを認識します。 - 最終的な LLM 呼び出し (例):
Agent_Llmは自然言語応答を生成するために、ツールの結果をLLMに送り返します。 - 最終テキスト イベントの Yield:
Agent_Llmは LLM から最終テキストを受け取り、Event(author='Agent_Llm', content=Content(parts=[Part(text=...)]))にラップしてyieldします。 - エージェントの一時停止:
Agent_Llmが一時停止します。 - Runner の処理:
Runnerは最終テキスト イベントを受信し、履歴のためにSessionServiceに渡し、アップストリームでUserに yield します。これはis_final_response()としてマークされる可能性があります。 - エージェントの再開と完了:
Agent_Llmが再開します。今回の呼び出しに対するタスクを完了したため、run_asyncジェネレータが終了します。 - Runner の完了:
Runnerはエージェントのジェネレータが完了したことを確認し、今回の呼び出しに対するループを終了します。
重要なランタイム動作¶
状態の更新とコミットのタイミング (State Updates & Commitment Timing)¶
- ルール: コード (エージェント、ツール、またはコールバック内) がセッション状態を変更すると (例:
context.state['my_key'] = 'new_value')、この変更は最初、現在のInvocationContext内にローカルに記録されます。変更は、対応するstate_deltaを含むEventがコードによってyieldされ、その後Runnerによって処理された後にのみ永続化 (SessionService によって保存) されることが保証されます。 - 意味:
yieldから再開した後に実行されるコードは、生成されたイベントで示された状態変更がコミットされていると安全に想定できます。
# エージェント ロジック内部 (概念的)
# 1. 状態を変更
ctx.session.state['status'] = 'processing'
event1 = Event(..., actions=EventActions(state_delta={'status': 'processing'}))
# 2. デルタとともにイベントを yield
yield event1
# --- 一時停止 --- Runner が event1 を処理し、SessionService が 'status' = 'processing' をコミット ---
# 3. 実行を再開
# コミットされた状態に依存しても安全です
current_status = ctx.session.state['status'] # 'processing' であることが保証されます
print(f"Status after resuming: {current_status}")
// エージェント ロジック内部 (概念的)
// 1. 状態を変更
ctx.state.set('status', 'processing');
const event1 = createEvent({
actions: createEventActions({stateDelta: {'status': 'processing'}}),
// ... その他のイベント フィールド
});
// 2. デルタとともにイベントを yield
yield event1;
// --- 一時停止 --- Runner が event1 を処理し、SessionService が 'status' = 'processing' をコミット ---
// 3. 実行を再開
const currentStatus = ctx.session.state['status']; // 'processing' であることが保証されます
console.log(`Status after resuming: ${currentStatus}`);
func (a *Agent) RunConceptual(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
updateData := map[string]interface{}{"field_1": "value_2"}
eventWithStateChange := session.NewEvent(ctx, ctx.InvocationID())
eventWithStateChange.Author = a.Name()
eventWithStateChange.Actions = &session.EventActions{StateDelta: updateData}
if !yield(eventWithStateChange, nil) {
return
}
finalEvent := session.NewEvent(ctx, ctx.InvocationID())
finalEvent.Author = a.Name()
yield(finalEvent, nil)
}
}
ConcurrentHashMap<String, Object> stateChanges = new ConcurrentHashMap<>();
stateChanges.put("status", "processing");
EventActions actions = EventActions.builder().stateDelta(stateChanges).build();
Event event1 = Event.builder().actions(actions).build();
return Flowable.just(event1)
.map(emittedEvent -> {
String currentStatus = (String) ctx.session().state().get("status");
System.out.println("Status after resuming: " + currentStatus);
return emittedEvent;
});
/**
* Conceptual view of state update timing in Kotlin
*/
suspend fun stateUpdateTiming(ctx: InvocationContext) {
// 1. Modify state
ctx.session.state["status"] = "processing"
val event1 =
Event(
author = "my_agent",
actions = EventActions(stateDelta = mutableMapOf("status" to "processing")),
)
// 2. Yield event with the delta (emit to flow)
// emit(event1)
// --- PAUSE --- Runner processes event1, SessionService commits 'status' = 'processing' ---
// 3. Resume execution
// Now it's safe to rely on the committed state
val currentStatus = ctx.session.state["status"] // Guaranteed to be 'processing'
println("Status after resuming: $currentStatus")
}
セッション状態の「ダーティ リード (Dirty Reads)」¶
- 定義: コミットは yield の後に発生しますが、同じ呼び出し内で後から実行され、状態変更イベントが実際に生成されて処理される前に実行されるコードは、ローカルのコミットされていない変更を確認できることがよくあります。これは「ダーティ リード (dirty read)」と呼ばれることがあります。
# before_agent_callback のコード
callback_context.state['field_1'] = 'value_1'
# 状態はローカルで 'value_1' に設定されていますが、まだ Runner によってコミットされていません
# ... エージェントが実行 ...
# 同じ呼び出し内で後から呼び出されたツールのコード
# 読み取り可能 (ダーティ リード) ですが、'value_1' はまだ永続的であるとは保証されていません
val = tool_context.state['field_1'] # ここでは 'val' は 'value_1' になる可能性が高いです
print(f"Dirty read value in tool: {val}")
/**
* Conceptual view of dirty reads in Kotlin
*/
fun dirtyRead(ctx: InvocationContext) {
// Code in a callback
ctx.session.state["field_1"] = "value_1"
// State is locally set to 'value_1', but not yet committed by Runner
// ... agent runs ...
// Code in a tool called later *within the same invocation*
// Readable (dirty read), but 'value_1' isn't guaranteed persistent yet.
val val1 = ctx.session.state["field_1"] // 'val' will likely be 'value_1' here
println("Dirty read value in tool: $val1")
// Assume the event carrying the state_delta={'field_1': 'value_1'}
// is yielded *after* this tool runs and is processed by the Runner.
}
ストリーミング出力と非ストリーミング出力 (partial=True)¶
- ストリーミング: LLM はトークンごとまたは小さなチャンクで応答を生成します。フレームワークは単一の応答に対して複数の
Eventオブジェクトを生成し、そのほとんどにpartial=Trueが設定されています。Runnerはpartial=Trueのイベントを受信すると、すぐにアップストリームに転送しますが、state_deltaなどのactionsの処理はスキップします。最後の完了イベント (partial=False) のみでactionsを完全に処理して状態をコミットします。 - 非ストリーミング: LLM は応答全体を一度に生成します。フレームワークは
partial=Falseとマークされた単一のイベントを生成し、Runnerはそれを完全に処理します。
非同期がプライマリ (run_async)¶
- コア設計: ADK ランタイムは、ブロッキングなしで同時実行操作を効率的に処理するために、非同期パターンとライブラリ (Python の
asyncio、Java のRxJava、TypeScript のネイティブPromiseおよびAsyncGenerator) に基づいて構築されています。 - メイン エントリ ポイント:
Runner.run_asyncがエージェント呼び出しを実行するためのプライマリ メソッドです。 - 同期の利便性 (
run): 同期のRunner.runメソッドは主に利便性のために存在します (簡単なスクリプトやテストなど)。内部的には、Runner.runはRunner.run_asyncを呼び出し、非同期イベント ループの実行を管理します。 - 同期コールバック/ツール:
- ブロッキング I/O: 長時間実行される同期 I/O 操作の場合、フレームワークが常にストールを防げるわけではありません。Python ADK は asyncio イベント ループ上で同期ツール関数をインラインで呼び出すため、内部のブロッキング入出力はループを停止させます。ライブ モードでは、
RunConfig.tool_thread_pool_configを設定してツールの実行をバックグラウンド スレッド プールで実行できます。Java ADK はブロッキング呼び出しのために適切な RxJava スケジューラまたはラッパーに依存することがよくあります。TypeScript では、フレームワークは単に関数を await します。同期関数がブロッキング I/O を実行すると、イベント ループが停止します。開発者は可能な限り非同期 I/O API (Promise を返す) を使用する必要があります。 - CPU バウンドの作業: 純粋に CPU を消費する同期タスクは、両方の環境で実行スレッドをブロックします。
- ブロッキング I/O: 長時間実行される同期 I/O 操作の場合、フレームワークが常にストールを防げるわけではありません。Python ADK は asyncio イベント ループ上で同期ツール関数をインラインで呼び出すため、内部のブロッキング入出力はループを停止させます。ライブ モードでは、