simbiot-harness/venv/lib/python3.12/site-packages/strands/multiagent/a2a/_converters.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

199 lines
6.8 KiB
Python

"""Conversion functions between Strands and A2A types."""
from typing import cast
from uuid import uuid4
from a2a.types import Message as A2AMessage
from a2a.types import Part, Role, TaskArtifactUpdateEvent, TaskState, TaskStatusUpdateEvent, TextPart
from ...agent.agent_result import AgentResult
from ...telemetry.metrics import EventLoopMetrics
from ...types.a2a import A2AResponse
from ...types.agent import AgentInput
from ...types.content import ContentBlock, Message
from ...types.event_loop import StopReason
# Mapping from A2A TaskState to Strands stop_reason
_STATE_TO_STOP_REASON: dict[TaskState, StopReason] = {
TaskState.completed: "end_turn",
TaskState.failed: "end_turn",
TaskState.canceled: "end_turn",
TaskState.rejected: "end_turn",
TaskState.input_required: "interrupt",
TaskState.auth_required: "interrupt",
}
def convert_input_to_message(prompt: AgentInput) -> A2AMessage:
"""Convert AgentInput to A2A Message.
Args:
prompt: Input in various formats (string, message list, or content blocks).
Returns:
A2AMessage ready to send to the remote agent.
Raises:
ValueError: If prompt format is unsupported.
"""
message_id = uuid4().hex
if isinstance(prompt, str):
return A2AMessage(
kind="message",
role=Role.user,
parts=[Part(TextPart(kind="text", text=prompt))],
message_id=message_id,
)
if isinstance(prompt, list) and prompt and (isinstance(prompt[0], dict)):
# Check for interrupt responses - not supported in A2A
if "interruptResponse" in prompt[0]:
raise ValueError("InterruptResponseContent is not supported for A2AAgent")
if "role" in prompt[0]:
for msg in reversed(prompt):
if msg.get("role") == "user":
content = cast(list[ContentBlock], msg.get("content", []))
parts = convert_content_blocks_to_parts(content)
return A2AMessage(
kind="message",
role=Role.user,
parts=parts,
message_id=message_id,
)
else:
parts = convert_content_blocks_to_parts(prompt)
return A2AMessage(
kind="message",
role=Role.user,
parts=parts,
message_id=message_id,
)
raise ValueError(f"Unsupported input type: {type(prompt)}")
def convert_content_blocks_to_parts(content_blocks: list[ContentBlock]) -> list[Part]:
"""Convert Strands ContentBlocks to A2A Parts.
Args:
content_blocks: List of Strands content blocks.
Returns:
List of A2A Part objects.
"""
parts = []
for block in content_blocks:
if "text" in block:
parts.append(Part(TextPart(kind="text", text=block["text"])))
return parts
def _extract_text_parts(parts: list[Part]) -> list[ContentBlock]:
"""Extract text content blocks from A2A parts.
Args:
parts: List of A2A Part objects.
Returns:
List of ContentBlock dicts for each text part found.
"""
content: list[ContentBlock] = []
for part in parts:
if hasattr(part, "root") and hasattr(part.root, "text"):
content.append({"text": part.root.text})
return content
def _extract_task_state(response: A2AResponse) -> TaskState | None:
"""Extract the task state from an A2A response.
Args:
response: A2A response (either A2AMessage or tuple of task and update event).
Returns:
The TaskState if available, None otherwise.
"""
if isinstance(response, tuple) and len(response) == 2:
_task, update_event = response
if isinstance(update_event, TaskStatusUpdateEvent):
if update_event.status and hasattr(update_event.status, "state"):
return update_event.status.state
return None
def convert_response_to_agent_result(response: A2AResponse) -> AgentResult:
"""Convert A2A response to AgentResult.
Maps A2A task lifecycle states to appropriate Strands stop_reasons:
- completed → end_turn
- failed → end_turn (with error content)
- canceled → end_turn (with cancellation info)
- rejected → end_turn (with rejection info)
- input_required → interrupt (agent needs user input)
- auth_required → interrupt (agent needs authentication)
Args:
response: A2A response (either A2AMessage or tuple of task and update event).
Returns:
AgentResult with extracted content and metadata.
"""
content: list[ContentBlock] = []
task_state = _extract_task_state(response)
stop_reason: StopReason = _STATE_TO_STOP_REASON.get(task_state, "end_turn") if task_state else "end_turn"
if isinstance(response, tuple) and len(response) == 2:
task, update_event = response
# Handle artifact updates
if isinstance(update_event, TaskArtifactUpdateEvent):
if update_event.artifact and hasattr(update_event.artifact, "parts") and update_event.artifact.parts:
content.extend(_extract_text_parts(update_event.artifact.parts))
# Handle status updates with messages
elif isinstance(update_event, TaskStatusUpdateEvent):
if (
update_event.status
and hasattr(update_event.status, "message")
and update_event.status.message
and update_event.status.message.parts
):
content.extend(_extract_text_parts(update_event.status.message.parts))
# Use task.artifacts when no content was extracted from the event
if not content and task and hasattr(task, "artifacts") and task.artifacts is not None:
for artifact in task.artifacts:
if hasattr(artifact, "parts") and artifact.parts:
content.extend(_extract_text_parts(artifact.parts))
# Use task.status.message when no content was extracted from artifacts
if (
not content
and task
and hasattr(task, "status")
and task.status
and hasattr(task.status, "message")
and task.status.message
and task.status.message.parts
):
content.extend(_extract_text_parts(task.status.message.parts))
elif isinstance(response, A2AMessage):
content.extend(_extract_text_parts(response.parts))
message: Message = {
"role": "assistant",
"content": content,
}
# Build state dict with A2A metadata
state: dict[str, str] = {}
if task_state is not None:
state["a2a_task_state"] = task_state.value
return AgentResult(
stop_reason=stop_reason,
message=message,
metrics=EventLoopMetrics(),
state=state,
)