diff --git a/workspace/a2a_executor.py b/workspace/a2a_executor.py index 39ca159e..0c160645 100644 --- a/workspace/a2a_executor.py +++ b/workspace/a2a_executor.py @@ -39,8 +39,9 @@ import uuid from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue from a2a.server.tasks import TaskUpdater -from a2a.types import Part, TextPart -from a2a.utils import new_agent_text_message +from a2a.types import Part +# KI-009: a2a-sdk v1 renames a2a.utils → a2a.helpers; TextPart removed (Part takes text= directly) +from a2a.helpers import new_agent_text_message from shared_runtime import ( extract_history as _extract_history, extract_message_text, @@ -334,7 +335,7 @@ class LangGraphA2AExecutor(AgentExecutor): texts = _extract_chunk_text(chunk.content) for text in texts: await updater.add_artifact( - parts=[Part(root=TextPart(text=text))], + parts=[Part(text=text)], # v1: TextPart removed, Part takes text= directly artifact_id=artifact_id, append=has_streamed, # False=first, True=append last_chunk=False, @@ -446,7 +447,7 @@ class LangGraphA2AExecutor(AgentExecutor): from a2a.types import TaskStatus, TaskState, TaskStatusUpdateEvent await event_queue.enqueue_event( TaskStatusUpdateEvent( - status=TaskStatus(state=TaskState.canceled), + status=TaskStatus(state=TaskState.TASK_STATE_CANCELED), # v1: TaskState uses SCREAMING_SNAKE_CASE final=True, ) ) diff --git a/workspace/adapters/google-adk/adapter.py b/workspace/adapters/google-adk/adapter.py index 5b21e4f1..8b3fe9db 100644 --- a/workspace/adapters/google-adk/adapter.py +++ b/workspace/adapters/google-adk/adapter.py @@ -36,7 +36,7 @@ from typing import TYPE_CHECKING, Any from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue -from a2a.utils import new_agent_text_message +from a2a.helpers import new_agent_text_message from adapter_base import AdapterConfig, BaseAdapter @@ -243,7 +243,7 @@ class GoogleADKA2AExecutor(AgentExecutor): await event_queue.enqueue_event( TaskStatusUpdateEvent( - status=TaskStatus(state=TaskState.canceled), + status=TaskStatus(state=TaskState.TASK_STATE_CANCELED), final=True, ) ) diff --git a/workspace/adapters/google-adk/test_adapter.py b/workspace/adapters/google-adk/test_adapter.py index 7a185c1a..770d088c 100644 --- a/workspace/adapters/google-adk/test_adapter.py +++ b/workspace/adapters/google-adk/test_adapter.py @@ -69,21 +69,18 @@ def _make_a2a_stubs() -> None: tasks_mod = ModuleType("a2a.server.tasks") types_mod = ModuleType("a2a.types") - class TextPart: - def __init__(self, text=""): + class Part: + # v1: Part takes text= directly; root= retained for compat during transition + def __init__(self, text=None, root=None, **kwargs): self.text = text - class Part: - def __init__(self, root=None): - self.root = root - - types_mod.TextPart = TextPart types_mod.Part = Part - utils_mod = ModuleType("a2a.utils") + # a2a.helpers (v1: moved from a2a.utils) + helpers_mod = ModuleType("a2a.helpers") # Passthrough so tests can assert on the plain text string, matching the # hermes_executor test convention from conftest.py. - utils_mod.new_agent_text_message = lambda text, **kwargs: text + helpers_mod.new_agent_text_message = lambda text, **kwargs: text a2a_mod = ModuleType("a2a") a2a_server_mod = ModuleType("a2a.server") @@ -94,7 +91,7 @@ def _make_a2a_stubs() -> None: sys.modules["a2a.server.events"] = events_mod sys.modules["a2a.server.tasks"] = tasks_mod sys.modules["a2a.types"] = types_mod - sys.modules["a2a.utils"] = utils_mod + sys.modules["a2a.helpers"] = helpers_mod def _make_google_adk_stubs() -> None: diff --git a/workspace/claude_sdk_executor.py b/workspace/claude_sdk_executor.py index f702eef5..e299af6f 100644 --- a/workspace/claude_sdk_executor.py +++ b/workspace/claude_sdk_executor.py @@ -39,7 +39,7 @@ import claude_agent_sdk as sdk from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue -from a2a.utils import new_agent_text_message +from a2a.helpers import new_agent_text_message from executor_helpers import ( CONFIG_MOUNT, diff --git a/workspace/cli_executor.py b/workspace/cli_executor.py index 2f2802ec..5be84d9f 100644 --- a/workspace/cli_executor.py +++ b/workspace/cli_executor.py @@ -34,7 +34,8 @@ from pathlib import Path from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue -from a2a.utils import new_agent_text_message +# KI-009: a2a-sdk v1 renames a2a.utils → a2a.helpers +from a2a.helpers import new_agent_text_message from config import RuntimeConfig from executor_helpers import ( diff --git a/workspace/hermes_executor.py b/workspace/hermes_executor.py index ceeeddba..e9453560 100644 --- a/workspace/hermes_executor.py +++ b/workspace/hermes_executor.py @@ -113,7 +113,7 @@ from typing import TYPE_CHECKING, Any from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.events import EventQueue -from a2a.utils import new_agent_text_message +from a2a.helpers import new_agent_text_message if TYPE_CHECKING: from heartbeat import HeartbeatLoop @@ -539,7 +539,7 @@ class HermesA2AExecutor(AgentExecutor): await event_queue.enqueue_event( TaskStatusUpdateEvent( - status=TaskStatus(state=TaskState.canceled), + status=TaskStatus(state=TaskState.TASK_STATE_CANCELED), final=True, ) ) diff --git a/workspace/main.py b/workspace/main.py index c95feba6..1cdd64aa 100644 --- a/workspace/main.py +++ b/workspace/main.py @@ -10,10 +10,12 @@ import socket import httpx import uvicorn -from a2a.server.apps import A2AStarletteApplication +# KI-009 a2a-sdk v1 migration: A2AStarletteApplication removed; use Starlette route factory +from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.tasks import InMemoryTaskStore -from a2a.types import AgentCard, AgentCapabilities, AgentSkill +from a2a.types import AgentCard, AgentCapabilities, AgentSkill, AgentInterface +from starlette.applications import Starlette from adapters import get_adapter, AdapterConfig from agents_md import generate_agents_md @@ -164,15 +166,20 @@ async def main(): # pragma: no cover machine_ip = os.environ.get("HOSTNAME", get_machine_ip()) workspace_url = f"http://{machine_ip}:{port}" + # v1: AgentCard.url removed; put url+protocol in supported_protocols instead. + # v1: AgentCapabilities.inputModes/outputModes removed; move to AgentCard.default_*. + # v1: pushNotifications → push_notifications (Pydantic field name) agent_card = AgentCard( name=config.name, description=config.description or config.name, version=config.version, - url=workspace_url, + supported_protocols=[ + AgentInterface(protocol_binding="https://a2a.g/v1", url=workspace_url) + ], capabilities=AgentCapabilities( streaming=config.a2a.streaming, - pushNotifications=config.a2a.push_notifications, - stateTransitionHistory=True, + push_notifications=config.a2a.push_notifications, + state_transition_history=True, ), skills=[ AgentSkill( @@ -184,8 +191,8 @@ async def main(): # pragma: no cover ) for skill in loaded_skills ], - defaultInputModes=["text/plain", "application/json"], - defaultOutputModes=["text/plain", "application/json"], + default_input_modes=["text/plain", "application/json"], + default_output_modes=["text/plain", "application/json"], ) # 7. Wrap in A2A. @@ -204,10 +211,11 @@ async def main(): # pragma: no cover task_store=InMemoryTaskStore(), ) - app = A2AStarletteApplication( - agent_card=agent_card, - http_handler=handler, - ) + # v1: replace A2AStarletteApplication with Starlette route factory + routes = [] + routes.extend(create_agent_card_routes(agent_card)) + routes.extend(create_jsonrpc_routes(request_handler=handler)) + app = Starlette(routes=routes) # 8. Register with platform agent_card_dict = { @@ -316,7 +324,8 @@ async def main(): # pragma: no cover print(f"Workspace {workspace_id} starting on port {port}") # Wrap the ASGI app with W3C TraceContext extraction middleware so incoming # A2A HTTP requests propagate their trace context into _incoming_trace_context. - starlette_app = app.build() + # v1: Starlette app is constructed directly; no build() step needed + starlette_app = app # Add /transcript route — exposes the most-recent agent session log # (claude-code reads ~/.claude/projects//.jsonl). Other diff --git a/workspace/requirements.txt b/workspace/requirements.txt index 24b11e35..b58699de 100644 --- a/workspace/requirements.txt +++ b/workspace/requirements.txt @@ -3,7 +3,10 @@ # and installed at container startup via entrypoint.sh # A2A protocol -a2a-sdk[http-server]==0.3.25 +# KI-009 a2a-sdk v1 migration (2026-04-24): bumped from ==0.3.25. +# v1.0 removes A2AStarletteApplication → Starlette route factory pattern. +# Rollback: pin ==0.3.25 and revert main.py + executor changes. +a2a-sdk[http-server]>=1.0.0,<2.0 # HTTP / server httpx>=0.27.0 diff --git a/workspace/tests/conftest.py b/workspace/tests/conftest.py index 1465d12c..4c1c5f04 100644 --- a/workspace/tests/conftest.py +++ b/workspace/tests/conftest.py @@ -65,25 +65,19 @@ def _make_a2a_mocks(): tasks_mod.TaskUpdater = TaskUpdater - # a2a.types needs Part and TextPart stubs for artifact construction + # a2a.types needs Part stub for artifact construction (v1: Part takes text= directly, no TextPart) types_mod = ModuleType("a2a.types") - class TextPart: - """Stub for A2A TextPart.""" - def __init__(self, text=""): + class Part: + """Stub for A2A Part (v1: takes text= kwarg directly).""" + def __init__(self, text=None, root=None, **kwargs): self.text = text - class Part: - """Stub for A2A Part (wraps TextPart / FilePart / DataPart).""" - def __init__(self, root=None): - self.root = root - - types_mod.TextPart = TextPart types_mod.Part = Part - # a2a.utils needs new_agent_text_message as a passthrough (accepts kwargs) - utils_mod = ModuleType("a2a.utils") - utils_mod.new_agent_text_message = lambda text, **kwargs: text + # a2a.helpers (v1: moved from a2a.utils) + helpers_mod = ModuleType("a2a.helpers") + helpers_mod.new_agent_text_message = lambda text, **kwargs: text # Register all module paths a2a_mod = ModuleType("a2a") @@ -95,7 +89,7 @@ def _make_a2a_mocks(): sys.modules["a2a.server.events"] = events_mod sys.modules["a2a.server.tasks"] = tasks_mod sys.modules["a2a.types"] = types_mod - sys.modules["a2a.utils"] = utils_mod + sys.modules["a2a.helpers"] = helpers_mod def _make_langchain_mocks(): diff --git a/workspace/tests/test_a2a_executor.py b/workspace/tests/test_a2a_executor.py index f393dfad..98ad19aa 100644 --- a/workspace/tests/test_a2a_executor.py +++ b/workspace/tests/test_a2a_executor.py @@ -1021,7 +1021,8 @@ async def test_cancel_emits_canceled_event(monkeypatch): types_mod = sys.modules["a2a.types"] class _TaskState: - canceled = "canceled" + # v1: TaskState enum uses SCREAMING_SNAKE_CASE keys + TASK_STATE_CANCELED = "canceled" class _TaskStatus: def __init__(self, state=None): @@ -1046,4 +1047,4 @@ async def test_cancel_emits_canceled_event(monkeypatch): 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" + assert event.status.state == _TaskState.TASK_STATE_CANCELED, "cancel event must have state=TASK_STATE_CANCELED" diff --git a/workspace/tests/test_hermes_executor.py b/workspace/tests/test_hermes_executor.py index 2269bf2c..0b9070e3 100644 --- a/workspace/tests/test_hermes_executor.py +++ b/workspace/tests/test_hermes_executor.py @@ -653,7 +653,7 @@ async def test_cancel_emits_canceled_event(): import a2a.types as a2a_types class _TaskState: - canceled = "canceled" + TASK_STATE_CANCELED = "canceled" # a2a-sdk v1 enum name class _TaskStatus: def __init__(self, state): @@ -675,7 +675,7 @@ async def test_cancel_emits_canceled_event(): eq.enqueue_event.assert_called_once() event = eq.enqueue_event.call_args[0][0] assert isinstance(event, _TaskStatusUpdateEvent) - assert event.status.state == "canceled" + assert event.status.state == _TaskState.TASK_STATE_CANCELED assert event.final is True