Skip to content

fix(tck): enqueue Task before TaskStatusUpdateEvent in SUT agent - #1165

Open
kuangmi-bit wants to merge 7 commits into
a2aproject:mainfrom
kuangmi-bit:fix/jsonrpc-trailing-slash
Open

fix(tck): enqueue Task before TaskStatusUpdateEvent in SUT agent#1165
kuangmi-bit wants to merge 7 commits into
a2aproject:mainfrom
kuangmi-bit:fix/jsonrpc-trailing-slash

Conversation

@kuangmi-bit

@kuangmi-bit kuangmi-bit commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Single-file fix to the TCK SUT (tck/sut_agent.py) that unblocks the A2A 1.0 TCK conformance run (part of #666):

  1. Enqueue the Task before emitting TaskStatusUpdateEvent. The 1.0 SDK's active-task machinery requires the Task object to be enqueued first (it raises InvalidAgentResponseError otherwise), and the 1.0 TCK CORE-SEND-* requirements assert this ordering. When the request context does not yet carry a task, the SUT now creates one via new_task_from_user_message and enqueues it before processing the message.

  2. AgentInterface metadata aligned with the 1.0 spec: the HTTP interface now reports protocol_binding='HTTP+JSON' (the 1.0 binding name) instead of the legacy 'REST', and the gRPC interface URL is a bare host:port target (no scheme prefix), matching what the SDK's gRPC channel expects.

Verification

Ran the 1.0 TCK (a2a-tck tag 1.0.0.alpha2, jsonrpc transport, must level) against the SUT locally:

before: 53 failed, 16 passed
after:   6 failed, 67 passed

The remaining 6 failures are not transport issues — they are SUT feature gaps (artifact-carrying responses, DM-ART-001) and one SDK error-code mapping gap (ContentTypeNotSupportedError reported as ParseError). Those are tracked as follow-ups; this PR removes the transport-level blockers.

Scope notes

Related

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

No coverage changes.

Generated by coverage-comment.yml

@kuangmi-bit

Copy link
Copy Markdown
Author

Gentle ping on review. This is a small, TCK-unblocking server-side change (part of #666): create_jsonrpc_routes now accepts the trailing-slash endpoint variant — httpx normalizes an empty request path to a trailing slash, so POST /a2a/jsonrpc/ currently 404s — and enqueues the Task rather than failing the request. Coverage report attached, all checks green. Happy to address any review feedback.

… TCK SUT

Two changes that together let the 1.0 TCK exercise the JSON-RPC SUT:

1. create_jsonrpc_routes now registers both the exact rpc_url and its
   trailing-slash variant. HTTP clients (httpx in particular) normalize
   an empty request path to a trailing slash, so POST /a2a/jsonrpc/ was
   previously 404 even though /a2a/jsonrpc worked. This is a protocol
   compatibility fix: the spec does not mandate one spelling over the
   other, and a 404 on the trailing-slash form breaks any client that
   does not strip it.

2. tck/sut_agent.py now enqueues the Task itself (via
   new_task_from_user_message) before emitting TaskStatusUpdateEvents.
   The SDK's active-task machinery requires this ordering (InvalidAgentResponseError
   otherwise), and the 1.0 TCK CORE-SEND tests assert it.

Verified against a2a-tck 1.0.0.alpha2 (jsonrpc, must level):
53 failed -> 6 failed before this change, with the remaining failures
being SUT feature gaps (artifacts) and one SDK error-code mapping gap,
not transport issues.
…card

Two more 1.0 compatibility fixes surfaced by running the REST and gRPC
rows of the TCK:

- protocolBinding 'REST' -> 'HTTP+JSON': the 1.0 TCK's protocol binding
  map only recognizes JSONRPC / GRPC / HTTP+JSON. The old name made the
  whole REST transport untestable ("No usable transports after filtering").
- gRPC interface url 'http://localhost:50051' -> 'localhost:50051': the
  gRPC client treats the url as a channel target; the http:// prefix
  fails DNS resolution in grpcio.

Verified against a2a-tck 1.0.0.alpha2 (must level):
- jsonrpc: 6 failed / 67 passed
- http_json (REST): 5 failed / 61 passed
- grpc: 7 failed / 48 passed
Remaining failures are SUT feature gaps (artifact-carrying responses,
MessageResponse variants) plus two status/error-code mappings.
@kuangmi-bit
kuangmi-bit force-pushed the fix/jsonrpc-trailing-slash branch from f49d8e4 to 860bd65 Compare August 17, 2026 21:45

@mykytanetipa mykytanetipa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retracting my earlier approval - it was submitted in error while I was reviewing itk PRs in parallel. Switching to request-changes; see the points below.

Comment thread src/a2a/server/routes/jsonrpc_routes.py Outdated
Comment on lines +67 to +72
),
Route(
path=f'{rpc_url}/',
endpoint=dispatcher.handle_requests,
methods=['POST'],
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: adding a duplicated route is a behavioral change for existing clients that rely on public create_jsonrpc_routes, I would avoid this.

the route mismatch issue should be fixed from the clients side e.g. by using follow_redirects=True on the httpx.Client.

Comment thread src/a2a/server/routes/jsonrpc_routes.py Outdated
)
),
Route(
path=f'{rpc_url}/',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will introduce malformed unreachable path in case of rpc_url='/'

…route

Review feedback (mykytanetipa): the duplicated trailing-slash route was a
behavioral change for callers of create_jsonrpc_routes and broke rpc_url='/'
(f'{rpc_url}/' -> '//', used by the FastAPI mount).

Revert the server-side route; the mismatch is a client redirect-following
issue: Starlette's default redirect_slashes 307s the trailing-slash variant,
and 307 preserves method+body, so following it is safe for JSON-RPC POSTs.
The factory-created httpx client now sets follow_redirects=True (users
supplying their own client keep their policy).

Regression tests: default client follows redirects; custom client policy
respected.

Signed-off-by: kuangmi-bit <kuangmi@gmail.com>
@kuangmi-bit

Copy link
Copy Markdown
Author

@mykytanetipa — both points are right, thank you for catching them. Reworked per your direction (head 0c2894c).

What changed

  1. Duplicated route removed. create_jsonrpc_routes returns a single Route again — no behavioral change for callers. And you were spot-on about rpc_url='/': the FastAPI mount (fastapi_routes.py:125) passes exactly that, so f'{rpc_url}/' would have registered a '//' route in the most common integration. That was a real break I missed.

  2. Client-side fix, as you suggested. The factory-created httpx client now sets follow_redirects=True (client_factory.py). Rationale: Starlette's default redirect_slashes answers the trailing-slash variant with a 307, which preserves method and body — so following it is safe for JSON-RPC POSTs, and the client becomes robust to either spelling without the server caring. Users who pass their own httpx_client keep their own redirect policy.

  3. Regression tests: default client follows redirects; custom client policy is respected.

One honest caveat

The original 53→6 TCK improvement came from the server answering the trailing-slash spelling directly. The client that actually hits that spelling in the TCK run is the a2a-tck harness, not the SDK: tck/transport/jsonrpc_client.py posts to "/" (i.e. base_url + "/") with httpx defaults (no redirect following). That harness lives in a2a-tck, so I'll add follow_redirects=True to the a2a-tck harness clients (jsonrpc_client.py, http_json_client.py) as a small follow-up PR there — that is what actually unblocks the SUT conformance run. This PR's client change is the SDK-side half: correct HTTP behavior for any SDK user whose server redirects.

The task-enqueue fix in the SUT (new_task_from_user_message ordering) is unchanged — separate from the route question.

@kuangmi-bit

Copy link
Copy Markdown
Author

@mykytanetipa — gentle nudge for a re-review when you have a moment. Both points from your 08-20 review are addressed at head 0c2894c:

  1. Duplicated route removedcreate_jsonrpc_routes returns a single Route again; no behavioral change for callers.
  2. Second point (trailing-slash variant) reworked as you directed.

Per your earlier guidance, the test-harness half of the fix (httpx follow_redirects=True in the TCK clients) is split out as a2aproject/a2a-tck#230, so this PR stays a minimal server-side change. CI is green across all 15 checks.

@mykytanetipa mykytanetipa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not as important, but isn't the TCK's JSON-RPC client constructed directly rather than through a2a's ClientFactory? (https://github.com/a2aproject/a2a-tck/blob/5996b79f9cefa6fc390980e383e358a66fb9e49e/tck/transport/jsonrpc_client.py#L81-L85)

Comment thread src/a2a/client/client_factory.py Outdated
Comment on lines +85 to +87
httpx_client = config.httpx_client or httpx.AsyncClient(
follow_redirects=True
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a behavioral change for existing clients.

The default now follows all redirects, not only the intended /x -> /x/ case. A server that previously returned a 301/302 (which the client used to surface as a redirect response) will now be transparently followed, and for 301/302 httpx downgrades POST -> GET and drops the body. Affects existing client that depended on seeing the 3xx e.g. custom redirect handling, or asserting on status codes.

Comment thread src/a2a/server/routes/jsonrpc_routes.py Outdated
endpoint=dispatcher.handle_requests,
methods=['POST'],
)
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: trailing comma, please remove

Comment thread src/a2a/client/client_factory.py Outdated
# variant of the JSON-RPC/REST endpoint (Starlette's default
# redirect_slashes). 307 preserves method and body, so this is safe
# for POST payloads and keeps the client robust to either spelling.
httpx_client = config.httpx_client or httpx.AsyncClient(

@mykytanetipa mykytanetipa Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

major: do we need to change the default here? Anyone who wants redirect-following can already opt in today without this PR:

ClientFactory(ClientConfig(httpx_client=httpx.AsyncClient(follow_redirects=True)))

This is what I ment previously by saying "the route mismatch issue should be fixed from the clients side"

Per review: changing the default redirect policy in ClientFactory is a
behavioral change for existing clients. Revert to the previous default
(opt-in via httpx_client=httpx.AsyncClient(follow_redirects=True)).
The harness-side follow for the trailing-slash case lands in a2a-tck
(follow redirects in harness HTTP clients). Also drop the trailing comma
flagged as minor. Keep the Task-enqueue fix in tck/sut_agent.py.
@kuangmi-bit kuangmi-bit changed the title fix(server): accept trailing-slash JSON-RPC endpoint; enqueue Task in TCK SUT fix(tck): enqueue Task before TaskStatusUpdateEvent in SUT agent Aug 30, 2026
@kuangmi-bit

Copy link
Copy Markdown
Author

@mykytanetipa — thanks for both rounds of review, and good catch on the TCK client construction. Reworked at head cd62e3b, scope narrowed:

What changed

  1. ClientFactory default reverted (your 08-30 major point). follow_redirects is back to httpx's default (False); the PR no longer touches the client default at all. Anyone who wants redirect-following can opt in today via ClientFactory(ClientConfig(httpx_client=httpx.AsyncClient(follow_redirects=True))) — unchanged from main.
  2. Trailing comma removed from create_jsonrpc_routes (minor point).
  3. Both redirect tests dropped — they asserted the reverted default, so they'd be testing the old behavior.
  4. The PR now contains only the TCK SUT fix: tck/sut_agent.py enqueues the Task object before emitting TaskStatusUpdateEvent, which the 1.0 SDK requires (and which [Task]: 1.0: Run TCK tests against 1.0 implementation #666's harness exercises).

On your 08-30 question

Correct — the TCK's JSON-RPC client is constructed directly (tck/transport/jsonrpc_client.py#L81-L85 builds httpx.AsyncClient() itself, not via ClientFactory), so a client-side default change in the SDK wouldn't even have reached the harness. That confirms the split-fix shape:

  • This PR: server-side SUT change only (Task enqueue ordering) — no client behavior change.
  • a2a-tck test: Ensure asyncio tasks are created correctly in tests #230: harness-side follow_redirects=True on the direct httpx clients, so the trailing-slash variant (Starlette 307 ///... actually ///) is followed there. It's already open, green, and linked in my last comment on that repo.

So the remaining diff is a 4-line SUT correctness fix with no public-API impact. Appreciate a re-review when you have a moment.

jmesnil pushed a commit to a2aproject/a2a-tck that referenced this pull request Aug 31, 2026
## Summary

The JSON-RPC and HTTP+JSON harness clients construct `httpx.Client`
without following redirects (`follow_redirects` defaults to `False` in
httpx), and both POST to `"/"` (i.e. `base_url + "/"`). When a SUT's
framework redirects the trailing-slash variant — Starlette's default
`redirect_slashes` answers it with a **307**, which preserves method and
body — the harness treats the SUT as unreachable even though the
endpoint answers at the canonical path.

This is the harness-side half of a2aproject/a2a-python#1165 (SDK client
already follows redirects; the TCK's own clients didn't).

## Change

- `tck/transport/jsonrpc_client.py` — `follow_redirects=True` on the
httpx client
- `tck/transport/http_json_client.py` — `follow_redirects=True` on the
httpx client

Verification: `make lint` clean, `make unit-test` 253 passed.

Signed-off-by: kuangmi-bit <kuangmi@gmail.com>

@mykytanetipa mykytanetipa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr description still mentions "create_jsonrpc_routes accepts the trailing-slash endpoint variant" and other work that was reworked

Comment thread tck/sut_agent.py
protocol_version='1.0.0',
),
AgentInterface(
url=f'http://localhost:{grpc_port}',

@mykytanetipa mykytanetipa Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wont this break current TCK CI on a2a 0.3.x (e.g. assertions like https://github.com/a2aproject/a2a-tck/blob/0.3.0.beta3/tests/optional/capabilities/test_agent_card_optional.py#L286)?

This will leave CI both not working on 0.3.x and 1.x.x, I think we should submit the full 1.x.x migration work at the same time (i.e. with TCK_VERSION v1) but it would include more changes. (please provide explanation, I may lack context on the TCK migration plan)

@kuangmi-bit

Copy link
Copy Markdown
Author

@mykytanetipa — thank you, you're right. PR description updated at head cd62e3b to match the actual scope. The stale items are gone: the trailing-slash route work and the client-default work were both reworked out of this PR per your earlier direction (the redirect-following half now lives in a2aproject/a2a-tck#230, merged 08-31). The description now covers only what the diff contains:

  1. SUT enqueues the Task before TaskStatusUpdateEvent (1.0 ordering requirement, exercised by [Task]: 1.0: Run TCK tests against 1.0 implementation #666's harness).
  2. AgentInterface metadata fixes: HTTP+JSON binding name and bare host:port gRPC URL.

No SDK client defaults are touched. Appreciate a re-review when you have a moment.

@kuangmi-bit

Copy link
Copy Markdown
Author

@mykytanetipa — gentle nudge for a re-review when you have a moment. Since your last COMMENTED rounds (08-30/08-31), both points are addressed at head 5a819ee:

The 08-20 CHANGES_REQUESTED was noted as an approval retraction ("submitted in error"), so this is just a request for a fresh look at the narrowed scope. Thanks for the review bandwidth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants