a2aproject / a2aproject/A2A

Real-world evidence: undefined stream completion signal (see #1746) allows silent, undetectable truncation on a production A2A server

Open
#2,130 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Shell
Stars
25.7k
Forks
2.6k
Avg merge
3d 6h
Merged PRs (30d)
16

Description

## Context

This relates to #1746 (closed/consolidated), which flagged that the spec names
a "final completion indicator" for `SendStreamingMessage` outputs but never
defines what it concretely is -- the reporter's own reading was that a stream
just closes, with no explicit signal required beforehand.

We hit a concrete, real-world consequence of that ambiguity and wanted to
share it as evidence, since #1746 was consolidated without (as far as we can
tell) a resolution.

## What we found

Testing `message:stream` (via `a2a-sdk`'s own `RestTransport.send_message_streaming`)
against a real, deployed Google Vertex AI Agent Engine target running
`google-adk`'s `A2aAgent` template:

- A test agent instructed to produce a long (~800-word), genuinely
multi-chunk streamed response instead delivered only **211 words across 2
real artifact chunks**, then the stream simply stopped. The last status
event observed was `TASK_STATE_WORKING` -- it never reached
`TASK_STATE_COMPLETED`, and no error was raised on the client.
- From the client's perspective, this is **indistinguishable** from the agent
legitimately finishing early. Nothing in the protocol signals that
something went wrong.
- We confirmed the full response genuinely existed: deploying the *same*
agent via the classic `AdkApp` template (native `:streamQuery`, bypassing
A2A's `message:stream` entirely) delivered the complete, correct response
every time. So the loss happens specifically in the A2A streaming
relay/adapter path, not the model or the underlying serving
infrastructure.

We also separately confirmed this is not the `append=True for nonexistent
artifact_id` bug (a different, already-being-fixed issue --
`google/adk-python#6680`, patched by `chelsealong/adk-python#6686`). That fix
does **not** resolve this failure mode; we re-tested against it directly.

## Why we think this belongs here, not just as an implementation bug report

We *did* file the implementation-side report against `google-adk`
(`google/adk-python#6680`), since the actual data loss happens in a specific
vendor's server implementation. But the reason this class of bug is possible
at all -- and was so hard to detect, requiring an out-of-band comparison
against a completely different deployment template to even prove real
content was being dropped -- traces back to the exact gap #1746 identified:
**the spec does not require an explicit terminal signal on every stream
close.**

If the spec mandated that a stream MUST end with an explicit terminal
`TaskStatusUpdateEvent` (`COMPLETED`/`FAILED`/`CANCELED`/etc.) for every
reason it ends -- including abnormal closure -- then a stream that closes
without ever sending one would itself be a detectable protocol violation.
Clients could treat "stream closed with no terminal event" as an error
condition rather than silently trusting whatever partial content arrived.
Today, nothing in the protocol gives a client that signal.

## Ask

Would it make sense to resolve #1746 by making an explicit terminal
`TaskStatusUpdateEvent` **mandatory** on every stream close (not just the
happy path), so this class of silent truncation becomes a detectable,
spec-level violation instead of something only discoverable through manual,
out-of-band verification like we had to do here?

Happy to share our reproduction scripts if useful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.