Real-world evidence: undefined stream completion signal (see #1746) allows silent, undetectable truncation on a production A2A server
- 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
Assessment
This issue has not been assessed yet.