Skip to main content

vv_agent/tools/base/
context.rs

1use 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}