vv_agent/tools/base/
context.rs1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use serde_json::Value;
6
7use super::paths::resolve_workspace_path_checked;
8use super::SubTaskRunner;
9use crate::model::ModelProvider;
10use crate::types::{ToolArguments, ToolCall};
11use crate::workspace::{
12 DiscoveryFilteredWorkspaceBackend, LocalWorkspaceBackend, WorkspaceBackend,
13};
14use crate::{RunConfig, RunContext};
15
16#[derive(Clone)]
17pub struct ToolContext {
18 pub workspace: PathBuf,
19 pub shared_state: BTreeMap<String, Value>,
20 pub cycle_index: u32,
21 pub task_id: String,
22 pub tool_call_id: String,
23 pub tool_name: String,
24 pub arguments: ToolArguments,
25 pub idempotency_key: Option<String>,
26 pub metadata: BTreeMap<String, Value>,
27 pub app_state: Option<Arc<dyn std::any::Any + Send + Sync>>,
28 pub workspace_backend: Arc<dyn WorkspaceBackend>,
29 pub model_provider: Option<Arc<dyn ModelProvider>>,
30 pub run_context: Option<RunContext>,
31 pub sub_task_runner: Option<SubTaskRunner>,
32 pub sub_task_manager: Option<crate::runtime::sub_task_manager::SubTaskManager>,
33 pub sub_task_turn_snapshot: Option<crate::runtime::sub_task_manager::SubTaskTurnSnapshot>,
34 pub execution_backend: Option<crate::runtime::backends::RuntimeExecutionBackend>,
35 #[doc(hidden)]
36 pub background_parent_run_config: Option<RunConfig>,
37}
38
39impl std::fmt::Debug for ToolContext {
40 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 formatter
42 .debug_struct("ToolContext")
43 .field("workspace", &self.workspace)
44 .field("shared_state", &self.shared_state)
45 .field("cycle_index", &self.cycle_index)
46 .field("task_id", &self.task_id)
47 .field("tool_call_id", &self.tool_call_id)
48 .field("tool_name", &self.tool_name)
49 .field("arguments", &self.arguments)
50 .field("idempotency_key", &self.idempotency_key)
51 .field("metadata", &self.metadata)
52 .field("has_app_state", &self.app_state.is_some())
53 .field("has_model_provider", &self.model_provider.is_some())
54 .field("has_run_context", &self.run_context.is_some())
55 .field("has_sub_task_runner", &self.sub_task_runner.is_some())
56 .field("has_sub_task_manager", &self.sub_task_manager.is_some())
57 .field(
58 "has_sub_task_turn_snapshot",
59 &self.sub_task_turn_snapshot.is_some(),
60 )
61 .field("has_execution_backend", &self.execution_backend.is_some())
62 .field(
63 "has_background_parent_run_config",
64 &self.background_parent_run_config.is_some(),
65 )
66 .finish_non_exhaustive()
67 }
68}
69
70impl ToolContext {
71 pub fn new(workspace: impl Into<PathBuf>) -> Self {
72 let workspace = workspace.into();
73 Self {
74 workspace: workspace.clone(),
75 shared_state: BTreeMap::new(),
76 cycle_index: 0,
77 task_id: String::new(),
78 tool_call_id: String::new(),
79 tool_name: String::new(),
80 arguments: ToolArguments::new(),
81 idempotency_key: None,
82 metadata: BTreeMap::new(),
83 app_state: None,
84 workspace_backend: Arc::new(crate::workspace::LocalWorkspaceBackend::new(workspace)),
85 model_provider: None,
86 run_context: None,
87 sub_task_runner: None,
88 sub_task_manager: None,
89 sub_task_turn_snapshot: None,
90 execution_backend: None,
91 background_parent_run_config: None,
92 }
93 }
94
95 pub fn allow_outside_workspace_paths(&self) -> bool {
96 for key in [
97 "allow_outside_workspace_paths",
98 "allow_outside_workspace",
99 "workspace_allow_outside_main",
100 "workspace_allow_outside",
101 ] {
102 if let Some(parsed) = parse_bool(self.metadata.get(key)) {
103 return parsed;
104 }
105 }
106 false
107 }
108
109 pub fn begin_tool_call(&mut self, call: &ToolCall) {
110 self.tool_call_id = call.id.clone();
111 self.tool_name = call.name.clone();
112 self.arguments = call.arguments.clone();
113 if let Some(snapshot) = self.sub_task_turn_snapshot.as_mut() {
114 snapshot.parent_tool_call_id = (!call.id.trim().is_empty()).then(|| call.id.clone());
115 }
116 }
117
118 pub fn app_state<T: Send + Sync + 'static>(&self) -> Option<&T> {
119 self.app_state.as_ref()?.downcast_ref::<T>()
120 }
121
122 pub fn resolve_workspace_path(&self, raw_path: &str) -> Result<PathBuf, String> {
123 resolve_workspace_path_checked(
124 &self.workspace,
125 raw_path,
126 self.allow_outside_workspace_paths(),
127 )
128 }
129
130 pub fn effective_workspace_backend(&self) -> Arc<dyn WorkspaceBackend> {
131 if self.allow_outside_workspace_paths() {
132 if let Some(filtered) = self
133 .workspace_backend
134 .as_any()
135 .downcast_ref::<DiscoveryFilteredWorkspaceBackend>()
136 {
137 if let Some(local) = filtered
138 .inner()
139 .as_any()
140 .downcast_ref::<LocalWorkspaceBackend>()
141 {
142 let mut local = local.clone();
143 local.allow_outside_root = true;
144 return Arc::new(
145 DiscoveryFilteredWorkspaceBackend::new(Arc::new(local), filtered.pattern())
146 .expect("existing discovery filter remains valid"),
147 );
148 }
149 }
150 if let Some(local) = self
151 .workspace_backend
152 .as_any()
153 .downcast_ref::<LocalWorkspaceBackend>()
154 {
155 let mut local = local.clone();
156 local.allow_outside_root = true;
157 return Arc::new(local);
158 }
159 }
160 self.workspace_backend.clone()
161 }
162}
163
164fn parse_bool(value: Option<&Value>) -> Option<bool> {
165 match value {
166 Some(Value::Bool(value)) => Some(*value),
167 Some(Value::Number(number)) => number.as_i64().and_then(|value| match value {
168 0 => Some(false),
169 1 => Some(true),
170 _ => None,
171 }),
172 Some(Value::String(value)) => match value.trim().to_ascii_lowercase().as_str() {
173 "1" | "true" | "yes" | "on" => Some(true),
174 "0" | "false" | "no" | "off" => Some(false),
175 _ => None,
176 },
177 _ => None,
178 }
179}