TypeScriptADK-TS

Event Patterns

Practical ADK-TS event patterns — chat loops, tool activity indicators, agent transfer tracking, state replay, and audit logs built on the real framework APIs.

These patterns address concrete problems you encounter when building with ADK-TS events. Each one is grounded in the actual APIs — runner.runAsync(), event.actions, session.events, and the helper methods on Event.

Multi-turn chat loop

A production chat loop needs to handle streaming chunks, tool activity, agent transfers, and the final reply in a single pass. This pattern separates each concern without nested conditionals:

import { AgentBuilder } from "@iqai/adk";

const { runner } = await AgentBuilder.create("assistant")
  .withModel("gemini-2.5-flash")
  .withInstruction("You are a helpful assistant.")
  .build();

async function chat(userId: string, sessionId: string, message: string) {
  let streamBuffer = "";

  for await (const event of runner.runAsync({
    userId,
    sessionId,
    newMessage: { role: "user", parts: [{ text: message }] },
  })) {
    // 1. Accumulate streaming chunks for real-time display
    if (event.partial && event.content?.parts?.[0]?.text) {
      streamBuffer += event.content.parts[0].text;
      onStreamChunk(streamBuffer);
      continue;
    }

    // 2. Show tool activity while the agent is working
    const calls = event.getFunctionCalls();
    if (calls.length > 0) {
      onToolStart(calls.map(c => c.name));
      continue;
    }

    const responses = event.getFunctionResponses();
    if (responses.length > 0) {
      onToolEnd(responses.map(r => r.name));
      continue;
    }

    // 3. React to state changes as they happen
    if (Object.keys(event.actions.stateDelta).length > 0) {
      onStateChange(event.actions.stateDelta);
    }

    // 4. Display the completed reply
    if (event.isFinalResponse()) {
      const text = event.content?.parts?.[0]?.text ?? streamBuffer;
      onFinalReply(text.trim());
      streamBuffer = "";
    }
  }
}

Tool activity indicator

When an agent makes tool calls, users need feedback that something is happening. Function-call and function-response events bracket the tool execution window — use them to drive a loading state:

for await (const event of runner.runAsync(request)) {
  const calls = event.getFunctionCalls();
  const responses = event.getFunctionResponses();

  if (calls.length > 0) {
    const names = calls.map(c => c.name).join(", ");
    setStatus(`Using tools: ${names}…`);
  } else if (responses.length > 0) {
    setStatus("Processing results…");
  } else if (event.isFinalResponse()) {
    setStatus("idle");
  }
}

For long-running tools specifically, the longRunningToolIds field appears on a final-response event before the tool finishes. This is the signal to show a persistent background indicator rather than a brief spinner:

for await (const event of runner.runAsync(request)) {
  if (event.longRunningToolIds && event.isFinalResponse()) {
    showBackgroundProcessingBanner(
      `Running in background: ${[...event.longRunningToolIds].join(", ")}`,
    );
  }

  if (event.isFinalResponse() && event.content?.parts?.[0]?.text) {
    displayReply(event.content.parts[0].text);
  }
}

Agent transfer tracking

In multi-agent systems, event.actions.transferToAgent appears the moment the active agent hands off to another. Watch for it to update a "currently talking to" indicator in your UI:

let activeAgent = "assistant";

for await (const event of runner.runAsync(request)) {
  if (event.actions.transferToAgent) {
    activeAgent = event.actions.transferToAgent;
    updateAgentLabel(activeAgent);
  }

  if (event.isFinalResponse() && event.content?.parts?.[0]?.text) {
    displayMessage({ author: event.author, text: event.content.parts[0].text });
  }
}

event.author identifies which agent produced each event. In a multi-agent conversation this lets you render a distinct name or avatar for each agent:

for await (const event of runner.runAsync(request)) {
  if (!event.isFinalResponse()) continue;
  const text = event.content?.parts?.[0]?.text;
  if (!text) continue;

  appendMessage({
    author: event.author === "user" ? "You" : event.author,
    text,
    // event.branch is the dotted path, e.g. "coordinator.billing"
    agentPath: event.branch,
  });
}

Reactive state updates

Rather than re-fetching session.state after each turn, read event.actions.stateDelta to learn exactly what changed. This is efficient for driving UI components that depend on individual state keys:

for await (const event of runner.runAsync(request)) {
  const delta = event.actions.stateDelta;

  // Only update the parts of the UI that changed
  if ("task_status" in delta) setTaskStatus(delta.task_status);
  if ("result_count" in delta) setResultBadge(delta.result_count);
  if ("app:theme" in delta) applyTheme(delta["app:theme"]);
}

Keys prefixed with temp_ are ephemeral and not persisted. Skip them if you are mirroring state to durable storage.

Artifact save notifications

event.actions.artifactDelta records every file saved during a turn. Use it to refresh previews or notify users without polling:

for await (const event of runner.runAsync(request)) {
  for (const [filename, version] of Object.entries(
    event.actions.artifactDelta,
  )) {
    if (filename.endsWith(".pdf")) {
      refreshPdfPreview(filename, version);
    } else if (filename.endsWith(".csv")) {
      refreshDataTable(filename, version);
    }
  }
}

Rebuild state from session history

session.events is the full chronological record of every event in a session. Walk the stateDelta fields in order to reconstruct what session.state looked like at any point — useful for debugging, auditing, or rewinding to a past turn:

import type { Session } from "@iqai/adk";

function rebuildStateAt(
  session: Session,
  beforeTimestamp: number,
): Record<string, any> {
  const state: Record<string, any> = {};

  for (const event of session.events) {
    if (event.timestamp >= beforeTimestamp) break;
    if (event.actions?.compaction) continue; // skip summary events

    for (const [key, value] of Object.entries(event.actions.stateDelta)) {
      if (!key.startsWith("temp_")) {
        state[key] = value;
      }
    }
  }

  return state;
}

Session audit log

session.events gives you a complete, ordered trace of everything that happened — who said what, which tools were called, and what state changed. This pattern builds a human-readable log from that history:

import type { Session } from "@iqai/adk";

function buildAuditLog(session: Session): string[] {
  const lines: string[] = [];

  for (const event of session.events) {
    const ts = new Date(event.timestamp * 1000).toISOString();

    const calls = event.getFunctionCalls();
    const responses = event.getFunctionResponses();

    if (calls.length > 0) {
      for (const call of calls) {
        lines.push(`[${ts}] ${event.author} called tool "${call.name}"`);
      }
    } else if (responses.length > 0) {
      for (const r of responses) {
        lines.push(`[${ts}] Tool "${r.name}" returned a result`);
      }
    } else if (event.content?.parts?.[0]?.text && !event.partial) {
      lines.push(`[${ts}] ${event.author}: ${event.content.parts[0].text}`);
    }

    const stateKeys = Object.keys(event.actions.stateDelta);
    if (stateKeys.length > 0) {
      lines.push(`[${ts}] State updated: ${stateKeys.join(", ")}`);
    }
  }

  return lines;
}

Next steps