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};
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}