nanocodex_tools/runtime/
selection.rs1use super::*;
2
3#[async_trait]
8pub trait DynamicToolProvider: Send + Sync {
9 fn start(&self);
11
12 fn direct_tools(&self) -> Vec<Arc<dyn Tool>>;
14
15 fn available_definitions(&self) -> Vec<ToolDefinition>;
17
18 fn contains(&self, name: &str) -> bool {
20 self.available_definitions()
21 .iter()
22 .any(|definition| definition.name() == name)
23 }
24
25 fn supports_parallel_tool_calls(&self, _name: &str) -> bool {
31 false
32 }
33
34 async fn execute(
40 &self,
41 name: &str,
42 input: Value,
43 context: ToolContext<'_>,
44 ) -> Option<ToolOutput>;
45}
46
47#[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 #[must_use]
108 pub fn builder() -> ToolsBuilder {
109 ToolsBuilder::default()
110 }
111
112 #[must_use]
115 pub const fn into_builder(self) -> ToolsBuilder {
116 ToolsBuilder { tools: self }
117 }
118
119 #[must_use]
121 pub const fn workspace_enabled(&self) -> bool {
122 self.workspace
123 }
124
125 #[must_use]
127 pub const fn web_search_enabled(&self) -> bool {
128 self.web_search
129 }
130
131 #[must_use]
133 pub const fn image_generation_enabled(&self) -> bool {
134 self.image_generation
135 }
136
137 #[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 pub fn start_providers(&self) {
164 for provider in &self.providers {
165 provider.start();
166 }
167 }
168}
169
170#[derive(Default)]
172pub struct ToolsBuilder {
173 tools: Tools,
174}
175
176#[derive(Debug, thiserror::Error)]
178pub enum ToolsBuildError {
179 #[error("tool name must not be empty")]
181 EmptyName,
182
183 #[error("working directory override must not be empty")]
185 EmptyWorkingDirectory,
186
187 #[error("default shell override must not be empty")]
189 EmptyDefaultShell,
190
191 #[error("tool name `{0}` is registered more than once")]
193 DuplicateName(Box<str>),
194
195 #[error("tool name `{0}` conflicts with an enabled built-in tool")]
197 BuiltInName(Box<str>),
198}
199
200impl ToolsBuilder {
201 #[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 #[must_use]
212 pub const fn workspace(mut self, enabled: bool) -> Self {
213 self.tools.workspace = enabled;
214 self
215 }
216
217 #[must_use]
219 pub const fn web_search(mut self, enabled: bool) -> Self {
220 self.tools.web_search = enabled;
221 self
222 }
223
224 #[must_use]
226 pub const fn image_generation(mut self, enabled: bool) -> Self {
227 self.tools.image_generation = enabled;
228 self
229 }
230
231 #[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 #[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 #[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 #[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 #[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 #[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 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}