diff --git a/workspace-template/a2a_executor.py b/workspace-template/a2a_executor.py index 59a3a79c..ebe40087 100644 --- a/workspace-template/a2a_executor.py +++ b/workspace-template/a2a_executor.py @@ -408,6 +408,12 @@ class LangGraphA2AExecutor(AgentExecutor): return _result - async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: # pragma: no cover - """Cancel a running task (cancellation via asyncio task cancellation).""" - pass + async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: + """Cancel a running task — emits canceled state to comply with A2A protocol.""" + from a2a.types import TaskStatus, TaskState, TaskStatusUpdateEvent + await event_queue.enqueue_event( + TaskStatusUpdateEvent( + status=TaskStatus(state=TaskState.canceled), + final=True, + ) + ) diff --git a/workspace-template/main.py b/workspace-template/main.py index 9f8c4fca..d54e7bb3 100644 --- a/workspace-template/main.py +++ b/workspace-template/main.py @@ -12,7 +12,7 @@ import httpx import uvicorn from a2a.server.apps import A2AStarletteApplication from a2a.server.request_handlers import DefaultRequestHandler -from a2a.server.tasks import InMemoryTaskStore +from a2a.server.tasks import InMemoryTaskStore, InMemoryPushNotificationConfigStore, PushNotificationSender from a2a.types import AgentCard, AgentCapabilities, AgentSkill from adapters import get_adapter, AdapterConfig @@ -136,6 +136,7 @@ async def main(): # pragma: no cover capabilities=AgentCapabilities( streaming=config.a2a.streaming, pushNotifications=config.a2a.push_notifications, + stateTransitionHistory=True, ), skills=[ AgentSkill( @@ -155,6 +156,8 @@ async def main(): # pragma: no cover handler = DefaultRequestHandler( agent_executor=executor, task_store=InMemoryTaskStore(), + push_config_store=InMemoryPushNotificationConfigStore(), + push_sender=PushNotificationSender(), ) app = A2AStarletteApplication( diff --git a/workspace-template/tests/test_a2a_executor.py b/workspace-template/tests/test_a2a_executor.py index 3c0eba11..9194cd96 100644 --- a/workspace-template/tests/test_a2a_executor.py +++ b/workspace-template/tests/test_a2a_executor.py @@ -998,3 +998,46 @@ def test_default_recursion_limit_value(): """Regression guard: DeepAgents fan-outs need 100+; 500 is today's ceiling.""" from a2a_executor import DEFAULT_RECURSION_LIMIT assert DEFAULT_RECURSION_LIMIT == 500 + + +# --------------------------------------------------------------------------- +# Issue #173 — cancel() emits TaskStatusUpdateEvent(state=canceled, final=True) +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_cancel_emits_canceled_event(monkeypatch): + """cancel() must enqueue a TaskStatusUpdateEvent with state=canceled and final=True. + + The a2a.types module is pre-mocked by conftest; inject the three extra + type stubs needed by cancel() so the local import inside the method resolves. + """ + import sys + types_mod = sys.modules["a2a.types"] + + class _TaskState: + canceled = "canceled" + + class _TaskStatus: + def __init__(self, state=None): + self.state = state + + class _TaskStatusUpdateEvent: + def __init__(self, status=None, final=False): + self.status = status + self.final = final + + monkeypatch.setattr(types_mod, "TaskState", _TaskState, raising=False) + monkeypatch.setattr(types_mod, "TaskStatus", _TaskStatus, raising=False) + monkeypatch.setattr(types_mod, "TaskStatusUpdateEvent", _TaskStatusUpdateEvent, raising=False) + + executor = LangGraphA2AExecutor(agent=MagicMock(), heartbeat=None) + context = _make_context([]) + eq = _make_event_queue() + + await executor.cancel(context, eq) + + eq.enqueue_event.assert_called_once() + event = eq.enqueue_event.call_args[0][0] + assert isinstance(event, _TaskStatusUpdateEvent), "expected a TaskStatusUpdateEvent" + assert event.final is True, "cancel event must be marked final=True" + assert event.status.state == _TaskState.canceled, "cancel event must have state=canceled"