Skip to content

fix(server): release the ActiveTask after a direct Message response - #1297

Open
christophstach wants to merge 1 commit into
a2aproject:mainfrom
christophstach:fix/release-active-task-after-message
Open

christophstach wants to merge 1 commit into
a2aproject:mainfrom
christophstach:fix/release-active-task-after-message

Conversation

@christophstach

Copy link
Copy Markdown

Description

A direct Message is a complete response for interactions that do not need task tracking (spec §3.1.1: the agent "MAY return a direct Message response for simple interactions"; §3.1.2: a message-only stream provides "No task tracking or updates"). No follow-up can continue such a request.

DefaultRequestHandlerV2 still kept the request's ActiveTask alive after a Message response:

  • the producer stays parked on self._request_queue.get();
  • the consumer and both EventQueueSource._dispatch_loop tasks wait with it;
  • the entry stays in the ActiveTaskRegistry.

Every message-only request therefore leaves four pending asyncio tasks behind until the process exits. In a long-running server, the pending task count grows linearly with traffic.

Fix

After the consumer forwards the Message to subscribers, it finishes the ActiveTask the same way it already does for a terminal task state: it sets _is_finished and shuts down the request queue (_handle_terminal_state). The producer then leaves its loop and closes the event queues, the consumer drains and exits, and _maybe_cleanup removes the registry entry.

The Message is enqueued to subscribers before the shutdown, so on_message_send and on_message_send_stream still receive it.

Out of scope: input-required tasks stay parked for a follow-up, as covered by test_cancel_of_input_required_task_cannot_be_undone. This PR does not change that.

Tests

Two new tests in tests/server/request_handlers/test_default_request_handler_v2.py drive the real handler with an executor that answers with a single Message:

  • test_on_message_send_message_response_releases_active_task: 3 × on_message_send, then no pending asyncio tasks and an empty registry;
  • test_on_message_send_stream_message_response_releases_active_task: a message-only stream yields exactly one Message, then the same assertions.

Both fail on main (leftover ActiveTask._run_producer, ActiveTask._run_consumer and EventQueueSource._dispatch_loop) and pass with the fix.

Standalone repro, 50 requests each, counting pending asyncio tasks afterwards:

                main (ddce6b8)   this PR
completed             0              0
message             200              0
input-required      200            200   (unchanged, by design)

Checks

  • ./scripts/lint.sh: passed. The 3 ty warnings are pre-existing, in client_factory.py and client/transports/__init__.py.
  • uv run pytest: 2173 passed, 181 skipped, 3 xfailed, 1 xpassed.
  • uv run pytest --cov=src: total 93 %, active_task.py 95 %; the new lines are covered.

Fixes #1296

A direct Message is a complete response without task tracking (spec
3.1.1, 3.1.2), so no follow-up can continue it. DefaultRequestHandlerV2
still kept the request's ActiveTask alive: the producer stayed parked
on the request queue and the consumer and both dispatchers waited with
it, so every message-only request left four pending asyncio tasks and a
registry entry behind until the process exited.

After forwarding the Message to subscribers, the consumer now finishes
the ActiveTask the same way it does for a terminal task state, which
releases the producer, consumer, queues and registry entry.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/agent_execution/active_task.py 95.02% 95.11% 🟢 +0.08%
Total 92.95% 92.95% 🟢 +0.01%

Generated by coverage-comment.yml

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DefaultRequestHandlerV2 keeps the ActiveTask (producer, consumer, 2 dispatchers) alive forever after a direct Message or input-required response

1 participant