In ADK, the Event is the atom. Everything an agent system does — a user speaks, a model emits text, a model asks to call a tool, a tool answers, a piece of state changes, control transfers to a sub-agent — is expressed as one immutable Event flowing through a single stream. An agent does not return a result; it yields a sequence of events, and the Runner consumes that stream, forwards each event to your application, and persists it into the Session. That one design choice — make every occurrence a first-class, recorded object rather than a hidden side effect — is what makes ADK agents debuggable, replayable, and auditable. This article is about the Event object itself and the event-driven architecture around it: what an Event carries, the kinds of events that flow, how EventActions encode side effects and control signals, how the async stream is produced and consumed, the difference between partial and final events, and why the resulting history is the most valuable artifact your agent produces. It is deliberately not about token-level or live audio streaming — that is a transport concern covered separately; here the subject is the event as a unit of meaning.

Why the event is the fundamental unit

It is tempting to model an agent as a function: text in, text out. That framing survives exactly one turn. The moment an agent calls a tool, delegates to a sub-agent, updates working state, or streams a partial answer, ‘the result’ stops being a single value and becomes a sequence of things that happened over time. ADK takes that sequence seriously and makes it the primary abstraction: an agent’s output is a stream of Event objects, each one a discrete, labelled record of a single occurrence.

This reframing pays for itself immediately. Because every model call, every tool invocation, and every state change is an event with an author and a timestamp, ‘the agent did something weird’ becomes a concrete, inspectable log rather than a mystery buried inside a prompt loop. The event stream is simultaneously the communication channel between components, the API your application subscribes to for real-time UI, and the durable history that gets persisted for audit, evaluation, and resumption. One abstraction serves all three roles, which is why understanding the Event object unlocks the rest of the runtime: sessions, tools, callbacks, and workflow agents all speak in events.

Advertisement

Anatomy of the Event object

An ADK Event is a structured record, not a bare string. Its fields answer a few precise questions: who, what, what-else-happened, and when.

FieldMeaning
authorWho produced this event — 'user' or the name of the agent
contentThe payload: a Content with a role and a list of parts (text, function call, function response)
actionsAn EventActions carrying side effects and control signals (state deltas, transfer, escalate…)
invocation_idCorrelates every event produced within one run_async invocation
idA unique identifier for this individual event
timestampWhen the event was created
partial / turn_completeStreaming flags distinguishing an in-progress chunk from a completed message
branchThe agent-tree path, so events from nested agents stay attributable

The shape matters because it is uniform. A user message, a model’s reasoning, a request to call get_weather, and that tool’s answer are all the same kind of object with the same fields. Consumers do not need a different parser per message type; they inspect author, walk content.parts, and check actions. That uniformity is what lets a generic Runner drive an arbitrarily complex agent tree.

Content and parts: what an event carries

The substance of most events lives in event.content.parts, a list borrowed from the underlying generative model’s content format. A single event can carry several parts, and each part is one of a small number of shapes. The three that matter most are text, a function call, and a function response.

for part in event.content.parts:
    if part.text:
        print(part.text)                      # model or user prose
    elif part.function_call:
        fc = part.function_call
        print(fc.name, fc.args)               # the model wants to call a tool
    elif part.function_response:
        fr = part.function_response
        print(fr.name, fr.response)           # the tool's result

This is the crux of how a tool-using agent actually works. The model does not ‘call’ your Python function directly — it emits an event whose part is a function_call naming the tool and supplying arguments. The runtime executes the real function, then produces a follow-up event whose part is a function_response carrying the return value. Both are ordinary events on the same stream, which is why a tool call and its result appear in the history exactly where they happened, with the arguments the model chose and the value the tool returned both permanently recorded.

Who authors events: roles on the stream

Every event names its author, and that single field carries most of the semantics of ‘event type.’ ADK does not rely on a large enum of message kinds; instead it combines the author with the shape of the content to tell you what an event is.

AuthorContent shapeWhat it represents
'user'text (or media) partThe human’s input that opened the turn
agent nametext partThe agent’s natural-language reply or reasoning
agent namefunction_call partThe agent asking the runtime to run a tool
agent namefunction_response partA tool’s result, folded back in under the agent’s authorship
agent nameempty content, actions onlyA pure side effect: a state change or control signal

Function responses are worth a note: although the value originates in a tool, ADK records the response as an event so the tool’s output is a visible, replayable part of history rather than a hidden intermediate. In a multi-agent system the author becomes even more valuable — when a triage agent transfers to a research agent, the stream interleaves events from both, and author plus branch keep every line attributable to the agent that actually produced it. Reading a session transcript, you always know who said what.

EventActions: side effects and control on the stream

