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:
parent
01fcc9a4b6
commit
35bcad9204
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user