Invocation Lifecycle
Complete lifecycle from user query to response completion
An invocation represents everything that happens in response to a single user query, from initial receipt until the agent finishes processing.
Step-by-Step Breakdown
- User Input: The user sends a query (for example, "What's the capital of France?").
- Runner Starts:
Runner.runAsyncbegins. It loads the relevantSessionviaSessionService, appends the user query as the firstEventin the history, and prepares anInvocationContext(ctx). - Agent Execution: The
Runnercallsagent.runAsync(ctx)on the designated agent (for example, aLlmAgent). - LLM Call (Example): The agent determines it needs external information and prepares a request for the LLM. Suppose the LLM decides to call
MyTool. - Yield FunctionCall Event: The agent receives the
FunctionCallfrom the LLM and wraps it in anEvent(e.g.,author: agent.name,content.parts: [{ functionCall: { name: 'MyTool', args: { ... } } }]), then yields it. - Agent Pauses: The agent's async generator pauses immediately after the
yield. - Runner Processes: The
Runnerreceives theFunctionCallevent, records it in the session viaSessionService, and emits it upstream to the user interface. - Agent Resumes: The
Runneradvances the generator (implicit vianext()), and the agent resumes execution. - Tool Execution: The runtime executes the requested tool on behalf of the agent (implementation calls
tool.runAsync(...)) and routes the result back into the agent's flow. - Tool Returns Result:
MyToolcompletes and returns a result (for example,{ result: 'Paris' }). - Yield FunctionResponse Event: The agent wraps the tool result in an
Eventcontaining aFunctionResponsepart (for example,content.parts: [{ functionResponse: { name: 'MyTool', response: { ... } } }]). If applicable, the event can also includeactionssuch asstateDeltaorartifactDelta. - Agent Pauses: The agent pauses again after yielding the
FunctionResponse. - Runner Processes: The
Runnerreceives theFunctionResponse, applies anyactions.stateDelta/actions.artifactDeltathroughSessionService, persists the event, and emits it upstream. - Agent Resumes: The agent resumes, now with the tool result available and any state/artifact changes committed.
- Final LLM Call (Example): The agent sends the tool result to the LLM to generate a natural language answer.
- Yield Final Text Event: The agent wraps the final text into an
Event(for example,content.parts: [{ text: 'The capital of France is Paris.' }]) and yields it. - Agent Pauses: The agent pauses again.
- Runner Processes: The
Runnerpersists the final text event viaSessionServiceand emits it to the user. This event is typically marked asisFinalResponse(). - Agent Resumes & Finishes: The agent resumes and, having no more events to yield for this invocation, its
runAsyncgenerator completes. - Runner Completes: The
Runnerdetects that the agent's generator is exhausted and finishes the loop for this invocation.
Important Runtime Behaviors
Understanding a few key aspects of how the ADK TypeScript Runtime handles state, streaming, and asynchronous execution will help you build predictable and efficient agents.
State Updates & Commitment Timing
- The rule: When your code (in an agent, tool, or callback) modifies session state, the change is locally visible within the current invocation immediately, but it is only guaranteed to be persisted after you yield a non-partial
Eventthat includes the correspondingstateDeltain itsactions. TheRunnerpersists that event viaSessionService.appendEvent(...), which applies thestateDeltato the session. - Implication: Code that runs after resuming from a
yieldcan reliably assume that the state changes signaled in the yielded event have been committed.
TypeScript example (agent logic):
import { Event, EventActions } from "@iqai/adk";
import type { InvocationContext } from "@iqai/adk";
async function* runAsyncImpl(ctx: InvocationContext) {
// 1) Stage a state change locally and construct an event that carries the delta
ctx.session.state["status"] = "processing"; // local visibility (dirty until committed)
const event1 = new Event({
invocationId: ctx.invocationId,
author: ctx.agent.name,
actions: new EventActions({ stateDelta: { status: "processing" } }),
content: { parts: [{ text: "Starting work..." }] },
partial: false,
});
// 2) Yield event with the delta
yield event1;
// --- PAUSE --- Runner persists event1 and applies stateDelta
// 3) Resume execution – state is now guaranteed committed
const current = ctx.session.state["status"]; // "processing"
// ... continue
}TypeScript example (tool logic using ToolContext):
import type { ToolContext } from "@iqai/adk";
export async function runAsync(params: any, toolCtx: ToolContext) {
toolCtx.state["progress"] = "started"; // local visibility during this invocation
toolCtx.actions.stateDelta["progress"] = "started"; // ensure it’s committed with the functionResponse event
return { ok: true };
}Notes:
- Use
CallbackContext.state/ToolContext.statefor delta-aware state updates. The runtime will merge local changes and the providedstateDeltawhen a non-partial event is persisted. - Persisted state updates occur only on non-partial events. Partial (streaming) events are forwarded but not committed.
"Dirty Reads" of Session State
- Definition: While commitment happens after the yield of a non-partial event, code running later within the same invocation (before the commit) can often see local, uncommitted changes. This is a “dirty read.”
- Benefit: Lets multiple parts of your logic within a single invocation (e.g., callbacks and tools) coordinate via state without waiting for a full yield/commit cycle.
- Caveat: Don’t rely on dirty reads for critical transitions. If the invocation fails before the committing event is yielded and processed, the uncommitted change is lost. For critical updates, ensure you attach a
stateDeltato a non-partial event that will be processed by theRunner.
TypeScript example (callback ➜ tool within the same invocation):
// beforeAgent callback
async function beforeAgentCallback(cbCtx: CallbackContext) {
cbCtx.state["field_1"] = "value_1"; // staged locally
cbCtx.eventActions.stateDelta["field_1"] = "value_1"; // will commit when the next non-partial event is persisted
}
// later, during the same invocation, inside a tool
async function runAsync(_params: any, toolCtx: ToolContext) {
const val = toolCtx.state["field_1"]; // likely "value_1" (dirty read)
// ... do work
return { value: val };
}Streaming vs. Non-Streaming Output (partial)
This concerns how LLM responses are handled, especially when using streaming generation APIs.
- Streaming: The LLM generates its response in chunks. The framework yields multiple
Eventobjects for one conceptual response. Most of these havepartial: true. - The Runner forwards partial events immediately to the UI but does not persist them or apply
actions(e.g.,stateDelta). - Eventually, a final event for that response is yielded with
partial: false(or implicitly as the turn’s completion). The Runner persists only this final event and applies anyactions.stateDelta/artifactDelta. - Non-Streaming: The LLM returns the whole response at once; the framework yields a single non-partial event, which the Runner processes fully.
- Why it matters: Ensures state changes are applied atomically and only once based on the complete response, while still allowing progressive UI updates.
Conceptual Runner loop:
for await (const event of agent.runAsync(invocationContext)) {
if (!event.partial) {
await sessionService.appendEvent(session, event); // applies stateDelta/artifactDelta
}
yield event; // always forward upstream
}Async is Primary (runAsync)
- Core design: The ADK TS Runtime is asynchronous and uses async generators to handle concurrent operations without blocking.
- Main entry point:
Runner.runAsyncreturns anAsyncGenerator<Event>for each invocation. - Synchronous convenience (
run): A sync wrapper exists primarily for local testing. Internally it drivesrunAsyncand buffers yielded events; preferrunAsyncin production. - Callbacks/tools: You can implement tools and callbacks as
asyncfunctions. Synchronous code works but can block the Node.js event loop. For long-running or CPU-bound work, consider using Worker Threads, child processes, or external queues/services to avoid blocking.
Understanding these behaviors will help you write robust ADK TS applications and debug issues related to state consistency, streaming updates, and asynchronous execution.
How is this guide?