Invocation Lifecycle
Understanding how the Runner executes a single user request from start to finish
An invocation represents a complete execution cycle: from receiving a user's message to delivering the final response. Understanding this lifecycle helps you debug issues, optimize performance, and build predictable agent behavior.
What is an Invocation?
Each call to runner.runAsync() creates a new invocation with a unique invocationId. All events yielded during that execution share this ID, linking them to that specific request-response cycle.
Lifecycle Overview
Execution Phases
An invocation progresses through several phases:
1. Initialization
- Runner loads or creates the session via
SessionService - User's message is appended to session history
- Plugins execute
before_runcallbacks (may cause early exit) - Invocation context is created with unique
invocationId
2. Agent Execution
- Runner calls
agent.runAsync(context)to start the agent - Agent processes input and begins yielding events
- Each event flows through the Runner's processing pipeline
3. Event Processing Loop
- Agent yields events (messages, tool calls, state updates)
- Runner processes each event:
- Partial events: Yielded immediately to client
- Complete events: Persisted, state applied, then yielded
- Loop continues until agent yields final response
4. Completion
- Plugins execute
after_runcallbacks - Invocation ends, returning control to caller
Detailed Walkthrough
Let's trace a typical invocation where an agent uses a tool:
Step 1: User Query
for await (const event of runner.runAsync({
userId: "user_123",
sessionId: "session_456",
newMessage: { parts: [{ text: "What's the capital of France?" }] },
})) {
console.log(event);
}Step 2: Session Initialization
- Runner loads session from
SessionService - User message added to session events
- Session state available to agent
Step 3: Agent Determines Action
- Agent receives context with session history
- Decides to call
searchToolfor factual information - LLM returns function call instruction
Step 4: Function Call Event
// Agent yields this event
yield new Event({
author: "assistant",
content: {
parts: [
{
functionCall: {
name: "searchTool",
args: { query: "capital of France" },
},
},
],
},
});- Runner receives event and persists it
- Event yielded to client for UI updates
- Agent execution pauses at this yield point
Step 5: Tool Execution
- Runner resumes agent (via generator protocol)
- Agent executes the tool internally
- Tool returns result:
{ result: 'Paris' }
Step 6: Function Response Event
// Agent yields tool result
yield new Event({
author: "user",
content: {
parts: [
{
functionResponse: {
name: "searchTool",
response: { result: "Paris" },
},
},
],
},
});- Runner persists function response
- Event yielded to client
Step 7: Final Response
- Agent sends tool result to LLM for synthesis
- LLM generates natural language answer
// Agent yields final response
yield new Event({
author: "assistant",
content: { parts: [{ text: "The capital of France is Paris." }] },
partial: false,
});- Runner persists final event
- Event marked as final response (
isFinalResponse() === true) - Invocation completes
Key Runtime Behaviors
Understanding these behaviors helps you build predictable agents and debug issues effectively.
State Persistence Timing
State changes are locally visible immediately but persisted only when a complete event is yielded.
async function* runAsyncImpl(ctx: InvocationContext) {
// Modify state locally
ctx.session.state.status = "processing";
// Yield event with stateDelta to persist the change
yield new Event({
author: ctx.agent.name,
content: { parts: [{ text: "Starting work..." }] },
actions: new EventActions({
stateDelta: { status: "processing" },
}),
partial: false, // Must be complete to persist
});
// State is now persisted - guaranteed committed
const status = ctx.session.state.status; // 'processing'
}Key Points:
- State changes are visible locally throughout the invocation
- Only complete events (
partial: false) trigger persistence - Include
stateDeltain event actions to commit changes - After yielding, you can rely on the state being saved
State Visibility Within Invocation
During a single invocation, state changes are visible across all components (agents, tools, callbacks) even before persistence:
// Before callback modifies state
async function beforeAgentCallback(ctx: CallbackContext) {
ctx.state.userPreference = "dark";
// Visible immediately, persisted when event yields
}
// Tool can see the change in same invocation
async function toolRun(params: any, ctx: ToolContext) {
const pref = ctx.state.userPreference; // 'dark'
return { theme: pref };
}Important: If the invocation fails before yielding a complete event with stateDelta, uncommitted changes are lost. For critical state, always include stateDelta in your events.
Streaming Events
When streaming is enabled, agents can yield multiple partial events before the final complete event:
// Streaming response
for await (const event of runner.runAsync({
userId,
sessionId,
newMessage,
runConfig: { streamingMode: StreamingMode.SSE },
})) {
if (event.partial) {
// Real-time update - NOT persisted
updateUI(event.content);
} else {
// Complete event - persisted with state changes
finalizeUI(event.content);
}
}Streaming behavior:
- Partial events: Yielded immediately for UI updates, not persisted
- Complete event: Persisted with all state/artifact changes
- Only the final complete event applies
stateDelta/artifactDelta
Non-streaming behavior:
- Single complete event yielded with full response
- State changes applied immediately
Asynchronous Execution
ADK-TS is built on async generators for non-blocking execution:
// Primary API - async generator
for await (const event of runner.runAsync(config)) {
// Process events asynchronously
await handleEvent(event);
}Best practices:
- Always use
runAsyncin production (returnsAsyncGenerator<Event>) - Implement tools and callbacks as
asyncfunctions - Avoid blocking operations in the event loop
- Use Worker Threads or external services for CPU-intensive tasks