vv_agent/runtime/
tool_planner.rs1use 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}