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