-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Wire A2A inter-agent messaging into orchestrator + API #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1214,3 +1214,55 @@ async def get_agent_status(agent_id: str): | |||||||||||||||
| error=execution.error, | ||||||||||||||||
| ).model_dump() | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| # ============================================================ | ||||||||||||||||
| # A2A Inter-Agent Messaging | ||||||||||||||||
| # ============================================================ | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| @router.post( | ||||||||||||||||
| "/agents/a2a/send", | ||||||||||||||||
| response_model=ApiResponse, | ||||||||||||||||
| summary="Send an A2A message between agents", | ||||||||||||||||
| tags=["Agents"], | ||||||||||||||||
| ) | ||||||||||||||||
| async def send_a2a_message( | ||||||||||||||||
| body: dict[str, Any] = {}, | ||||||||||||||||
| ): | ||||||||||||||||
| """Send a context-share or tool-request message between agents.""" | ||||||||||||||||
|
Comment on lines
+1231
to
+1233
|
||||||||||||||||
| body: dict[str, Any] = {}, | |
| ): | |
| """Send a context-share or tool-request message between agents.""" | |
| body: dict[str, Any] | None = None, | |
| ): | |
| """Send a context-share or tool-request message between agents.""" | |
| body = body or {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AgentOrchestrator is instantiated directly. This endpoint should use the existing dependency injection pattern (Depends(get_agent_orchestrator_service)) to ensure it uses the shared global instance and maintains consistency with other endpoints. Please add orch: AgentOrchestrator = Depends(get_agent_orchestrator_service) to the function signature and remove this line.
Copilot
AI
Feb 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
send_a2a_message/get_a2a_log create a fresh AgentOrchestrator() per request, which bypasses the app’s orchestrator singleton from get_agent_orchestrator_service() (and therefore has no registered agent types and an empty _a2a_log). This makes message delivery/log retrieval effectively non-functional across requests. Use the DI-provided orchestrator (e.g., orch: AgentOrchestrator = Depends(get_agent_orchestrator_service)) instead of instantiating a new one here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AgentOrchestrator is instantiated directly. This endpoint should use the existing dependency injection pattern (Depends(get_agent_orchestrator_service)) to ensure it uses the shared global instance and maintains consistency with other endpoints. Please add orch: AgentOrchestrator = Depends(get_agent_orchestrator_service) to the function signature and remove this line.
Copilot
AI
Feb 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New A2A endpoints and orchestrator behavior are introduced here, but there are no accompanying tests. There are already unit tests covering API v1 models and orchestrator behavior in tests/; adding tests for /api/v1/agents/a2a/send, /api/v1/agents/a2a/log, and the expected context-sharing/logging semantics would help prevent regressions and keep coverage in line with repo expectations.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,13 +9,31 @@ | |||||
|
|
||||||
| import asyncio | ||||||
| import logging | ||||||
| import uuid | ||||||
| from dataclasses import dataclass, field | ||||||
| from datetime import datetime | ||||||
| from typing import Any, Optional | ||||||
|
|
||||||
| from ..base_agent import AgentRequest, AgentResult, BaseAgent | ||||||
|
|
||||||
|
|
||||||
| @dataclass | ||||||
| class A2AContextMessage: | ||||||
| """Lightweight A2A context-share message within the orchestrator.""" | ||||||
|
|
||||||
| sender: str | ||||||
| recipient: str | ||||||
| content: dict[str, Any] | ||||||
| conversation_id: str = "" | ||||||
| timestamp: str = "" | ||||||
|
|
||||||
| def __post_init__(self): | ||||||
| if not self.conversation_id: | ||||||
| self.conversation_id = str(uuid.uuid4()) | ||||||
| if not self.timestamp: | ||||||
| self.timestamp = datetime.utcnow().isoformat() | ||||||
|
|
||||||
|
|
||||||
| @dataclass | ||||||
| class OrchestrationResult: | ||||||
| """Result from agent orchestration""" | ||||||
|
|
@@ -42,6 +60,7 @@ def __init__(self): | |||||
| self.logger = logging.getLogger("agent_orchestrator") | ||||||
| self._agents: dict[str, BaseAgent] = {} | ||||||
| self._agent_types: dict[str, type[BaseAgent]] = {} | ||||||
| self._a2a_log: list[A2AContextMessage] = [] | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The You'll need to
Suggested change
Comment on lines
60
to
+63
|
||||||
| self._task_mappings: dict[str, list[str]] = { | ||||||
| "video_analysis": [ | ||||||
| "video_master", | ||||||
|
|
@@ -178,6 +197,25 @@ async def execute_task( | |||||
| asyncio.get_event_loop().time() - start_time | ||||||
| ) | ||||||
|
|
||||||
| # A2A context sharing: broadcast each agent's output to all others | ||||||
| if orchestration_result.success and len(orchestration_result.results) > 1: | ||||||
| conv_id = str(uuid.uuid4()) | ||||||
| for sender_name, sender_result in orchestration_result.results.items(): | ||||||
| for recipient_name in orchestration_result.results: | ||||||
| if recipient_name != sender_name: | ||||||
| msg = A2AContextMessage( | ||||||
| sender=sender_name, | ||||||
| recipient=recipient_name, | ||||||
| content={"type": "context_share", "output": sender_result.output}, | ||||||
| conversation_id=conv_id, | ||||||
| ) | ||||||
| self._a2a_log.append(msg) | ||||||
|
Comment on lines
+200
to
+212
|
||||||
| self.logger.debug( | ||||||
| "A2A context shared across %d agents (conv=%s)", | ||||||
| len(orchestration_result.results), | ||||||
| conv_id, | ||||||
| ) | ||||||
|
|
||||||
| self.logger.info( | ||||||
| f"Task execution completed: {task_type} " | ||||||
| f"(success={orchestration_result.success}, " | ||||||
|
|
@@ -262,6 +300,54 @@ def add_task_mapping(self, task_type: str, agent_names: list[str]): | |||||
| self._task_mappings[task_type] = agent_names | ||||||
| self.logger.info(f"Added task mapping: {task_type} -> {agent_names}") | ||||||
|
|
||||||
| # --- A2A messaging --- | ||||||
|
|
||||||
| async def send_a2a_message( | ||||||
| self, | ||||||
| sender: str, | ||||||
| recipient: str, | ||||||
| content: dict[str, Any], | ||||||
| conversation_id: str | None = None, | ||||||
| ) -> A2AContextMessage: | ||||||
| """Send an A2A context-share message between agents.""" | ||||||
| msg = A2AContextMessage( | ||||||
| sender=sender, | ||||||
| recipient=recipient, | ||||||
| content=content, | ||||||
| conversation_id=conversation_id or str(uuid.uuid4()), | ||||||
| ) | ||||||
| self._a2a_log.append(msg) | ||||||
|
|
||||||
| # Deliver to recipient agent if it exists | ||||||
| agent = self._agents.get(recipient) | ||||||
| if agent and hasattr(agent, "receive_context"): | ||||||
| try: | ||||||
| await agent.receive_context(content) | ||||||
| except Exception as e: | ||||||
| self.logger.warning("Agent %s failed to receive context: %s", recipient, e) | ||||||
|
|
||||||
| return msg | ||||||
|
|
||||||
| def get_a2a_log( | ||||||
| self, | ||||||
| conversation_id: str | None = None, | ||||||
| limit: int = 50, | ||||||
| ) -> list[dict[str, Any]]: | ||||||
| """Return recent A2A messages, optionally filtered by conversation.""" | ||||||
| msgs = self._a2a_log | ||||||
| if conversation_id: | ||||||
| msgs = [m for m in msgs if m.conversation_id == conversation_id] | ||||||
| return [ | ||||||
| { | ||||||
| "sender": m.sender, | ||||||
| "recipient": m.recipient, | ||||||
| "content": m.content, | ||||||
| "conversation_id": m.conversation_id, | ||||||
| "timestamp": m.timestamp, | ||||||
| } | ||||||
| for m in msgs[-limit:] | ||||||
| ] | ||||||
|
Comment on lines
+331
to
+349
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function can be improved for type safety and robustness:
Here's a revised implementation that addresses both points and is robust for both def get_a2a_log(
self,
conversation_id: str | None = None,
limit: int = 50,
) -> list[A2AContextMessage]:
"""Return recent A2A messages, optionally filtered by conversation."""
if conversation_id:
# Filtering creates a list, so negative slicing is safe here.
return [m for m in self._a2a_log if m.conversation_id == conversation_id][-limit:]
# If self._a2a_log is a deque, convert to list for slicing.
# This is safe but could be optimized for very large deques if performance is critical.
return list(self._a2a_log)[-limit:]References
|
||||||
|
|
||||||
|
|
||||||
| # Global orchestrator instance | ||||||
| orchestrator = AgentOrchestrator() | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function signature uses
dict[str, Any]for the request body, which violates the style guide's requirement for "strict type hinting". Using a Pydantic model provides automatic validation, better API documentation, and improved type safety.You should define a
A2ASendMessageRequestmodel inmodels.pyand use it here. For example:Then, you can update the function signature and body to use this model, which also allows you to remove the manual validation for
recipient.References
dict[str, Any]for the request body instead of a strictly typed Pydantic model, which is required by the coding standards. (link)