ADR 0162: Cross-session A2A addressing
- Status: Superseded by ADR 0165
- Date: 2026-09-05
- Deciders: PI-Desktop core
- Related: D318, D277, ADR 0147, ADR 0062, ADR 0089,
03-runtime/02-agent-runtime.md§5f.2,03-runtime/06-host-rpc-protocol.md§4, E2E-165, E2E-165b, E2E-165c - Amends: ADR 0147. The local A2A broker, capability-token auth, typed parts, durable task lifecycle, and parent-cannot-call-A2A boundary are unchanged. Same-context-only addressing is lifted.
Context
ADR 0147 bound every A2A agent to contextId = sessionId and refused cross-context discovery, send, and task access with A2A_CROSS_CONTEXT_DENIED. That matched the in-process mailbox it replaced, which was session-scoped by construction.
The broker itself is already process-global: one in-memory registry in host-core, one sidecar JSON-RPC pipe, and one DesktopAgentRuntime per session. Agents from different sessions already sit in the same map; isolation was a filter. Concurrent work in two open sessions therefore cannot share a fact, a file claim, or a roundtable seat even though both delegates are local to the same host.
The remaining risk is identity, not transport. Peer ids are unique per session (discussant, discussant-2) but not across sessions, so two chats can both run discussant. Tasks store agentName / requesterName as those ids, and the sidecar broadcasts a2a.task.event to every session runtime. A naive lift of the filter would let the wrong discussant read a task or wake on the other session's event.
Decision
Allow A2A discovery and addressing across sessions on the same host.
a2a.agents.listreturns every other live agent, not only the caller'scontextId. Each Agent Card carriescontextId(the session id it registered under) so a delegate can tell same-session peers from other-session peers. The caller's own card stays excluded.a2a.message.sendmay address any registered peer. Whentois omitted, the broker still prefers a same-session peer (preserving today's single-other-peer default) and only then a unique other-session peer.- Task access is membership, not context. The caller must be the task's
requesterNameoragentName. A stranger — same session or not — still fails withA2A_UNKNOWN_AGENT.A2A_CROSS_CONTEXT_DENIEDis kept in the error-code list for wire compatibility and is no longer produced.contextIdon the task remains the requester's session id and still capsA2A_MAX_TASKS_PER_CONTEXT. - Live peer ids are unique across the registry. On
a2a.agents.register, ifcard.nameis already taken the broker suffixes it (discussant-2, …) and returns the uniquifiedagentId. The runtime adopts that id for the delegate's A2A tool, wait queue, and prompt, soagentName/requesterNameon a task cannot collide with another live agent. - Events carry
recipientContextId.a2a.task.eventanda2a.pushare shaped{ recipient, recipientContextId, contextId, … }. Each session runtime delivers an event only whenrecipientContextIdequals itssessionId(an omitted field keeps today's same-session delivery). This stops a broadcast on the shared sidecar pipe from queueing work for the wrong session. - Unchanged boundaries (amended by ADR 0164). A settled delegate is deregistered. There is still no nested delegation and no remote or cross-machine transport. Parent-to-parent A2A is ADR 0164; parents still cannot address subagents.
Consequences
- Two A2A-capable delegates in different open sessions can discover each other, create a durable task, and complete the counterpart round-trip.
- Same-session A2A is unchanged for the common case: unique names, omitted
topicks the other local peer, events still wake the local waiter. - A definition named
discussantin a second session may register asdiscussant-2when the first session already holdsdiscussant. The delegate is told its assigned peer id. A2A_CROSS_CONTEXT_DENIEDremains a documented code but is unused. New cross-session failures areA2A_UNKNOWN_AGENT,A2A_UNKNOWN_TASK, orA2A_NO_PEERS.- Not addressed: nested delegation and any network A2A binding. Parent-to- parent A2A is decided in ADR 0164. Name reuse after deregister (a later
discussantreading a stale task that still namesdiscussant) is the same intra-session hazard as today.
Alternatives considered
- Keep same-session isolation: rejected. The user-visible request is inter-session coordination, and the broker is already global; the deny was a leftover mailbox boundary, not a transport limit.
- Address by
(name, contextId)without uniquifying names: rejected. Tasks persistagentName/requesterNameas strings; two livebobagents would both pass membership checks. Uniquifying at register keeps that contract. - Qualify stored names as
name@contextId: rejected. It would leak session ids into every task summary the model reads, and would still need event filtering on the shared pipe.