TypeScriptADK-TS
Runtime

Event Loop

Understanding the cooperative async generator pattern and event flow

At the heart of the ADK Runtime is an Event Loop that facilitates communication between the Runner component and your execution logic (agents, tools, callbacks).

Event-Driven Architecture

The Runtime operates on a cooperative async generator pattern where components communicate through events:

  1. Runner receives user query and initiates agent processing
  2. Agent executes logic until it has something to report (response, tool call, state change)
  3. Event yielded by agent as part of async generator
  4. Runner processes event, commits changes via services, forwards event upstream
  5. Agent resumes execution after Runner completes event processing
  6. Cycle repeats until agent has no more events for the current query

Event Processing Order

Agent execution pauses after yielding events until the Runner completes processing. This ensures state consistency and proper event ordering.

Execution Flow

Runner Role (Orchestrator)

The Runner acts as the central coordinator:

  • Query Processing: Receives user queries and initializes sessions
  • Event Coordination: Processes events from execution logic
  • Service Integration: Coordinates with SessionService, ArtifactService, MemoryService
  • Upstream Communication: Forwards processed events to user interfaces
  • State Management: Ensures consistent state across the entire system

Agent Role (Execution Logic)

Agents and other execution components:

  • Computation: Perform reasoning and decision-making
  • Event Generation: Yield events to communicate results or needs
  • Cooperative Execution: Pause execution until Runner processes events
  • Context Awareness: Resume with updated state and context

Event Flow Example

Here's how events flow through the system:

import { LlmAgent, Runner, InMemorySessionService, Event } from '@iqai/adk';

async function demonstrateEventFlow() {
  const agent = new LlmAgent({
    name: "demo_agent",
    model: "gemini-2.5-flash",
    description: "Demonstrates event flow",
    instruction: "You are helpful and explain your thinking"
  });

  const sessionService = new InMemorySessionService();
  const session = await sessionService.createSession("demo", "user1");

  const runner = new Runner({
    appName: "demo",
    agent,
    sessionService
  });

  // Each iteration represents one event in the flow
  for await (const event of runner.runAsync({
    userId: "user1",
    sessionId: session.id,
    newMessage: { parts: [{ text: "What's 2+2?" }] }
  })) {
    console.log(`Event from ${event.author}:`, {
      content: event.content?.parts?.map(p => p.text).join(' '),
      hasActions: !!event.actions,
      isFinal: event.isFinalResponse()
    });
  }
}

Event Types and Processing

Content Events

Events containing user messages or agent responses:

// User message event (generated by Runner)
const userEvent = new Event({
  invocationId: "inv_123",
  author: "user",
  content: {
    parts: [{ text: "Hello, how are you?" }]
  }
});

// Agent response event (generated by Agent)
const agentEvent = new Event({
  invocationId: "inv_123",
  author: "my_agent",
  content: {
    parts: [{ text: "I'm doing well, thank you!" }]
  }
});

Function Call Events

Events requesting tool execution:

// Function call event
const functionCallEvent = new Event({
  invocationId: "inv_123",
  author: "my_agent",
  content: {
    parts: [{
      functionCall: {
        name: "search_web",
        args: { query: "latest AI news" }
      }
    }]
  }
});

// Function response event
const functionResponseEvent = new Event({
  invocationId: "inv_123",
  author: "my_agent",
  content: {
    parts: [{
      functionResponse: {
        name: "search_web",
        response: { results: [...] }
      }
    }]
  }
});

State Change Events

Events that modify session state:

import { EventActions, Event } from '@iqai/adk';

const stateChangeEvent = new Event({
  invocationId: "inv_123",
  author: "my_agent",
  actions: new EventActions({
    stateDelta: {
      "user_preference": "dark_mode",
      "session_count": 5
    }
  })
});

Async Generator Pattern

Agent Implementation

Agents implement the async generator pattern in their runAsyncImpl method:

import { BaseAgent, InvocationContext, Event } from '@iqai/adk';

class CustomAgent extends BaseAgent {
  protected async *runAsyncImpl(
    ctx: InvocationContext
  ): AsyncGenerator<Event, void, unknown> {

    // Step 1: Analyze user input
    yield new Event({
      invocationId: ctx.invocationId,
      author: this.name,
      content: { parts: [{ text: "Let me think about this..." }] }
    });

    // Step 2: Perform some computation
    const result = await this.processUserQuery(ctx.userContent);

    // Step 3: Yield final response
    yield new Event({
      invocationId: ctx.invocationId,
      author: this.name,
      content: { parts: [{ text: result }] }
    });
  }
}

Runner Processing

The Runner processes each yielded event:

// Simplified Runner event processing
for await (const event of agent.runAsync(invocationContext)) {
  // 1. Validate event
  if (!event.partial) {
    // 2. Persist to session
    await this.sessionService.appendEvent(session, event);

    // 3. Apply state changes
    if (event.actions?.stateDelta) {
      // Apply state changes through service
    }

    // 4. Handle artifacts
    if (event.actions?.artifactDelta) {
      // Process artifact changes
    }
  }

  // 5. Forward upstream
  yield event;
}

Event Processing Guarantees

Ordering Guarantees

  • Events are processed in the order they are yielded
  • State changes are applied atomically per event
  • No race conditions between event processing

State Consistency

  • Agent execution pauses until Runner processes events
  • State changes are committed before agent resumes
  • Rollback support for failed operations

Error Handling

  • Exceptions in event processing are propagated to agents
  • Partial state changes are rolled back on failure
  • Graceful degradation for service failures

Streaming and Partial Events

Partial Events

For real-time user experience, agents can yield partial events:

// Streaming response
yield new Event({
  invocationId: ctx.invocationId,
  author: this.name,
  content: { parts: [{ text: "Thinking" }] },
  partial: true  // Not persisted to session
});

yield new Event({
  invocationId: ctx.invocationId,
  author: this.name,
  content: { parts: [{ text: "Thinking..." }] },
  partial: true
});

// Final response
yield new Event({
  invocationId: ctx.invocationId,
  author: this.name,
  content: { parts: [{ text: "Here's my complete response" }] },
  partial: false  // Persisted to session
});

Live Mode

For real-time interactions, the Runtime supports live mode:

// Live mode processing (voice/video)
for await (const event of agent.runLive?.(invocationContext) ||
                          agent.runAsync(invocationContext)) {
  // Handle real-time events
  if (event.content) {
    sendToUserInterface(event);
  }
}

Best Practices

Event Design

  • Single Responsibility: Each event should represent one logical operation
  • Immutable Content: Event content should not be modified after creation
  • Descriptive Authors: Use clear agent names for event attribution
  • Appropriate Granularity: Balance between too many and too few events

Performance Optimization

  • Batch State Changes: Group related state changes in single events
  • Minimize Event Overhead: Avoid excessive event generation
  • Use Partial Events: Stream content for better user experience
  • Efficient Serialization: Keep event payloads reasonably sized

Error Recovery

  • Graceful Failures: Handle service failures without breaking the flow
  • Event Validation: Validate events before processing
  • Rollback Support: Design for operation rollback on failures
  • Circuit Breakers: Prevent cascading failures