TypeScriptADK-TS

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_run callbacks (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_run callbacks
  • 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 searchTool for 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 stateDelta in 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 runAsync in production (returns AsyncGenerator<Event>)
  • Implement tools and callbacks as async functions
  • Avoid blocking operations in the event loop
  • Use Worker Threads or external services for CPU-intensive tasks

Next Steps