Repository navigation
Expose the /run_sse event serialization as a public function and allow an optional SSE keep-alive #7578
Description
🔴 Required Information
Is your feature request related to a specific problem?
Applications that need different run semantics than the built-in /run_sse
(for example running the Runner outside the HTTP request, or fronting it with
their own transport) still want to emit the same wire format so existing ADK
clients keep working. Today that format is not reusable:
- The serialization lives inline in the nested
event_generator()in
cli/api_server.py. It combines three steps: theartifactDeltasplit into a
content event and an action-only event,public_event(...)from
events/_internal_metadata.py, and_redact_credential_secrets(...)from
auth/auth_credential.py, thenpydantic_core.to_json(...). - Two of those helpers are private (underscore module / name), so a downstream
implementation has to copy the logic and keep it in sync by hand.
The sync cost is real. The framing changed in 2.11 (public_event was
introduced into the stream) and again in 2.12 (credential redaction), and each
time a downstream copy had to be updated and its golden-framing test re-pinned.
A copy that silently diverges either leaks fields the server hides or drops
fields clients expect.
Separately, the stream is silent while the agent waits on a slow tool: nothing
is written between events, and the response writes no periodic frame. Proxies and
clients that treat prolonged silence as a dead stream close it. (Combined with
abort-on-disconnect, see #4029, that ends the run.)
Describe the Solution You'd Like
- A public, documented function that turns an
Eventinto the SSE frame
exactly asrun_agent_ssedoes (including the redaction step), used by
run_agent_sseitself so the two cannot diverge. For example
google.adk.cli.sse.format_event_frame(event, *, split_artifact_delta=True). - An optional keep-alive for
/run_sse: when no event has been written for N
seconds, emit an SSE comment (: keep-alive\n\n), which clients ignore per the
SSE spec. Off by default, so existing behavior is unchanged.
Impact on your work
We maintain a downstream implementation of the stream and re-verify it against
every ADK minor. A public serializer turns that from "diff a private handler" into
"call a function". The keep-alive removes the need for us to inject comment frames
ourselves.
Willingness to contribute
Yes, for both. Item 1 is a refactor with a unit test that asserts the function and
the handler produce identical bytes. Item 2 is a small timer in the generator plus
a test. I would send them as two separate PRs.
🟡 Recommended Information
Describe Alternatives You've Considered
- Copying the serialization (current approach): works, but must be re-checked on
every release, as the 2.11 and 2.12 changes showed. - Wrapping
/run_ssein a proxy that re-frames the response: loses event-level
control and adds a hop. - Comment frames written by a wrapper around the stream: fine as a workaround,
but every deployment has to reinvent it.
Proposed API / Implementation
# google/adk/cli/sse.py (illustrative)
def format_event_frame(event: Event, *, split_artifact_delta: bool = True) -> list[str]:
"""Returns the `data: ...\n\n` frame(s) `run_agent_sse` writes for `event`."""
# get_fast_api_app(..., sse_keep_alive_seconds: float | None = None)Additional Context
The contract is already de facto public: clients consume it. This asks for it to
be explicit and shared rather than copied.
Activity
ALDRIN121 commentedon Oct 17, 2026
Claiming item 1 and PR is up: #7594. Following the assign-first rule in CONTRIBUTING, could a maintainer assign this issue to me?
Scope is item 1 only — extracting the /run_sse framing (
public_event→model_dump(exclude_none=True, by_alias=True, mode="json")→_redact_credential_secrets→to_json, plus the artifactDelta split) into a publicgoogle.adk.cli.ssemodule and makingrun_agent_sse()use it, so the route and any external copy cannot drift. Item 2 (optional SSE keep-alive) is deliberately left out: it adds a configurable timer to the route and is its own behaviour change, as this issue proposes splitting them.One thing I should flag rather than paper over: @Javviviii2 wrote above that they would be willing to send both PRs. I opened #7594 before weighing that. If they would still like to take item 1, say so and I will close mine — no hard feelings, theirs should win on the reporter's-own-work principle. If not, #7594 is ready:
check-changes,header-checkandcla/googlepass,tests/unittests/cliis 1497 passed / 5 skipped / 1 xfailed, and the only red check isagent-triage-pull-request, which is a repo-side Gemini 429.fix(cli): expose /run_sse event framing as public API