1use std::sync::Arc;
2
3use serde_json::{json, Value};
4
5use crate::agent::Agent;
6use crate::tools::{ToolContext, ToolHandler, ToolSpec, ToolSpecKind};
7use crate::types::{ToolArguments, ToolDirective, ToolExecutionResult, ToolResultStatus};
8
9#[derive(Clone)]
10pub struct Handoff {
11 target: Arc<Agent>,
12 description: Option<String>,
13 tool_name: String,
14}
15
16impl Handoff {
17 pub fn target(&self) -> &Agent {
18 &self.target
19 }
20
21 pub fn description(&self) -> Option<&str> {
22 self.description.as_deref()
23 }
24
25 pub fn tool_name(&self) -> &str {
26 &self.tool_name
27 }
28
29 pub fn as_tool_spec(&self, from_agent: &str) -> ToolSpec {
30 let target = self.target.name().to_string();
31 let description = self
32 .description
33 .clone()
34 .unwrap_or_else(|| format!("Transfer the conversation to {target}."));
35 let from_agent = from_agent.to_string();
36 let tool_name = self.tool_name.clone();
37 let target_for_handler = target.clone();
38 let handler: ToolHandler = Arc::new(
39 move |_context: &mut ToolContext, arguments: &ToolArguments| {
40 handoff_tool_result(&from_agent, &target_for_handler, arguments)
41 },
42 );
43 let mut spec = ToolSpec::new(tool_name.clone(), description.clone(), handler);
44 spec.kind = ToolSpecKind::Handoff;
45 spec.schema = json!({
46 "type": "function",
47 "function": {
48 "name": tool_name,
49 "description": description,
50 "parameters": {
51 "type": "object",
52 "properties": {
53 "input": {
54 "type": "string",
55 "description": "Input or handoff summary for the target agent."
56 }
57 },
58 "required": ["input"]
59 }
60 }
61 });
62 spec
63 }
64}
65
66pub fn handoff(agent: &Agent) -> HandoffBuilder {
67 HandoffBuilder {
68 target: Arc::new(agent.clone()),
69 description: None,
70 tool_name: None,
71 }
72}
73
74pub struct HandoffBuilder {
75 target: Arc<Agent>,
76 description: Option<String>,
77 tool_name: Option<String>,
78}
79
80impl HandoffBuilder {
81 pub fn description(mut self, description: impl Into<String>) -> Self {
82 self.description = Some(description.into());
83 self
84 }
85
86 pub fn name(mut self, name: impl Into<String>) -> Self {
87 self.tool_name = Some(name.into());
88 self
89 }
90
91 pub fn build(self) -> Handoff {
92 let target_name = self.target.name().to_string();
93 Handoff {
94 target: self.target,
95 description: self.description,
96 tool_name: self
97 .tool_name
98 .unwrap_or_else(|| format!("transfer_to_{target_name}")),
99 }
100 }
101}
102
103impl From<HandoffBuilder> for Handoff {
104 fn from(builder: HandoffBuilder) -> Self {
105 builder.build()
106 }
107}
108
109fn handoff_tool_result(
110 from_agent: &str,
111 to_agent: &str,
112 arguments: &ToolArguments,
113) -> ToolExecutionResult {
114 let input = arguments
115 .get("input")
116 .and_then(value_as_string)
117 .unwrap_or_default();
118 let mut metadata = std::collections::BTreeMap::new();
119 metadata.insert("handoff".to_string(), Value::Bool(true));
120 metadata.insert(
121 "from_agent".to_string(),
122 Value::String(from_agent.to_string()),
123 );
124 metadata.insert("to_agent".to_string(), Value::String(to_agent.to_string()));
125 metadata.insert("handoff_input".to_string(), Value::String(input.clone()));
126 metadata.insert(
127 "final_message".to_string(),
128 Value::String(format!("Handing off to {to_agent}.")),
129 );
130 ToolExecutionResult {
131 tool_call_id: String::new(),
132 content: json!({
133 "ok": true,
134 "handoff": true,
135 "from_agent": from_agent,
136 "to_agent": to_agent,
137 "input": input,
138 })
139 .to_string(),
140 status: ToolResultStatus::Success,
141 directive: ToolDirective::Finish,
142 error_code: None,
143 metadata,
144 image_url: None,
145 image_path: None,
146 }
147}
148
149fn value_as_string(value: &Value) -> Option<String> {
150 match value {
151 Value::String(value) => {
152 let value = value.trim();
153 (!value.is_empty()).then(|| value.to_string())
154 }
155 other => {
156 let value = other.to_string();
157 (!value.is_empty()).then_some(value)
158 }
159 }
160}