Skip to content

Commit f7e314c

Browse files
fix(server): release the ActiveTask after a direct Message response
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.
1 parent ddce6b8 commit f7e314c

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

‎src/a2a/server/agent_execution/active_task.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,21 @@ async def _process_event_inner(self, event: Event) -> None:
237237

238238
await self._enqueue_to_subscribers(event, updated_task)
239239

240+
if isinstance(event, Message):
241+
self._handle_message_completion()
242+
243+
def _handle_message_completion(self) -> None:
244+
# A direct Message is a complete response without task tracking: no
245+
# task exists that a follow-up could continue. Finish like a terminal
246+
# state so the producer, consumer and queues are released instead of
247+
# waiting for another request forever.
248+
logger.debug(
249+
'Consumer[%s]: Message response, finishing',
250+
self.active_task._task_id,
251+
)
252+
self.active_task._is_finished.set()
253+
self.active_task._request_queue.shutdown(immediate=True)
254+
240255
def _handle_message_event(self, event: Message) -> None:
241256
if self.task_mode is True:
242257
raise InvalidAgentResponseError(

‎tests/server/request_handlers/test_default_request_handler_v2.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,3 +2331,83 @@ async def test_cancel_of_input_required_task_cannot_be_undone():
23312331
assert stored.status.state == TaskState.TASK_STATE_CANCELED
23322332
assert agent.execute_calls == 1
23332333
await handler.aclose()
2334+
2335+
2336+
class _MessageOnlyAgent(AgentExecutor):
2337+
"""Answers every request with a single direct Message (no task)."""
2338+
2339+
async def execute(self, context: RequestContext, event_queue: EventQueue):
2340+
await event_queue.enqueue_event(new_text_message('hello'))
2341+
2342+
async def cancel(self, context: RequestContext, event_queue: EventQueue):
2343+
pass
2344+
2345+
2346+
async def _wait_until_released(
2347+
handler: DefaultRequestHandlerV2, baseline: set[asyncio.Task]
2348+
) -> set[asyncio.Task]:
2349+
"""Waits for the handler to drop its ActiveTasks; returns tasks still pending."""
2350+
leftover: set[asyncio.Task] = set()
2351+
for _ in range(100):
2352+
leftover = asyncio.all_tasks() - baseline - {asyncio.current_task()}
2353+
if not handler._active_task_registry._active_tasks and not leftover:
2354+
break
2355+
await asyncio.sleep(0.01)
2356+
return leftover
2357+
2358+
2359+
@pytest.mark.asyncio
2360+
async def test_on_message_send_message_response_releases_active_task():
2361+
"""A direct Message ends the interaction, so its ActiveTask (producer,
2362+
consumer, dispatchers) must not stay alive waiting for a follow-up."""
2363+
handler = DefaultRequestHandlerV2(
2364+
agent_executor=_MessageOnlyAgent(),
2365+
task_store=InMemoryTaskStore(),
2366+
agent_card=create_default_agent_card(),
2367+
)
2368+
baseline = asyncio.all_tasks()
2369+
2370+
for i in range(3):
2371+
result = await handler.on_message_send(
2372+
SendMessageRequest(
2373+
message=Message(
2374+
role=Role.ROLE_USER,
2375+
message_id=f'm{i}',
2376+
parts=[Part(text='hi')],
2377+
)
2378+
),
2379+
ServerCallContext(),
2380+
)
2381+
assert isinstance(result, Message)
2382+
2383+
assert await _wait_until_released(handler, baseline) == set()
2384+
assert handler._active_task_registry._active_tasks == {}
2385+
2386+
2387+
@pytest.mark.asyncio
2388+
async def test_on_message_send_stream_message_response_releases_active_task():
2389+
"""Same for a message-only stream: one Message, then nothing left running."""
2390+
handler = DefaultRequestHandlerV2(
2391+
agent_executor=_MessageOnlyAgent(),
2392+
task_store=InMemoryTaskStore(),
2393+
agent_card=create_default_agent_card(),
2394+
)
2395+
baseline = asyncio.all_tasks()
2396+
2397+
events = [
2398+
event
2399+
async for event in handler.on_message_send_stream(
2400+
SendMessageRequest(
2401+
message=Message(
2402+
role=Role.ROLE_USER,
2403+
message_id='m1',
2404+
parts=[Part(text='hi')],
2405+
)
2406+
),
2407+
ServerCallContext(),
2408+
)
2409+
]
2410+
2411+
assert [type(event) for event in events] == [Message]
2412+
assert await _wait_until_released(handler, baseline) == set()
2413+
assert handler._active_task_registry._active_tasks == {}

0 commit comments

Comments
 (0)