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
AuraAgentExecutor runs the agent inside the execution stream, keeps its own cancel map (TaskCancelState and its guard), passes its own token to RunOptions::cancelled_by, and folds StreamItems into task status by hand. BusBridgedExecutor then republishes every execution event over the session-store bus with its own sequence numbers so another instance can relay a subscribe. That is a second registry and a second journal, both A2A-private, and the task's state is a fold over stream items rather than a reading of the run — which is how #737 happens: a cancel racing the fold records Canceled over a task the fold already completed.
Move the executor onto the runtime. A task starts a run; task status is a projection of the run's lifecycle; subscribe is a subscription; and the run's status, not a fold, decides whether a cancel still applies.
Goals
An A2A task starts a run ([FEATURE]: A runtime that owns sessions and their runs #780) with the context id as its session. The task itself claims the session for the execution's life ([FEATURE]: Observers declare whether they collect, claim, or attend #781) — the epic's "bounded backend task" — so the run lives as long as the task does, whatever its clients do. That is today's behavior, not a change: a2a-server spawns drive_execution on a task of its own and hands each client a broadcast subscription, so an execution already outlives a client that disconnects. Subscribers, including the one that sent message/stream, collect.
tasks/{id}:subscribe attaches to the session as a collecting observer from the run's Started ([FEATURE]: A session's event journal and late attach #779), so a subscriber arriving mid-task sees the task from the start without the bus.
Proposed. The projection is a function over the subscription; the ids it needs are the task's.
pubstructA2aTaskIds{pubtask_id:String,pubcontext_id:String}/// Projects one run event onto the A2A execution stream. `None` for events A2A/// has no frame for.pubfnproject_a2a(event:&SessionEvent,ids:&A2aTaskIds) -> Option<StreamResponse>;// None for another run's events// Lifecycle(Started) -> StatusUpdate { state: Working }// Agent(TextDelta | Completed) -> ArtifactUpdate (one part per chunk, as today)// Agent(ApprovalPending) | Lifecycle(Parked) -> StatusUpdate { state: InputRequired } (the #209 wiring)// Lifecycle(Finished) -> StatusUpdate { state: Completed, final: true }// Lifecycle(Cancelled) -> StatusUpdate { state: Canceled, final: true }// Lifecycle(Failed) -> StatusUpdate { state: Failed, final: true }// Lifecycle(ObserverAttached | ...) -> None
Additional Context
Task-store writes stay where they are. The store is the durable record of a task; the runtime is the live record of a run; the executor keeps them in step, as it does today. The task record gains the run's RunId and SessionId (in its metadata, as the upstream Task type allows), so an instance serving tasks/{id}:subscribe for a task it did not start can find the session and the cursor without a fold over the execution stream.
A worker's tool calls within an orchestrated run are attributed to the run, not the worker, until #732 gives MCP a stream identity. The projection carries whatever AgentContext the event has; it does not repair it.
a2a-server's own ActiveExecution — a broadcast per task that its request handler subscribes clients to, with a "subscription fell behind active execution" error on lag — is upstream and stays. The executor's stream feeds it; the runtime's journal is what feeds the executor. The UnboundedSender<StreamResponse> the executor writes into is the one channel end this seam keeps, because the upstream executor interface is a stream; it is one hop at the seam, not a reach into the run. Because the task holds the claim for its whole life, ClaimsExhausted never fires for an A2A run before it is terminal, which is why #784 has no a2a liveness entry.
Summary
AuraAgentExecutorruns the agent inside the execution stream, keeps its own cancel map (TaskCancelStateand its guard), passes its own token toRunOptions::cancelled_by, and foldsStreamItems into task status by hand.BusBridgedExecutorthen republishes every execution event over the session-store bus with its own sequence numbers so another instance can relay a subscribe. That is a second registry and a second journal, both A2A-private, and the task's state is a fold over stream items rather than a reading of the run — which is how #737 happens: a cancel racing the fold recordsCanceledover a task the fold already completed.Move the executor onto the runtime. A task starts a run; task status is a projection of the run's lifecycle; subscribe is a subscription; and the run's status, not a fold, decides whether a cancel still applies.
Goals
a2a-serverspawnsdrive_executionon a task of its own and hands each client a broadcast subscription, so an execution already outlives a client that disconnects. Subscribers, including the one that sentmessage/stream, collect.tasks/{id}:subscribeattaches to the session as a collecting observer from the run'sStarted([FEATURE]: A session's event journal and late attach #779), so a subscriber arriving mid-task sees the task from the start without the bus.TaskCancelStateand its guard go.bus_bridgestays, fed from the runtime's subscription instead of the executor's stream, and keeps its contract from Cross-instance cancel leaves the executing instance's subscribers without a terminal frame #475. Generalizing it from A2A tasks to every run is [FEATURE]: Endpoints to list, inspect, attach to, and control sessions and runs #785's cross-instance goal.Data structures
Proposed. The projection is a function over the subscription; the ids it needs are the task's.
Additional Context
Task-store writes stay where they are. The store is the durable record of a task; the runtime is the live record of a run; the executor keeps them in step, as it does today. The task record gains the run's
RunIdandSessionId(in its metadata, as the upstreamTasktype allows), so an instance servingtasks/{id}:subscribefor a task it did not start can find the session and the cursor without a fold over the execution stream.A worker's tool calls within an orchestrated run are attributed to the run, not the worker, until #732 gives MCP a stream identity. The projection carries whatever
AgentContextthe event has; it does not repair it.a2a-server's ownActiveExecution— a broadcast per task that its request handler subscribes clients to, with a "subscription fell behind active execution" error on lag — is upstream and stays. The executor's stream feeds it; the runtime's journal is what feeds the executor. TheUnboundedSender<StreamResponse>the executor writes into is the one channel end this seam keeps, because the upstream executor interface is a stream; it is one hop at the seam, not a reach into the run. Because the task holds the claim for its whole life,ClaimsExhaustednever fires for an A2A run before it is terminal, which is why #784 has noa2aliveness entry.Searched Issues
Code of Conduct