vv-agent 0.7.1

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

use serde::de::DeserializeOwned;

use crate::guardrails::{InputGuardrail, OutputGuardrail};
use crate::handoffs::Handoff;
use crate::model::ModelRef;
use crate::model_settings::ModelSettings;
use crate::runtime::RuntimeHook;
use crate::tools::common::trim_portable_whitespace;
use crate::tools::{AgentToolBuilder, BackgroundAgentTaskBuilder};
use crate::tools::{Tool, ToolPolicy};
use crate::types::{Metadata, NoToolPolicy, SubAgentConfig};

pub type InstructionProvider =
    Arc<dyn Fn(&crate::context::RunContext, &Agent) -> String + Send + Sync>;
pub type OutputValidator = Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>;

#[derive(Clone)]
pub struct Agent {
    name: String,
    instructions: String,
    instruction_provider: Option<InstructionProvider>,
    model: Option<ModelRef>,
    model_settings: ModelSettings,
    tools: Vec<Arc<dyn Tool>>,
    handoffs: Vec<Handoff>,
    input_guardrails: Vec<Arc<dyn InputGuardrail>>,
    output_guardrails: Vec<Arc<dyn OutputGuardrail>>,
    output_type_name: Option<&'static str>,
    output_validator: Option<OutputValidator>,
    hooks: Vec<Arc<dyn RuntimeHook>>,
    max_cycles: Option<u32>,
    no_tool_policy: Option<NoToolPolicy>,
    tool_use_behavior: ToolUseBehavior,
    tool_policy: ToolPolicy,
    sub_agents: BTreeMap<String, SubAgentConfig>,
    metadata: Metadata,
}

impl Agent {
    pub fn builder(name: impl Into<String>) -> AgentBuilder {
        AgentBuilder {
            agent: Self {
                name: name.into(),
                instructions: String::new(),
                instruction_provider: None,
                model: None,
                model_settings: ModelSettings::default(),
                tools: Vec::new(),
                handoffs: Vec::new(),
                input_guardrails: Vec::new(),
                output_guardrails: Vec::new(),
                output_type_name: None,
                output_validator: None,
                hooks: Vec::new(),
                max_cycles: None,
                no_tool_policy: None,
                tool_use_behavior: ToolUseBehavior::default(),
                tool_policy: ToolPolicy::default(),
                sub_agents: BTreeMap::new(),
                metadata: Metadata::new(),
            },
            sub_agent_error: None,
        }
    }

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

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

    pub(crate) fn has_dynamic_instructions(&self) -> bool {
        self.instruction_provider.is_some()
    }

    pub fn resolve_instructions(&self, context: &crate::context::RunContext) -> String {
        self.instruction_provider
            .as_ref()
            .map(|provider| provider(context, self))
            .unwrap_or_else(|| self.instructions.clone())
    }

    pub fn model(&self) -> Option<&ModelRef> {
        self.model.as_ref()
    }

    pub fn model_settings(&self) -> &ModelSettings {
        &self.model_settings
    }

    pub fn tools(&self) -> &[Arc<dyn Tool>] {
        &self.tools
    }

    pub fn handoffs(&self) -> &[Handoff] {
        &self.handoffs
    }

    pub(crate) fn input_guardrails(&self) -> &[Arc<dyn InputGuardrail>] {
        &self.input_guardrails
    }

    pub(crate) fn output_guardrails(&self) -> &[Arc<dyn OutputGuardrail>] {
        &self.output_guardrails
    }

    pub fn output_type_name(&self) -> Option<&'static str> {
        self.output_type_name
    }

    pub fn validate_output(&self, output: &str) -> Result<(), String> {
        match self.output_validator.as_ref() {
            Some(validator) => validator(output),
            None => Ok(()),
        }
    }

    pub fn hooks(&self) -> &[Arc<dyn RuntimeHook>] {
        &self.hooks
    }

    pub fn max_cycles(&self) -> Option<u32> {
        self.max_cycles
    }

    pub fn no_tool_policy(&self) -> Option<NoToolPolicy> {
        self.no_tool_policy
    }

    pub fn tool_use_behavior(&self) -> &ToolUseBehavior {
        &self.tool_use_behavior
    }

    pub fn tool_policy(&self) -> &ToolPolicy {
        &self.tool_policy
    }

    pub fn sub_agents(&self) -> &BTreeMap<String, SubAgentConfig> {
        &self.sub_agents
    }

    pub fn metadata(&self) -> &Metadata {
        &self.metadata
    }

    pub fn as_tool(&self) -> AgentToolBuilder {
        AgentToolBuilder::new(self.clone())
    }

    pub fn as_background_task(&self) -> BackgroundAgentTaskBuilder {
        BackgroundAgentTaskBuilder::new(self.clone())
    }
}

