feat(workspace): migrate a2a-sdk from 0.3.x to 1.0.0 (KI-009) (#1974)

* feat(workspace): migrate a2a-sdk from 0.3.x to 1.0.0 (KI-009)

Migrates all workspace code from a2a-sdk v0.3.x to v1.0.0, following the
official migration guide from a2aproject/a2a-python.

Breaking changes applied:
- A2AStarletteApplication → Starlette route factory
  (create_agent_card_routes + create_jsonrpc_routes)
- AgentCard.url removed; url+protocol now in supported_protocols[].url
- AgentCapabilities fields renamed to snake_case
  (pushNotifications→push_notifications,
   stateTransitionHistory→state_transition_history)
- AgentCard.defaultInputModes/outputModes → default_input_modes/output_modes
- TaskState.canceled → TaskState.TASK_STATE_CANCELED
- a2a.utils → a2a.helpers
- Part(root=TextPart(text=t)) → Part(text=t) (TextPart removed)

Files changed:
- requirements.txt: pinned >=1.0.0,<2.0
- main.py: Starlette route factory + AgentCard restructure
- a2a_executor.py: Part() + TaskState + helpers import
- hermes_executor.py: TaskState + helpers import
- google-adk/adapter.py: TaskState + helpers import
- cli_executor.py: helpers import
- claude_sdk_executor.py: helpers import
- tests/conftest.py: a2a.helpers mock stub
- tests/test_a2a_executor.py: TaskState enum key
- adapters/google-adk/test_adapter.py: Part + helpers stub

Refs: KI-009
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): update _TaskState mock to a2a-sdk v1 enum name (TASK_STATE_CANCELED)

---------

Co-authored-by: Molecule AI Tech Researcher <tech-researcher@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: molecule-ai[bot] <276602405+molecule-ai[bot]@users.noreply.github.com>
This commit is contained in:
molecule-ai[bot] 2026-04-24 04:43:17 +00:00 committed by GitHub
parent 01fcc9a4b6
commit 35bcad9204
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 57 additions and 51 deletions

View File

@ -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,
)
)

View File

@ -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,
)
)

View File

@ -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:

View File

@ -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,

View File

@ -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 (

View File

@ -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,
)
)

View File

@ -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/<cwd>/<session>.jsonl). Other

View File

@ -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

View File

@ -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():

View File

@ -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"

View File

@ -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