Text and function parts describe what was said. The actions field — an EventActions object — describes what changed and what should happen next. This is where ADK keeps the consequences of a turn out of the prose and in a structured, machine-readable place.

ActionEffect
state_deltaA map of session-state keys to write when this event is committed
artifact_deltaRecords that named binary artifacts were saved in this step
transfer_to_agentHand control to a named sub-agent
escalateBubble control up — e.g. break out of a LoopAgent
skip_summarizationTell the model not to re-summarize a tool result; use it verbatim

Bundling these onto the event, rather than mutating the world as a side effect, is a deliberate discipline. A state change is not applied the instant a tool runs; it is attached to an event as a state_delta and applied when the Runner commits that event to the session. The consequence is that history and state never disagree: replaying the events in order reproduces the exact sequence of state transitions, because each transition is an event. Control signals get the same treatment — a transfer or an escalate is data on the stream, so the reason the flow changed direction is recorded next to where it changed.

State deltas: how events mutate session state

ADK keeps a mutable key-value session.state, but agents and tools do not write to it directly and imperatively. Instead a change is expressed as a state_delta on an event’s actions, and the SessionService folds that delta into the materialized state when the event is appended. The most common way to produce one is via the ToolContext or CallbackContext handed to your code:

def record_city(city: str, tool_context: ToolContext) -> dict:
    # This assignment becomes a state_delta on the event the runtime emits.
    tool_context.state['last_city'] = city
    tool_context.state['user:preferred_city'] = city   # user-scoped, persists across sessions
    return {'status': 'ok'}

Scope prefixes on the key decide reach: a plain key is session-scoped, user: follows the user across sessions, app: is shared application-wide, and temp: lives only for the current invocation and is never persisted. Because the write travels as a delta on an event, you get atomicity and auditability for free: the state at any point in a conversation is exactly the left-fold of every state_delta up to that event, which means you can reconstruct why a value is what it is by reading the events that set it.

Control-flow events: transfer, escalate, loop exit

Multi-agent orchestration in ADK is not implemented with hidden function calls between agents; it is implemented with events. When an LlmAgent decides to delegate, it emits an event carrying transfer_to_agent in its actions, and the Runner responds by routing the next turn to the named sub-agent. The delegation is therefore visible on the stream — you can see the exact moment control moved and which agent requested it.

The escalate signal is the complement: it bubbles control upward. Inside a LoopAgent, a sub-agent that has satisfied the exit condition emits an event with escalate=True, and the loop stops iterating — a clean, data-driven way to say ‘we are done’ without a magic return value or an exception. This is how you build a critique-and-revise loop that terminates when a reviewer approves, or a retry loop that ends on success. Because both transfer and escalate are ordinary EventActions, an evaluation harness can assert on them directly: did the triage agent transfer to billing for this input? is a question you answer by scanning the recorded events, not by instrumenting the model. Control flow becomes testable precisely because it is expressed as data.

Advertisement

Agents yield events: the async stream

The production side of the stream is an asynchronous generator. An ADK agent’s core method does not build a list and return it; it yields events one at a time as they occur, which is what allows the caller to react the instant something happens rather than after the whole turn finishes.

class MyAgent(BaseAgent):
    async def _run_async_impl(self, ctx: InvocationContext):
        # ... do work, call the model, run a tool ...
        yield Event(author=self.name, content=some_text_content)
        yield Event(author=self.name, actions=EventActions(
            state_delta={'step': 'done'}))

The async/yield shape is doing real work here. It means an agent can emit a partial text chunk, then another, then a function-call event, then a function-response event, then a final message — and each is handed to the consumer as it is produced. It also composes: a workflow agent like SequentialAgent runs its children and simply re-yields their events upward, so a deeply nested tree still presents to the Runner as one flat, ordered stream. The generator model is the reason ADK can express both a simple one-shot reply and a long, multi-step, multi-agent run through the exact same interface — the only difference is how many events get yielded before the generator is exhausted.

The Runner consumes the stream and persists it

If agents are the producers, the Runner is the consumer and the system of record. You start a turn with runner.run_async(...), and it drives the root agent’s generator, doing three jobs for every event that comes out. First, it appends the event to the Session through the SessionService, which also applies any state_delta and artifact_delta the event carries. Second, it yields the event onward to your application code so you can render or log it. Third, it continues the loop — if the event was a tool call, the runtime executes the tool and feeds the resulting response event back so the model can react.

This is why persistence and delivery are never out of sync: the same event that reaches your UI is the one written to history, in the same order, in the same operation. It also cleanly separates concerns. Your application decides how to present events; the Runner guarantees they are durable and correctly ordered; the agent decides what events to produce. Swap the SessionService from in-memory to a database-backed implementation and nothing about the agent or the consumer loop changes — only where the same events land.

Iterating over run_async: the consumer loop

