fix(a2a): keep late replies and refuse a shared port - #130744
deadczarvc wants to merge 2 commits into
Conversation
When the request thread stops waiting (A2A_REPLY_TIMEOUT on a blocking send, or a stream client that disconnected), the task used to be marked FAILED and the reply that arrived later was dropped: GetTask kept returning FAILED. The task is now detached instead: it stays WORKING, the caller gets that task, and a daemon thread records the reply when it arrives (bounded by the orphan ceiling, after which it fails as before). On Windows, stdlib SO_REUSEADDR let a second gateway profile bind a listening A2A port, and requests split between the two. The server now binds with SO_EXCLUSIVEADDRUSE there; elsewhere the stdlib behaviour is kept.
marcusrfaust
left a comment
There was a problem hiding this comment.
Review (marcusrfaust): LGTM. Late-reply detach keeps task WORKING instead of failing on timeout/disconnect, bounded by 24h orphan ceiling. Exclusive-port fix Windows-only, safe elsewhere. Tests updated to assert late reply lands.
|
A2A lifecycle review: detaching instead of terminal-failing at the request timeout preserves ownership of the in-flight agent work. The task remains |
What does this PR do?
Two bugs in the A2A platform adapter that we hit on a Windows host with several agents talking over A2A.
1. A reply that comes after the server stops waiting is lost. When a blocking
message/sendruns longer thanA2A_REPLY_TIMEOUT, or amessage/streamclient disconnects, the task is markedTASK_STATE_FAILED. The agentkeeps working, the gateway logs the finished response, but the task is already terminal, so
GetTaskkeepsreturning FAILED and the reply is gone. The docs said the opposite ("a late reply is stored, not discarded").
With this change the task is detached instead of failed: it stays
TASK_STATE_WORKING, the caller gets that taskback, and a daemon thread records the reply when it arrives, so
GetTask,ListTasksand push notifications see it.A task that never gets a reply still fails, at the existing orphan ceiling (24 h).
2. Two profiles can share one A2A port on Windows. The server is a stdlib
ThreadingHTTPServer, which setsSO_REUSEADDR. On Windows that lets a second socket bind a port that is already listening; with multiplex profileswe saw two gateways on 9902 and requests split between them. The server now binds with
SO_EXCLUSIVEADDRUSEonWindows, so the second bind fails loudly (the existing
bind_failedpath). Elsewhere the stdlib behaviour is kept:there
SO_REUSEADDRonly lets a restart reuse a port in TIME_WAIT. A restart right after closing a server withTIME_WAIT connections still binds on Windows (checked).
Related Issue
Related to #91687 (long A2A jobs). This PR does not add non-blocking sends (#91688, #94880, #103453 cover that);
it only stops losing the work when the server gives up waiting. The question this raises for the spec (a blocking
send MUST wait for a terminal state, a server cannot always keep waiting) is in
a2aproject/A2A#2277.
Type of Change
Changes Made
plugins/platforms/a2a/adapter.py:_detach()/_finish()replace "fail on timeout or disconnect" inmessage/sendandmessage/stream;_ExclusiveHTTPServerfor the listener.tests/plugins/test_a2a_plugin.py: the timeout test now expects WORKING and then the late reply; a ceiling test;a test that a second server on the same port fails to bind.
tests/plugins/test_a2a_phase23.py: the stream-disconnect test now expects the task to stay WORKING, then completewith the late reply and release the active request.
website/docs/user-guide/messaging/a2a.md,plugins/platforms/a2a/README.md: what happens pastA2A_REPLY_TIMEOUT.Behaviour change to note: a blocking caller whose request outlives
A2A_REPLY_TIMEOUTnow gets a WORKING taskinstead of a FAILED one, and has to follow it with
GetTask.How to Test
pytest tests/plugins/test_a2a_plugin.py tests/plugins/test_a2a_phase23.py tests/plugins/test_a2a_tools_gate.py plugins/platforms/a2a -m ""(the round-trip tests are marked
integration, so plainpytestskips them): 160 passed, 1 skipped.to start its A2A platform.
Checklist
Code
pytest tests/ -qand all tests pass — I ran the A2A suites above (all pass); not the whole treeDocumentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A