pub struct AgentBuilder {
    agent: Agent,
    sub_agent_error: Option<String>,
}

impl AgentBuilder {
    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
        self.agent.instructions = instructions.into();
        self.agent.instruction_provider = None;
        self
    }

    pub fn dynamic_instructions(
        mut self,
        provider: impl Fn(&crate::context::RunContext, &Agent) -> String + Send + Sync + 'static,
    ) -> Self {
        self.agent.instructions.clear();
        self.agent.instruction_provider = Some(Arc::new(provider));
        self
    }

    pub fn model(mut self, model: ModelRef) -> Self {
        self.agent.model = Some(model);
        self
    }

    pub fn model_settings(mut self, settings: ModelSettings) -> Self {
        self.agent.model_settings = settings;
        self
    }

    pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
        self.agent.tools.push(Arc::new(tool));
        self
    }

    pub fn tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
        self.agent.tools.push(tool);
        self
    }

    pub fn handoff(mut self, handoff: impl Into<Handoff>) -> Self {
        self.agent.handoffs.push(handoff.into());
        self
    }

    pub fn input_guardrail(mut self, guardrail: Arc<dyn InputGuardrail>) -> Self {
        self.agent.input_guardrails.push(guardrail);
        self
    }

    pub fn output_guardrail(mut self, guardrail: Arc<dyn OutputGuardrail>) -> Self {
        self.agent.output_guardrails.push(guardrail);
        self
    }

    pub fn output_type<T>(mut self) -> Self
    where
        T: DeserializeOwned + 'static,
    {
        self.agent.output_type_name = Some(std::any::type_name::<T>());
        self.agent.output_validator = Some(Arc::new(|output| {
            serde_json::from_str::<T>(output)
                .map(|_| ())
                .map_err(|error| error.to_string())
        }));
        self
    }

    pub fn output_validator(
        mut self,
        name: &'static str,
        validator: impl Fn(&str) -> Result<(), String> + Send + Sync + 'static,
    ) -> Self {
        self.agent.output_type_name = Some(name);
        self.agent.output_validator = Some(Arc::new(validator));
        self
    }

    pub fn hook(mut self, hook: Arc<dyn RuntimeHook>) -> Self {
        self.agent.hooks.push(hook);
        self
    }

    pub fn max_cycles(mut self, max_cycles: u32) -> Self {
        self.agent.max_cycles = Some(max_cycles);
        self
    }

    pub fn no_tool_policy(mut self, policy: NoToolPolicy) -> Self {
        self.agent.no_tool_policy = Some(policy);
        self
    }

    pub fn tool_use_behavior(mut self, behavior: ToolUseBehavior) -> Self {
        self.agent.tool_use_behavior = behavior;
        self
    }

    pub fn tool_policy(mut self, tool_policy: ToolPolicy) -> Self {
        self.agent.tool_policy = tool_policy;
        self
    }

    pub fn sub_agent(mut self, id: impl AsRef<str>, config: impl Borrow<SubAgentConfig>) -> Self {
        self.insert_sub_agent(id.as_ref(), config.borrow());
        self
    }

    pub fn sub_agents<I, K, C>(mut self, sub_agents: I) -> Self
    where
        I: IntoIterator<Item = (K, C)>,
        K: AsRef<str>,
        C: Borrow<SubAgentConfig>,
    {
        for (id, config) in sub_agents {
            self.insert_sub_agent(id.as_ref(), config.borrow());
        }
        self
    }

    pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
        self.agent.metadata.insert(key.into(), value);
        self
    }

    pub fn build(self) -> Result<Agent, String> {
        if self.agent.name.trim().is_empty() {
            return Err("agent name cannot be empty".to_string());
        }
        if self.agent.instructions.trim().is_empty() && self.agent.instruction_provider.is_none() {
            return Err("agent instructions cannot be empty".to_string());
        }
        if let Some(error) = self.sub_agent_error {
            return Err(error);
        }
        Ok(self.agent)
    }

    fn insert_sub_agent(&mut self, id: &str, config: &SubAgentConfig) {
        let normalized_id = trim_portable_whitespace(id);
        if normalized_id.is_empty() {
            self.sub_agent_error
                .get_or_insert_with(|| "sub-agent id cannot be empty".to_string());
            return;
        }
        if self.agent.sub_agents.contains_key(normalized_id) {
            self.sub_agent_error.get_or_insert_with(|| {
                format!("duplicate sub-agent id after normalization: {normalized_id}")
            });
            return;
        }
        self.agent
            .sub_agents
            .insert(normalized_id.to_string(), config.clone());
    }
}

#[derive(Clone, Default)]
pub enum ToolUseBehavior {
    #[default]
    RunLlmAgain,
    StopOnFirstTool,
    StopAtToolNames(Vec<String>),
}