a2aproject / a2aproject/a2a-python

cancel() and producer-failure write a terminal state to the store but not to active subscriber streams

Open
#1,175 5 comments 0 reactions 1 assignee Claimed by @rohityan View on GitHub
component: server status:awaiting response
Dominant language
Python
Stars
2.1k
Forks
496
Avg merge
4d 17h
Merged PRs (30d)
12

Description

Follow-up from the review of #1172 (raised by @astrogilda).

### What happens

`ActiveTask.cancel()` now closes a task out to a terminal `CANCELED` state when
the executor's `cancel()` is cleanup-only or the task was parked in a
non-terminal state (e.g. `input-required`). That terminal state is written
directly to the task store via `save_task_event`, because by that point both
the agent and subscriber event queues are already closed.

The consequence is that the terminal state reaches the **store** but not an
**active subscriber stream**. A client that was streaming the task
(`SubscribeToTask` / `message/stream`) receives the initial `Task` in `WORKING`
and then the stream ends; it never sees a `CANCELED` event and has to re-read
the task to learn the outcome. The push path does not fire either, since
`_update_task_state` only notifies on `PushNotificationEvent`.

### Why it was left out of #1172

At the point of the direct store write, both queues are already closed, so a
store write is the only option left. The same pattern already exists for the
producer-failure path (`active_task.py`, the `FAILED` write in
`_run_producer`'s `except Exception` arm), under a comment that says so. #1172
kept scope to the authorization fix (#1159) and the "cancel leaves an actionable
terminal state" fix (#1170); this stream-visibility gap is orthogonal.

### Proposed follow-up

Give a streaming or push-subscribed client a way to observe the terminal state
without polling. Options to weigh:

- Emit a synthetic terminal `TaskStatusUpdateEvent` to `_event_queue_subscribers`
before the queues are torn down, so an active subscriber sees the transition.
- Route the terminal store write through the push path so `push_sender` fires.

Both the `cancel()` CANCELED write and the producer-failure `FAILED` write share
this gap and should get the same treatment.

### Repro

Subscribe to a live task whose executor's `cancel()` is cleanup-only, cancel it,
and collect the events the subscriber receives: the stream yields the initial
`WORKING` task and ends, while `task_store.get` afterwards returns `CANCELED`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.