Files
Liang Wu 98896eb2aa feat(live): let a live streaming tool send messages to the user directly
A streaming tool used to only talk to the model: every value it yielded came
back as a FunctionResponse. Therefore it costs model context and could derail the model's reasoning.

An Event with message yielded by a streaming tool is now addressed to the user instead. It is enqueued on the invocation's event queue, so the runner appends it to the session and streams it to the client, and it is never sent over the live model
connection. Plain values are still sent to the model as FunctionResponse. A tool can mix and match any number of each, in any order.

Runner's run_live also now initializes the invocation's event queue and merges it with the live agent's own event stream. Without a queue, anything running under
the live agent that enqueues an event -- a streaming tool, or a node -- fails
with "_event_queue is not set".

Co-authored-by: Liang Wu <wuliang@google.com>
PiperOrigin-RevId: 968533249
2026-08-21 09:15:04 -07:00
..

Streaming Tool Events

In a streaming tool, yield Event(message=...) to talk to the user directly, and yield <value> to give the model a result. Mix and match, in any order.

Overview

A streaming tool reports progress to the user while streaming results to the model, so narrating a long-running tool costs no model turn. Only supported in streaming (live) agents/api.

Sample Inputs

  • Help me monitor the stock price for $XYZ stock.

    The tool tells you directly that it connected to the feed, without going through the model. The price alerts do go to the model, and it reports them in its own words.

  • Stop monitoring $XYZ.

    The model calls stop_streaming, which cancels the background monitor.

Graph

graph TD
    Agent[streaming_tool_events_agent] -->|calls| Monitor(monitor_stock_price)
    Agent -->|calls| Stop(stop_streaming)

How To

Write an async generator and put it in tools. The yielded type picks the audience:

async def monitor_stock_price(stock_symbol: str) -> AsyncGenerator[Any, None]:
  """Starts a background monitor for the price of the given stock_symbol."""
  yield Event(message=f"Connected to the {stock_symbol} price feed.")
  yield f"the price for {stock_symbol} is 300"
  yield f"the price for {stock_symbol} is 900"
  yield Event(message="That is my last update for now.")

Key points:

  • User updates: yield Event(message=...) to send a message straight to the client. message takes a string, a types.Part or a types.Content. Framework metadata (author, branch, invocation_id, the content role) is filled in for you; any other field you set on the event is ignored with a warning, and the message is still delivered.
  • Model results: yield a plain value (str, dict, ...) to send a FunctionResponse back to the model.
  • Side effects: use tool_context.actions, not the event.

Where the message goes

The message is streamed to your client and appended to the session. It does not go over the live connection, so it consumes no model turns or tokens during the active turn and cannot derail the model's reasoning mid-task. It is ordinary session history, though, so the model does see it once the history is replayed on the next connect.