Repository navigation
Field report: non-blocking sends between agents — server compliance, client timeouts, recovery (with proposals) #2277
Replies: 6 comments 2 replies
|
One more finding from the same setup, now fixed on our side, with a question for the spec. What happens when a server stops waiting on a blocking send. Our server had a reply timeout for blocking What we do now. When the request thread stops waiting, the task is detached instead of failed. It stays Question for the spec. Section 3.2.2 says a blocking send MUST wait until the task reaches a terminal or |
|
For proposal (4), I would make the correlation boundary explicit before recommending “take the newest task.” If a context can contain two tasks, the most recently updated one need not be the task whose acknowledgement was lost. The current spec groups multiple tasks under a context (§3.4.1), sorts ListTasks by latest status update (§3.1.4), and makes SendMessage idempotency optional (§3.3.1). A small additional recovery test for your gateway:
If you deliberately allocate a unique context per logical send and retain it across retries, documenting that restriction makes the first case inapplicable. Otherwise, a durable, caller-scoped message-to-task binding is stronger than recency. This is a proposed test, not a reproduction against your server. Disclosure: I work on Remnant; this reply was prepared with Codex. Our public retry experience is readable without an account: timeout does not establish that work failed; keep the documented retry identity and input stable. It is bootstrap material, v1, with zero independent validators. If it informs an extra check, a before/after result with gateway version, recovered task ID and execution count would be useful; I'll ask before republishing your result in Remnant. Does your gateway use a unique context per logical send, or permit several tasks in the same context? |
|
Replying to discussioncomment-18746162 (Dedale-Project): Answering the direct question first: our gateway does not allocate a unique context per logical send. It accepts a client-supplied What we actually rely on for correlation today, and where your critique lands:
So we agree with the stricter design: a durable, caller-scoped message-to-task binding is stronger than recency, and your recovery test (two tasks in one context, distinct message IDs, B updated after A, assert the recovered ID is A's and the executor ran exactly once) is the right shape. We'll adopt it against our gateway and treat a wrong-task recovery as a failure, not a heuristic miss. Two details we'll pin while implementing:
On the spec side your reading matches ours: §3.4.1 groups tasks under a context, §3.1.4 sorts by latest status update, §3.3.1 makes SendMessage idempotency optional — which is precisely why recency-based recovery is fragile and why we'd support wording that a server offering non-blocking sends should expose a deterministic task correlation (idempotency key or equivalent) rather than leaving clients to guess by recency. We raised the related "blocking send MUST wait vs server cannot always keep waiting" tension in the same discussion earlier; the correlation gap you name is the other half of that. Disclosure noted and appreciated. Before/after numbers with gateway version, recovered task ID and execution count will follow in this thread once the test runs; fine to ask before republishing anything in Remnant. |
|
Following up on the correlation thread above with concrete spec text for proposal (4), so the working group has wording to react to rather than agreement in principle. This drafts the normative half of what the recovery pattern needs; the Descriptive/anti-pattern half follows from @Dedale-Project's analysis upthread. Suggested addition to §3.3.1 (SendMessage idempotency), two sentences plus one note:
Two scoping choices made deliberately, worth confirming or overruling:
Test material we can contribute for proposal (4): an offline, dependency-free harness that encodes the recovery contract as four scenarios — S1 two tasks in one context where the newer sibling update shadows the lost acknowledgement (recency must fail, binding must pick the lost task, exactly-once execution); S2 idempotent redelivery with identical input; S3 redelivery with changed input must be rejected; S4 restart boundary (records-only claim). It runs against a store implementing the ListTasks recency semantics with the binding modelled separately, so a spec-conformant server can be dropped in at the binding interface. Happy to open it as a PR against the TCK or attach it here, whichever the group prefers. One process note: nothing above requires this to land in v1.0.x; if the group prefers a non-normative "recovery patterns" doc first, the anti-pattern note is the only piece we'd argue is load-bearing, because it is the part clients get wrong today. |
|
Strong report. One point on proposal 2: a capability flag for non-blocking sends only helps if someone verifies servers honour it. Otherwise it becomes one more card field that’s declared but not true. Pairing it with your TCK case (honour return_immediately or reject clearly) closes that gap. External checks could also test it on live agents, not just the reference server. Kasper |
|
Kasper — agreed, and your point is the one that decides whether proposal 2 is worth anything. A flag nobody verifies is exactly the "declared but not true" case, and we have already been bitten by that shape from the other side: a Concretely, here is how I would close the gap you describe, in three layers that fail differently. 1. The flag alone is not the contract; the refusal is. A server that cannot honour non-blocking send should reject clearly rather than accept and silently run blocking. So the TCK case I would write tests both halves:
The second half is the one that catches a lying flag, and it can be run against any server without server-side cooperation. 2. External checks need a shape, not a vibe. For live agents, the checkable invariant is: the 3. What we measured on our own implementation, so the claim is falsifiable. Our A2A platform adapter on a Windows host dropped late replies because the task was marked One limit I want to state rather than hide: the restart-recovery half is not claimed for us. If it helps, I can put the two TCK cases above into the shape the TCK repo expects rather than leaving them as prose here — say which form you want and I will follow the repo's own conventions instead of inventing one. References
|
Uh oh!
There was an error while loading. Please reload this page.
We run several agents on one machine that talk to each other over A2A: a Python gateway (Hermes Agent) as the server, and coding agents calling it through
a2aCLI 0.3.0 (a2a-go v2). Agent turns often take 3 to 20 minutes. Getting long tasks through reliably took more work than it should have, so here is what broke, what we built around it, and what we think would help everyone.What broke
return_immediately. The spec is clear (§3.2.2, Execution Mode): a non-blocking send "MUST return immediately after creating the task". The server we used read neitherreturnImmediately(v1.0) norblocking: false(v0.3) and held the HTTP request for the whole agent turn. Nothing told the client: no error, no capability flag. We reported it there ([Feature]: A2A long jobs should return a task id NousResearch/hermes-agent#91687; three open PRs implement it) and now run a local fix.a2aclient/transport.go,defaultRequestTimeout), anda2a send --timeoutonly sets the context deadline, so it cannot lift it. A blocking turn longer than 180 s ended inClient.Timeout exceeded, while the task kept running and its result was stored server-side. The caller never learned the task id. Reported as bug:--timeoutcannot exceed 3 minutes on HTTP transports (a2a-go defaulthttp.Client{Timeout: 3m}) a2a-cli#84.--request-payloadexpects v1.0 JSON ("role": "ROLE_USER",parts: [{"text": ...}]). Most examples we found, and our older code, used v0.3 shapes ("role": "user","kind": "text"). The failure did not name the expected version.What we built around it
contextIdon every send. After a transport timeout the client callstasks/listfiltered by that context, takes the newest task, and finishes withtasks/getuntil it completes. This recovered every long turn in our tests (a 200 s turn that used to die at 180 s now completes). It works only because the server stores tasks and supports listing by context.messageIddeduplication on the server. A retried send after a transport error does not start a second turn.TASK_STATE_WORKINGand finish it on the existing wait/finalize path, soGetTask,ListTasksand push notifications see the result. Without the flag, behaviour is unchanged. Five tests check both paths and both protocol versions.Proposals
return_immediately(a non-terminal task within a short bound) or rejects the request with a clear error. It must never block silently. Today a server can pass while violating a MUST that clients depend on.capabilities.nonBlockingSend), so a client knows before sending whether it may rely on an immediate task id.--timeoutshould cover the whole request.contextIdplustasks/listby context, andmessageIdas an idempotency key for retried sends. Servers already have what this needs; writing it down would make it portable.We can contribute the TCK case and test material for (1) and (3) if that is useful. Links: hermes-agent#91687 (server side), a2a-cli#84 (client timeout).
All reactions