Coverage for agentos/core/handoff.py: 53%
47 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
1"""
2Handoff protocol for NexusAgent.
4Provides Swarm-style task transfer between agents.
5When an agent cannot handle a request, it can transfer
6to another agent that is better suited.
7"""
9from __future__ import annotations
11from dataclasses import dataclass, field
12from typing import Any, TypeVar
14from agentos.core.di import Agent, RunContext
16# Type variable for agent
17T = TypeVar("T")
20@dataclass
21class Handoff:
22 """
23 Represents a handoff request to another agent.
25 Usage:
26 class SupportAgent(Agent[str, str]):
27 async def run(self, ctx: RunContext[str]) -> str | Handoff:
28 if "billing" in ctx.deps.lower():
29 return transfer_to(BillingAgent(), ctx.deps)
30 return "General support"
31 """
33 target_agent: Agent[Any, Any]
34 input_data: Any
35 metadata: dict[str, Any] = field(default_factory=dict)
36 reason: str = ""
38 def __post_init__(self):
39 """Validate handoff."""
40 if self.target_agent is None:
41 raise ValueError("target_agent cannot be None")
44@dataclass
45class HandoffResult:
46 """
47 Result of a handoff operation.
49 Contains:
50 - output: The final output from the target agent
51 - source_agent: Name of the original agent
52 - target_agent: Name of the agent that handled it
53 - handoff_chain: List of agents involved
54 """
56 output: Any
57 source_agent: str
58 target_agent: str
59 handoff_chain: list[str] = field(default_factory=list)
60 metadata: dict[str, Any] = field(default_factory=dict)
63def transfer_to(agent: Agent[Any, Any], input_data: Any, reason: str = "", **metadata) -> Handoff:
64 """
65 Create a handoff to another agent.
67 Args:
68 agent: Target agent to transfer to
69 input_data: Data to pass to the target agent
70 reason: Reason for the handoff
71 **metadata: Additional metadata
73 Returns:
74 Handoff object
76 Usage:
77 return transfer_to(BillingAgent(), ctx.deps, reason="Billing question")
78 """
79 return Handoff(
80 target_agent=agent,
81 input_data=input_data,
82 metadata=metadata,
83 reason=reason,
84 )
87def can_handle(agent: Agent[Any, Any], input_data: Any) -> bool:
88 """
89 Check if an agent can handle the input.
91 This is a helper function that calls the agent's
92 can_handle() method if it exists, otherwise returns True.
94 Args:
95 agent: Agent to check
96 input_data: Input data
98 Returns:
99 True if agent can handle, False otherwise
100 """
101 if hasattr(agent, "can_handle"):
102 return agent.can_handle(input_data)
103 return True
106async def execute_with_handoff(
107 agent: Agent[Any, Any], input_data: Any, max_hops: int = 10, **metadata
108) -> HandoffResult | Any:
109 """
110 Execute an agent with automatic handoff handling.
112 If the agent returns a Handoff, automatically execute
113 the target agent and return the result.
115 Args:
116 agent: Starting agent
117 input_data: Input data
118 max_hops: Maximum number of handoffs
119 **metadata: Additional metadata
121 Returns:
122 HandoffResult if handoffs occurred, otherwise raw output
124 Raises:
125 RuntimeError: If max_hops exceeded
126 """
127 current_agent = agent
128 current_input = input_data
129 handoff_chain = [current_agent.name]
131 for hop in range(max_hops):
132 # Execute current agent
133 result = await current_agent.invoke(current_input, **metadata)
135 # Check if result is a handoff
136 if isinstance(result, Handoff):
137 # Move to next agent
138 current_agent = result.target_agent
139 current_input = result.input_data
140 handoff_chain.append(current_agent.name)
142 # Merge metadata
143 metadata.update(result.metadata)
144 else:
145 # No handoff, we're done
146 if len(handoff_chain) > 1:
147 # Return HandoffResult if we had handoffs
148 return HandoffResult(
149 output=result,
150 source_agent=handoff_chain[0],
151 target_agent=handoff_chain[-1],
152 handoff_chain=handoff_chain,
153 metadata=metadata,
154 )
155 else:
156 # No handoffs, return raw output
157 return result
159 raise RuntimeError(f"Max handoff hops ({max_hops}) exceeded")
162class HandoffAwareAgent(Agent[Any, Any]):
163 """
164 Base class for agents that support handoffs.
166 Provides can_handle() method for checking if agent
167 can handle input, and run() can return Handoff.
168 """
170 def can_handle(self, input_data: Any) -> bool:
171 """
172 Check if this agent can handle the input.
174 Override in subclass to add custom logic.
176 Args:
177 input_data: Input data
179 Returns:
180 True if can handle, False otherwise
181 """
182 return True
184 async def run(self, ctx: RunContext[Any]) -> Any:
185 """
186 Main agent logic. Can return Handoff to transfer.
188 Override in subclass.
189 """
190 raise NotImplementedError("Subclass must implement run()")
193# ── Auto-generated compat stubs ──