vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::sync::Arc;

use serde_json::{json, Value};

use crate::agent::Agent;
use crate::tools::{ToolContext, ToolHandler, ToolSpec, ToolSpecKind};
use crate::types::{ToolArguments, ToolDirective, ToolExecutionResult, ToolResultStatus};

#[derive(Clone)]
pub struct Handoff {
    target: Arc<Agent>,
    description: Option<String>,
    tool_name: String,
}

impl Handoff {
    pub fn target(&self) -> &Agent {
        &self.target
    }

    pub fn description(&self) -> Option<&str> {
        self.description.as_deref()
    }

    pub fn tool_name(&self) -> &str {
        &self.tool_name
    }

    pub fn as_tool_spec(&self, from_agent: &str) -> ToolSpec {
        let target = self.target.name().to_string();
        let description = self
            .description
            .clone()
            .unwrap_or_else(|| format!("Transfer the conversation to {target}."));
        let from_agent = from_agent.to_string();
        let tool_name = self.tool_name.clone();
        let target_for_handler = target.clone();
        let handler: ToolHandler = Arc::new(
            move |_context: &mut ToolContext, arguments: &ToolArguments| {
                handoff_tool_result(&from_agent, &target_for_handler, arguments)
            },
        );
        let mut spec = ToolSpec::new(tool_name.clone(), description.clone(), handler);
        spec.kind = ToolSpecKind::Handoff;
        spec.schema = json!({
            "type": "function",
            "function": {
                "name": tool_name,
                "description": description,
                "parameters": {
                    "type": "object",
                    "properties": {
                        "input": {
                            "type": "string",
                            "description": "Input or handoff summary for the target agent."
                        }
                    },
                    "required": ["input"]
                }
            }
        });
        spec
    }
}

pub fn handoff(agent: &Agent) -> HandoffBuilder {
    HandoffBuilder {
        target: Arc::new(agent.clone()),
        description: None,
        tool_name: None,
    }
}

pub struct HandoffBuilder {
    target: Arc<Agent>,
    description: Option<String>,
    tool_name: Option<String>,
}

impl HandoffBuilder {
    pub fn description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.tool_name = Some(name.into());
        self
    }

    pub fn build(self) -> Handoff {
        let target_name = self.target.name().to_string();
        Handoff {
            target: self.target,
            description: self.description,
            tool_name: self
                .tool_name
                .unwrap_or_else(|| format!("transfer_to_{target_name}")),
        }
    }
}

impl From<HandoffBuilder> for Handoff {
    fn from(builder: HandoffBuilder) -> Self {
        builder.build()
    }
}

fn handoff_tool_result(
    from_agent: &str,
    to_agent: &str,
    arguments: &ToolArguments,
) -> ToolExecutionResult {
    let input = arguments
        .get("input")
        .and_then(value_as_string)
        .unwrap_or_default();
    let mut metadata = std::collections::BTreeMap::new();
    metadata.insert("handoff".to_string(), Value::Bool(true));
    metadata.insert(
        "from_agent".to_string(),
        Value::String(from_agent.to_string()),
    );
    metadata.insert("to_agent".to_string(), Value::String(to_agent.to_string()));
    metadata.insert("handoff_input".to_string(), Value::String(input.clone()));
    metadata.insert(
        "final_message".to_string(),
        Value::String(format!("Handing off to {to_agent}.")),
    );
    ToolExecutionResult {
        tool_call_id: String::new(),
        content: json!({
            "ok": true,
            "handoff": true,
            "from_agent": from_agent,
            "to_agent": to_agent,
            "input": input,
        })
        .to_string(),
        status: ToolResultStatus::Success,
        directive: ToolDirective::Finish,
        error_code: None,
        metadata,
        image_url: None,
        image_path: None,
    }
}

fn value_as_string(value: &Value) -> Option<String> {
    match value {
        Value::String(value) => {
            let value = value.trim();
            (!value.is_empty()).then(|| value.to_string())
        }
        other => {
            let value = other.to_string();
            (!value.is_empty()).then_some(value)
        }
    }
}