Skip to main content

nanocodex_tools/runtime/
selection.rs

1use super::*;
2
3/// A lazily populated family of Code Mode tools.
4///
5/// Providers start with the agent driver, advertise only their small direct
6/// tool surface initially, and may make additional tools callable at runtime.
7#[async_trait]
8pub trait DynamicToolProvider: Send + Sync {
9    /// Starts background discovery or connection work. Implementations must be idempotent.
10    fn start(&self);
11
12    /// Returns the provider's always-visible tools, such as `tool_search`.
13    fn direct_tools(&self) -> Vec<Arc<dyn Tool>>;
14
15    /// Returns deferred tools currently activated for new Code Mode cells.
16    fn available_definitions(&self) -> Vec<ToolDefinition>;
17
18    /// Returns whether this provider currently exposes `name`.
19    fn contains(&self, name: &str) -> bool {
20        self.available_definitions()
21            .iter()
22            .any(|definition| definition.name() == name)
23    }
24
25    /// Returns whether an activated deferred tool is safe to execute in parallel.
26    ///
27    /// Providers are conservative by default. Implementations must return
28    /// `true` only for a currently activated tool with explicit safety
29    /// metadata.
30    fn supports_parallel_tool_calls(&self, _name: &str) -> bool {
31        false
32    }
33
34    /// Executes an activated deferred tool, or returns `None` when this provider
35    /// does not currently expose `name`.
36    ///
37    /// The owning runtime converts handler panics into a failed `aborted`
38    /// output; they never unwind through the runtime owner.
39    async fn execute(
40        &self,
41        name: &str,
42        input: Value,
43        context: ToolContext<'_>,
44    ) -> Option<ToolOutput>;
45}
46
47/// Declarative selection of the built-in tools installed for an agent.
48#[derive(Clone)]
49pub struct Tools {
50    workspace: bool,
51    web_search: bool,
52    image_generation: bool,
53    pub(super) working_directory: Option<Arc<str>>,
54    pub(super) default_shell: Option<Arc<str>>,
55    process_environment: Arc<Vec<(OsString, OsString)>>,
56    remote_http_client: Option<reqwest::Client>,
57    pub(super) registered: Vec<Arc<dyn Tool>>,
58    pub(super) providers: Vec<Arc<dyn DynamicToolProvider>>,
59}
60
61impl Default for Tools {
62    fn default() -> Self {
63        Self {
64            workspace: true,
65            web_search: true,
66            image_generation: true,
67            working_directory: None,
68            default_shell: None,
69            process_environment: Arc::new(Vec::new()),
70            remote_http_client: None,
71            registered: Vec::new(),
72            providers: Vec::new(),
73        }
74    }
75}
76
77impl fmt::Debug for Tools {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        let remote_http_client_configured = self.remote_http_client.is_some();
80        formatter
81            .debug_struct("Tools")
82            .field("workspace", &self.workspace)
83            .field("web_search", &self.web_search)
84            .field("image_generation", &self.image_generation)
85            .field("working_directory", &self.working_directory)
86            .field("default_shell", &self.default_shell)
87            .field("process_environment_count", &self.process_environment.len())
88            .field(
89                "remote_http_client_configured",
90                &remote_http_client_configured,
91            )
92            .field(
93                "registered",
94                &self
95                    .registered
96                    .iter()
97                    .map(|tool| tool.definition().name().to_owned())
98                    .collect::<Vec<_>>(),
99            )
100            .field("provider_count", &self.providers.len())
101            .finish()
102    }
103}
104
105impl Tools {
106    /// Starts a builder with all standard tools enabled.
107    #[must_use]
108    pub fn builder() -> ToolsBuilder {
109        ToolsBuilder::default()
110    }
111
112    /// Resumes configuring this tool selection while preserving its built-ins,
113    /// registered tools, and dynamic providers.
114    #[must_use]
115    pub const fn into_builder(self) -> ToolsBuilder {
116        ToolsBuilder { tools: self }
117    }
118
119    /// Returns whether the standard workspace tools are enabled.
120    #[must_use]
121    pub const fn workspace_enabled(&self) -> bool {
122        self.workspace
123    }
124
125    /// Returns whether the standard web-search tool is enabled.
126    #[must_use]
127    pub const fn web_search_enabled(&self) -> bool {
128        self.web_search
129    }
130
131    /// Returns whether the standard image-generation tool is enabled.
132    #[must_use]
133    pub const fn image_generation_enabled(&self) -> bool {
134        self.image_generation
135    }
136
137    /// Returns this tool selection bound to one agent session.
138    ///
139    /// Native workspace commands receive the session ID through
140    /// `CODEX_THREAD_ID`. This binding replaces a caller-provided value without
141    /// mutating other clones of the tool selection.
142    #[must_use]
143    pub fn for_session(mut self, session_id: &str) -> Self {
144        self.insert_process_environment(CODEX_THREAD_ID_ENV_VAR.into(), session_id.into());
145        self
146    }
147
148    pub(super) fn process_environment(&self) -> Arc<Vec<(OsString, OsString)>> {
149        Arc::clone(&self.process_environment)
150    }
151
152    fn insert_process_environment(&mut self, name: OsString, value: OsString) {
153        let environment = Arc::make_mut(&mut self.process_environment);
154        environment.retain(|(candidate, _)| candidate != &name);
155        environment.push((name, value));
156    }
157
158    pub(super) fn remote_http_client(&self) -> Option<reqwest::Client> {
159        self.remote_http_client.clone()
160    }
161
162    /// Starts all dynamic providers without waiting for their handshakes.
163    pub fn start_providers(&self) {
164        for provider in &self.providers {
165            provider.start();
166        }
167    }
168}
169
170/// Builder for the built-in tool selection.
171#[derive(Default)]
172pub struct ToolsBuilder {
173    tools: Tools,
174}
175
176/// Invalid declarative tool selection.
177#[derive(Debug, thiserror::Error)]
178pub enum ToolsBuildError {
179    /// A custom definition has an empty registry name.
180    #[error("tool name must not be empty")]
181    EmptyName,
182
183    /// The model-visible working-directory override is empty.
184    #[error("working directory override must not be empty")]
185    EmptyWorkingDirectory,
186
187    /// The model-visible shell override is empty.
188    #[error("default shell override must not be empty")]
189    EmptyDefaultShell,
190
191    /// Two custom tools use the same definition name.
192    #[error("tool name `{0}` is registered more than once")]
193    DuplicateName(Box<str>),
194
195    /// A custom tool collides with an enabled built-in tool.
196    #[error("tool name `{0}` conflicts with an enabled built-in tool")]
197    BuiltInName(Box<str>),
198}
199
200impl ToolsBuilder {
201    /// Starts from an empty built-in tool set.
202    #[must_use]
203    pub const fn without_defaults(mut self) -> Self {
204        self.tools.workspace = false;
205        self.tools.web_search = false;
206        self.tools.image_generation = false;
207        self
208    }
209
210    /// Enables or disables the standard command, patch, plan, and file tools.
211    #[must_use]
212    pub const fn workspace(mut self, enabled: bool) -> Self {
213        self.tools.workspace = enabled;
214        self
215    }
216
217    /// Enables or disables the built-in direct web-search tool.
218    #[must_use]
219    pub const fn web_search(mut self, enabled: bool) -> Self {
220        self.tools.web_search = enabled;
221        self
222    }
223
224    /// Enables or disables the built-in image-generation tool.
225    #[must_use]
226    pub const fn image_generation(mut self, enabled: bool) -> Self {
227        self.tools.image_generation = enabled;
228        self
229    }
230
231    /// Overrides the default working directory described to the model.
232    #[must_use]
233    pub fn working_directory(mut self, directory: impl Into<Arc<str>>) -> Self {
234        self.tools.working_directory = Some(directory.into());
235        self
236    }
237
238    /// Overrides the default shell described to the model.
239    #[must_use]
240    pub fn default_shell(mut self, shell: impl Into<Arc<str>>) -> Self {
241        self.tools.default_shell = Some(shell.into());
242        self
243    }
244
245    /// Adds explicit environment overrides to workspace-tool child processes.
246    ///
247    /// Overrides are scoped to commands spawned by this tool selection and do
248    /// not mutate the embedding process. A later value for the same name wins.
249    #[must_use]
250    pub fn process_environment<I, K, V>(mut self, variables: I) -> Self
251    where
252        I: IntoIterator<Item = (K, V)>,
253        K: Into<OsString>,
254        V: Into<OsString>,
255    {
256        for (name, value) in variables {
257            self.tools
258                .insert_process_environment(name.into(), value.into());
259        }
260        self
261    }
262
263    /// Overrides the HTTP client used by in-process remote tools.
264    #[must_use]
265    pub fn remote_http_client(mut self, client: reqwest::Client) -> Self {
266        self.tools.remote_http_client = Some(client);
267        self
268    }
269
270    /// Adds a function or freeform tool to the runtime.
271    #[must_use]
272    pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
273        self.tools.registered.push(Arc::new(tool));
274        self
275    }
276
277    /// Adds a dynamic family of Code Mode tools.
278    #[must_use]
279    pub fn provider<P: DynamicToolProvider + 'static>(mut self, provider: P) -> Self {
280        let provider: Arc<dyn DynamicToolProvider> = Arc::new(provider);
281        self.tools.registered.extend(provider.direct_tools());
282        self.tools.providers.push(provider);
283        self
284    }
285
286    /// Validates tool names and finishes the runtime configuration.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error for empty, duplicate, or enabled built-in tool names.
291    pub fn build(self) -> Result<Tools, ToolsBuildError> {
292        if self
293            .tools
294            .working_directory
295            .as_deref()
296            .is_some_and(|directory| directory.trim().is_empty())
297        {
298            return Err(ToolsBuildError::EmptyWorkingDirectory);
299        }
300        if self
301            .tools
302            .default_shell
303            .as_deref()
304            .is_some_and(|shell| shell.trim().is_empty())
305        {
306            return Err(ToolsBuildError::EmptyDefaultShell);
307        }
308        let mut names = HashSet::with_capacity(self.tools.registered.len());
309        for tool in &self.tools.registered {
310            let definition = tool.definition();
311            let name = definition.name();
312            if name.is_empty() {
313                return Err(ToolsBuildError::EmptyName);
314            }
315            if built_in_name(&self.tools, name) {
316                return Err(ToolsBuildError::BuiltInName(name.into()));
317            }
318            if !names.insert(name.to_owned()) {
319                return Err(ToolsBuildError::DuplicateName(name.into()));
320            }
321        }
322        Ok(self.tools)
323    }
324}
325
326fn built_in_name(tools: &Tools, name: &str) -> bool {
327    (tools.workspace
328        && matches!(
329            name,
330            "exec_command" | "write_stdin" | "update_plan" | "apply_patch" | "view_image"
331        ))
332        || (tools.web_search && name == "web__run")
333        || (tools.image_generation && name == "image_gen__imagegen")
334}