Here is the pattern nearly every ADK application is built around: iterate the async stream, branch on the shape of each event, and treat is_final_response() as the signal that the turn’s user-facing answer has arrived.

content = types.Content(role='user',
                        parts=[types.Part(text='Weather in Paris?')])

async for event in runner.run_async(user_id=uid,
                                    session_id=sid,
                                    new_message=content):
    # 1) natural-language and tool traffic
    if event.content and event.content.parts:
        for part in event.content.parts:
            if part.text:
                print(f'[{event.author}] {part.text}')
            elif part.function_call:
                print(f'[{event.author}] -> {part.function_call.name}'
                      f'({part.function_call.args})')
            elif part.function_response:
                print(f'[tool] <- {part.function_response.response}')

    # 2) side effects
    if event.actions and event.actions.state_delta:
        print('state:', event.actions.state_delta)

    # 3) the final answer for this turn
    if event.is_final_response():
        print('FINAL:', event.content.parts[0].text)

Notice that a single loop handles the model’s prose, its tool requests, the tools’ answers, and state changes — because they are all the same kind of object. You do not write separate handlers for ‘streaming’ versus ‘tool’ versus ‘final’ modes; you inspect fields on a uniform Event and decide what to do.

Partial vs final: is_final_response()

Not every event is meant to be shown as a completed message. During generation an agent may emit partial events — incremental chunks flagged with partial=True — so a UI can render text as it is produced. These are genuine events on the stream, but they are provisional: the same content will be re-delivered as a consolidated, non-partial event when the message completes. Treating a partial as final leads to duplicated or truncated output.

To cut through this, ADK gives Event a helper: event.is_final_response(). It returns True for the event that represents the actual, user-facing answer for the current step — a completed message that is not partial and is not merely a request to call a tool. Intermediate events — partial chunks, function calls, function responses, pure state deltas — return False. This single predicate is what lets your consumer loop distinguish ‘still working’ from ‘here is the reply.’ The distinction also matters for logging and evaluation: you often want to capture all events for the audit trail but surface only final responses to the end user, and is_final_response() is the line between those two views of the same stream.

Events as durable, replayable history

Because the Session is the ordered list of every event, it is far more than a chat log — it is a complete, replayable record of the run. This is where the event-driven design earns its keep in production. Observability: a misbehaving agent is diagnosed by reading its events, since every model decision, tool argument, tool result, and state change is right there in order. Evaluation: recorded sessions become test fixtures — you assert that the right tool was called with the right arguments, or that control transferred to the right sub-agent, by inspecting events rather than re-running a nondeterministic model. Resumption: because state is the fold of state-deltas over the event history, a crashed or paused conversation can be reconstructed exactly by reloading its events.

Contrast this with the naive while-loop agent, where intermediate reasoning and tool calls evaporate the moment the function returns. There, ‘why did it do that?’ is unanswerable after the fact. In ADK the answer is always on disk, because the thing that drives the agent and the thing that records it are the same stream. The event log is, in a real sense, the most durable and valuable artifact your agent produces — more durable than any single answer it gives.

Inspecting events: practical patterns and gotchas

A few habits make working with the stream smoother. Always guard content access — event.content and event.content.parts can be absent on pure-action events, so check before you iterate. Use the convenience accessors when they exist (for example gathering the function calls in an event) rather than manually scanning parts every time. And treat author plus invocation_id as your primary keys when reasoning about a run: the invocation id groups every event of a single run_async call, and the author tells you which agent in the tree emitted each one.

The classic mistakes all come from forgetting that events are a stream, not a return value. Rendering partial events as if they were final duplicates text. Applying state yourself instead of via a state_delta desynchronizes history from state. Assuming one turn equals one event breaks the moment a tool is involved — a single question can produce a dozen events (reasoning, call, response, more reasoning, final). And expecting the final answer before the stream is exhausted misses it, because is_final_response() may only become true near the end. Internalize that an agent’s output is the event sequence, and these edge cases stop being surprises and become the expected texture of an event-driven runtime.

In ADK, the Event is the unit of communication and the unit of memory. Every occurrence — user input, model text, a tool-call request, a tool response, a state delta, a transfer or escalate — is one immutable event with an author, content, actions, invocation_id, and timestamp. Agents yield these events as an asynchronous stream; the Runner consumes each one, forwards it to your application, and persists it to the Session along with any state or artifact deltas it carries. Because side effects and control signals ride on events rather than happening invisibly, history and state can never disagree, and is_final_response() cleanly separates the finished answer from the partial and intermediate traffic around it. The payoff is that the event log becomes a durable, replayable record — the substrate for observability, evaluation, and resumption. Model your agent as a producer of a meaningful event sequence, iterate the stream from run_async, and the rest of the runtime falls into place.