You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm building souk (AgentSouk), an open relay where agents running anywhere — laptops, private networks — serve standard AG-UI and A2A callers without holding a public endpoint. Three parties are in the game:
Caller — the side asking for work: a human on AG-UI, or a delegating agent on A2A. Human-backed at its root.
Provider — the side doing the work: the operator who registered the agent and holds its key. Also human-backed.
Relay (souk) — registers, verifies, relays — and, in doing so, is the party that records the delegation topology: which thread spawned which, and a hop-signed actor chain where each hop binds to the previous hop's hash, verifiable against registered keys. Deliberately mechanism, never policy: it holds the record straight but refuses to adjudicate anything on either side's behalf.
That refusal is why I'm posting instead of just picking a behavior: when a delegated sub-agent goes input-required, someone has to decide whose human may answer, and what proves they were entitled to — and a neutral relay can't be that someone. The spec defines the state but not the authority; #1016 and google/adk-python#3276 suggest we're not alone in hitting this.
The observation that unlocked it for us
Both ends of every conversation have a human — but the two are not symmetric in protocol terms, and the asymmetry is right:
input-required is addressed: "the party that called me must supply something."
A provider waiting on its own human is invisible by design — it just stays working, behind normal opacity. No new state needed. (What is missing there is latency semantics — "alive, waiting on my operator, expect hours" — a separate gap.)
Single-hop is therefore clean. Multi-hop is where it breaks:
user ──AG-UI──▶ main ──A2A──▶ sub
⏸ input-required
"the party that called me" = main,
which cannot present anything to anyone
(maintainers' position, google/adk-python#3276)
Which human in the tree does the request escalate to — and what proves they were the right one?
Bubble up, or walk down?
The framework-conventional answer is bubbling: each hop proxies the request upward until something with a UI is reached. The design we've settled on — written up, not yet implemented, which is exactly why we're posting it while it's still cheap to change — is the inverse: walking down. The relay already holds the two facts that make it possible (these two do run today): lineage (the tree is queryable, so the root's human can find the stuck leaf) and the actor chain (hop-signed, so "who is the head of the chain this work is ultimately for" is a verifiable fact, not a convention). On top of them, the plan: an interrupt becomes resolvable by exactly two keys — the head of its chain, or the provider's own key (the stall keeper resolving their own agent).
The part we'd like poked at: responsibility, declared per edge
Who counts as "the head" is not global. Before a provider wires its agent to a sub-agent, only it knows whether it could carry on if the sub gets stuck. So — in the plan — the delegating edge declares:
Break (the default): my sub, my problem. The provider absorbs — its human resolves the subtree's interrupts, it funds the subtree, the subtree is invisible upstream, and the subtree's failures are the provider's failures. Suppliers are implementation, behind normal A2A opacity.
Extend: if this stalls, it goes back up. The escalation path stays connected — the upstream principal sees this branch, may walk down into it, and funds it.
One tree, both kinds of edge (═══ extend, ──x── break):
user ●═══AG-UI═══▶ main ─────A2A──x──▶ sub₂ ═══▶ sub₃ ⏸
│ ║ ○ main is this segment's head:
│ ║ its human resolves sub₂/sub₃'s
│ ║ interrupts, its wallet funds them,
│ ║ user never sees this branch
│ ╚═══A2A, extend═══▶ sub₁ ⏸
└─ user's key may walk down and ────▶ resolvable by: user's key (●)
resolve sub₁ directly or sub₁'s own stall key
Intervention rights, cost, and visibility travel together deliberately: split them and agents pick exploitable combinations ("the user pays but may not look"). One rule fell out that we now treat as absolute: identifiers are never credentials — thread/task ids can't rotate, anything acting as a key must, so resolution rights are signatures and ids are just names. Full mechanics: docs/responsibility-chains.md.
Questions
Should "who may resume" be expressible in A2A at all (as an extension?), or is authority deliberately out of scope for the spec?
Has anyone needed server→client semantics for "this interrupt was resolved elsewhere / withdrawn" — a provider un-asking?
The break/extend bundle — try to split it. If rights, cost, and visibility can safely decouple, we'd rather learn that now, before any of this hardens into code.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
I'm building souk (AgentSouk), an open relay where agents running anywhere — laptops, private networks — serve standard AG-UI and A2A callers without holding a public endpoint. Three parties are in the game:
That refusal is why I'm posting instead of just picking a behavior: when a delegated sub-agent goes
input-required, someone has to decide whose human may answer, and what proves they were entitled to — and a neutral relay can't be that someone. The spec defines the state but not the authority; #1016 and google/adk-python#3276 suggest we're not alone in hitting this.The observation that unlocked it for us
Both ends of every conversation have a human — but the two are not symmetric in protocol terms, and the asymmetry is right:
input-requiredis addressed: "the party that called me must supply something."working, behind normal opacity. No new state needed. (What is missing there is latency semantics — "alive, waiting on my operator, expect hours" — a separate gap.)Single-hop is therefore clean. Multi-hop is where it breaks:
Which human in the tree does the request escalate to — and what proves they were the right one?
Bubble up, or walk down?
The framework-conventional answer is bubbling: each hop proxies the request upward until something with a UI is reached. The design we've settled on — written up, not yet implemented, which is exactly why we're posting it while it's still cheap to change — is the inverse: walking down. The relay already holds the two facts that make it possible (these two do run today): lineage (the tree is queryable, so the root's human can find the stuck leaf) and the actor chain (hop-signed, so "who is the head of the chain this work is ultimately for" is a verifiable fact, not a convention). On top of them, the plan: an interrupt becomes resolvable by exactly two keys — the head of its chain, or the provider's own key (the stall keeper resolving their own agent).
The part we'd like poked at: responsibility, declared per edge
Who counts as "the head" is not global. Before a provider wires its agent to a sub-agent, only it knows whether it could carry on if the sub gets stuck. So — in the plan — the delegating edge declares:
One tree, both kinds of edge (
═══extend,──x──break):Intervention rights, cost, and visibility travel together deliberately: split them and agents pick exploitable combinations ("the user pays but may not look"). One rule fell out that we now treat as absolute: identifiers are never credentials — thread/task ids can't rotate, anything acting as a key must, so resolution rights are signatures and ids are just names. Full mechanics: docs/responsibility-chains.md.
Questions
All reactions