Skip to main content

vv_agent/
handoffs.rs

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}