a2aproject / a2aproject/a2a-itk
Offer: wire-format conformance vectors alongside the traversal scenarios
- Dominant language
- Python
- Stars
- 7
- Forks
- 9
- Avg merge
- 4d 4h
- Merged PRs (30d)
- 20
Description
### What ITK covers today
The traversal model checks that a message routes across a cluster of agents built on
different SDKs and transports, and that the trace comes back intact. That catches things no
single-implementation suite can.
What it doesn't reach is whether two SDKs agree on the bytes. I don't think that's an
oversight, I think it falls out of the design, and it's visible in three places.
Every request carries one Part shape, a base64 protobuf blob:
https://github.com/a2aproject/a2a-itk/blob/84c822b785599418564ecfb839f8e18c792dcc20/testlib.py#L260-L266
Every response is read back as text only. [`_extract_response_text`](https://github.com/a2aproject/a2a-itk/blob/84c822b785599418564ecfb839f8e18c792dcc20/testlib.py#L278-L285)
collects `part['text']` and drops everything else. And a scenario passes when the expected
tokens appear anywhere in that concatenation:
https://github.com/a2aproject/a2a-itk/blob/84c822b785599418564ecfb839f8e18c792dcc20/testlib.py#L384
So the corpus is bytes in, text out, happy-path values, asserted by substring containment.
No traversal ever sends an unknown enum, a non-UTC timestamp, a `DataPart` whose value is
null, or two arms of a oneof. [`TaskStatus.timestamp`](https://github.com/a2aproject/a2a-itk/blob/84c822b785599418564ecfb839f8e18c792dcc20/testlib.py#L375)
is never asserted at all. Any divergence that leaves the ASCII trace tokens readable passes
green.
### Why it's worth closing
I built a differential harness that round-trips a shared corpus through `a2a-python` and
`a2a-js` and diffs the JSON. Both generate from the same `.proto` through different
codegen, so a disagreement is a divergence rather than a preference. 92 cases, 83 agreed,
and every disagreement was real:
| Divergence | `a2a-python` | `a2a-js` | Issue |
| --- | --- | --- | --- |
| unknown enum value | `{"state": 99}` | `{"state": "UNRECOGNIZED"}`, which Python then rejects | a2aproject/a2a-js#640 |
| `Timestamp` offset | `2026-01-01T00:00:00Z` | `2026-01-01T05:30:00+05:30` | a2aproject/a2a-js#641 |
| `Timestamp` validation | rejects `not-a-timestamp` | stores and forwards it | a2aproject/a2a-js#641 |
| `Part.data` is null | keeps the `data` arm | drops the content oneof | a2aproject/a2a-js#642 |
| two oneof arms | rejects | silently picks one | a2aproject/a2a-js#643 |
All of them reproduce on published `@a2a-js/sdk` 1.0.1, not just `main`. All of them pass
the current ITK scenarios, because none of those values ever enters a traversal.
The enum row is why I'm raising this now rather than later. `TaskState` defines 0 through 8,
so 9 is the next value the spec adds. On that day a 1.0 SDK sitting between two newer peers
turns a forward-compatible number into a string nothing can parse, and there is no test
anywhere today that catches it before release.
### What I'd add
A versioned corpus of protocol objects paired with their expected canonical JSON, plus a
small runner per SDK, reported the same way traversal results already are.
The corpus would be language-neutral data rather than Python, so it isn't encoding one
implementation's habits. It would cover the axes where independent codegen actually drifts:
enums including unknown values, `Timestamp` formatting and validation, oneof arm selection,
bytes and base64, `Struct` and `Value` including null, numeric formatting, and non-ASCII
text.
This sits beside the traversal scenarios rather than replacing them. Traversal answers "can
these agents talk", vectors answer "do they agree on what they said". The five agent
implementations already in [`agents/`](https://github.com/a2aproject/a2a-itk/tree/84c822b785599418564ecfb839f8e18c792dcc20/agents)
are the substrate, so the marginal cost per SDK is a runner rather than a new harness.
### Two questions before I write anything
**1. Here or beside?** Does wire-format conformance belong in ITK, or would you rather it
live in its own repo and be consumed as a pinned dependency? ITK already has the agent
fleet and the CI wiring, which argues for here, but it does widen what the kit is for.
**2. Expected bytes, or only agreement?** Agreement between implementations is cheaper and
needs nobody to adjudicate. It can't catch the case where two SDKs agree with each other and
both diverge from the spec, which is a real case: see a2aproject/A2A#2122, where neither
Python nor JS reproduces the §8.4.1 worked example and they fail it identically. Expected
bytes catch that, but someone has to decide what's authoritative.
Happy to start with a narrow slice, enums and `Timestamp` across the Python and TS agents,
so you can see the shape before committing to the full surface.
Contributor guide
Assessment
This issue has not been assessed yet.