Skip to main content

vv_agent/runtime/
tool_planner.rs

1use serde_json::Value;
2
3use crate::constants::{
4    ACTIVATE_SKILL_TOOL_NAME, ASK_USER_TOOL_NAME, BASH_TOOL_NAME,
5    CHECK_BACKGROUND_COMMAND_TOOL_NAME, CREATE_SUB_TASK_TOOL_NAME, READ_IMAGE_TOOL_NAME,
6    SUB_TASK_STATUS_TOOL_NAME, TASK_FINISH_TOOL_NAME, WORKSPACE_TOOLS,
7};
8use crate::tools::ToolRegistry;
9use crate::types::AgentTask;
10
11use super::shell::{normalize_windows_shell_priority, resolve_shell_invocation};
12
13const BASH_RUNTIME_HINT_METADATA_KEY: &str = "_vv_agent_bash_runtime_hint";
14
15pub fn plan_tool_names(task: &AgentTask, memory_usage_percentage: Option<u32>) -> Vec<String> {
16    let _ = memory_usage_percentage;
17    let mut names = vec![TASK_FINISH_TOOL_NAME.to_string()];
18    if task.allow_interruption {
19        names.push(ASK_USER_TOOL_NAME.to_string());
20    }
21    if task.use_workspace {
22        names.extend(WORKSPACE_TOOLS.into_iter().map(str::to_string));
23    }
24    if task.agent_type.as_deref() == Some("computer") {
25        names.push(BASH_TOOL_NAME.to_string());
26        names.push(CHECK_BACKGROUND_COMMAND_TOOL_NAME.to_string());
27    }
28    if task.sub_agents_enabled() {
29        names.push(CREATE_SUB_TASK_TOOL_NAME.to_string());
30        names.push(SUB_TASK_STATUS_TOOL_NAME.to_string());
31    }
32    if task
33        .metadata
34        .get("available_skills")
35        .is_some_and(is_json_truthy)
36    {
37        names.push(ACTIVATE_SKILL_TOOL_NAME.to_string());
38    }
39    if task.native_multimodal {
40        names.push(READ_IMAGE_TOOL_NAME.to_string());
41    }
42    names.extend(task.extra_tool_names.clone());
43    if !task.exclude_tools.is_empty() {
44        names.retain(|name| !task.exclude_tools.contains(name));
45    }
46
47    let mut deduped = Vec::new();
48    for name in names {
49        if !deduped.contains(&name) {
50            deduped.push(name);
51        }
52    }
53    deduped
54}
55
56pub fn plan_tool_schemas(
57    registry: &ToolRegistry,
58    task: &AgentTask,
59    memory_usage_percentage: Option<u32>,
60) -> Vec<Value> {
61    let names = plan_tool_names(task, memory_usage_percentage);
62    let available_names = names
63        .into_iter()
64        .filter(|name| registry.has_tool(name) && registry.has_schema(name))
65        .collect::<Vec<_>>();
66    let schemas = registry
67        .list_openai_schemas(Some(&available_names))
68        .expect("planned tool names were pre-filtered to registered schemas");
69    patch_dynamic_tool_schema_hints(task, schemas)
70}
71
72pub fn freeze_dynamic_tool_schema_hints(task: &mut AgentTask) {
73    if task.agent_type.as_deref() == Some("computer")
74        || task.extra_tool_names.iter().any(|name| name == "bash")
75    {
76        let hint = build_bash_runtime_hint(task);
77        task.metadata.insert(
78            BASH_RUNTIME_HINT_METADATA_KEY.to_string(),
79            Value::String(hint),
80        );
81    }
82}
83
84pub fn patch_dynamic_tool_schema_hints(task: &AgentTask, tool_schemas: Vec<Value>) -> Vec<Value> {
85    let mut bash_hint = None::<String>;
86    tool_schemas
87        .into_iter()
88        .map(|mut schema| {
89            if schema["function"]["name"].as_str() != Some("bash") {
90                return schema;
91            }
92            let hint = bash_hint.get_or_insert_with(|| build_bash_runtime_hint(task));
93            let base_description = schema["function"]["description"]
94                .as_str()
95                .unwrap_or_default()
96                .trim_end()
97                .to_string();
98            schema["function"]["description"] =
99                Value::String(format!("{base_description}\n\n{hint}").trim().to_string());
100            schema
101        })
102        .collect()
103}
104
105fn build_bash_runtime_hint(task: &AgentTask) -> String {
106    if let Some(cached) = task
107        .metadata
108        .get(BASH_RUNTIME_HINT_METADATA_KEY)
109        .and_then(Value::as_str)
110        .map(str::trim)
111        .filter(|value| !value.is_empty())
112    {
113        return cached.to_string();
114    }
115    let shell = task
116        .metadata
117        .get("bash_shell")
118        .and_then(Value::as_str)
119        .map(str::trim)
120        .filter(|value| !value.is_empty());
121    let windows_shell_priority =
122        match normalize_windows_shell_priority(task.metadata.get("windows_shell_priority")) {
123            Ok(priority) => priority,
124            Err(error) => return format!("Runtime shell hint: invalid shell config. {error}."),
125        };
126    match resolve_shell_invocation(shell, windows_shell_priority.as_deref()) {
127        Ok(resolved) => format!(
128            "Runtime shell hint: commands run via `{}` using prefix `{}`.",
129            resolved.kind,
130            resolved.prefix.join(" ")
131        ),
132        Err(error) => format!("Runtime shell hint: invalid shell config. {error}."),
133    }
134}
135
136fn is_json_truthy(value: &Value) -> bool {
137    match value {
138        Value::Null => false,
139        Value::Bool(value) => *value,
140        Value::Number(value) => value
141            .as_i64()
142            .map(|number| number != 0)
143            .or_else(|| value.as_u64().map(|number| number != 0))
144            .or_else(|| value.as_f64().map(|number| number != 0.0))
145            .unwrap_or(true),
146        Value::String(value) => !value.is_empty(),
147        Value::Array(value) => !value.is_empty(),
148        Value::Object(value) => !value.is_empty(),
149    }
150}