1use super::*;
2
3#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
5pub enum ToolExposure {
6 #[default]
8 CodeModeOnly,
9 DirectAndCodeMode,
12 DirectOnly,
14 Hidden,
16}
17
18impl ToolExposure {
19 pub(super) const fn is_direct(self) -> bool {
20 matches!(self, Self::DirectAndCodeMode | Self::DirectOnly)
21 }
22
23 pub(super) const fn is_available_in_code_mode(self) -> bool {
24 matches!(self, Self::CodeModeOnly | Self::DirectAndCodeMode)
25 }
26}
27
28#[derive(Clone)]
29pub(super) struct RegisteredTool {
30 pub(super) handler: Arc<dyn Tool>,
31 pub(super) exposure: Option<ToolExposure>,
32}
33
34#[async_trait]
39pub trait DynamicToolProvider: Send + Sync {
40 fn start(&self);
42
43 fn direct_tools(&self) -> Vec<Arc<dyn Tool>>;
45
46 fn direct_tools_for_exposure(&self, _exposure: ToolExposure) -> Vec<Arc<dyn Tool>> {
52 self.direct_tools()
53 }
54
55 fn available_definitions(&self) -> Vec<ToolDefinition>;
57
58 fn code_mode_tool_summaries(&self) -> Vec<(String, String)> {
65 Vec::new()
66 }
67
68 fn contains(&self, name: &str) -> bool {
70 self.available_definitions()
71 .iter()
72 .any(|definition| definition.name() == name)
73 }
74
75 fn supports_parallel_tool_calls(&self, _name: &str) -> bool {
81 false
82 }
83
84 async fn execute(
90 &self,
91 name: &str,
92 input: Value,
93 context: ToolContext<'_>,
94 ) -> Option<ToolOutput>;
95}
96
97#[derive(Clone)]
99pub struct Tools {
100 exposure: ToolExposure,
101 workspace: bool,
102 web_search: bool,
103 image_generation: bool,
104 pub(super) working_directory: Option<Arc<str>>,
105 pub(super) default_shell: Option<Arc<str>>,
106 process_environment: Arc<Vec<(OsString, OsString)>>,
107 remote_http_client: Option<reqwest::Client>,
108 pub(super) registered: Vec<RegisteredTool>,
109 pub(super) provider_direct: Vec<Arc<dyn Tool>>,
110 pub(super) providers: Vec<Arc<dyn DynamicToolProvider>>,
111 pub(super) deferred_tools_guidance_enabled: bool,
112}
113
114impl Default for Tools {
115 fn default() -> Self {
116 Self {
117 exposure: ToolExposure::default(),
118 workspace: true,
119 web_search: true,
120 image_generation: true,
121 working_directory: None,
122 default_shell: None,
123 process_environment: Arc::new(Vec::new()),
124 remote_http_client: None,
125 registered: Vec::new(),
126 provider_direct: Vec::new(),
127 providers: Vec::new(),
128 deferred_tools_guidance_enabled: false,
129 }
130 }
131}
132
133impl fmt::Debug for Tools {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 let remote_http_client_configured = self.remote_http_client.is_some();
136 formatter
137 .debug_struct("Tools")
138 .field("exposure", &self.exposure)
139 .field("workspace", &self.workspace)
140 .field("web_search", &self.web_search)
141 .field("image_generation", &self.image_generation)
142 .field("working_directory", &self.working_directory)
143 .field("default_shell", &self.default_shell)
144 .field("process_environment_count", &self.process_environment.len())
145 .field(
146 "remote_http_client_configured",
147 &remote_http_client_configured,
148 )
149 .field(
150 "registered",
151 &self
152 .registered
153 .iter()
154 .map(|tool| tool.handler.definition().name().to_owned())
155 .collect::<Vec<_>>(),
156 )
157 .field(
158 "provider_direct",
159 &self
160 .provider_direct
161 .iter()
162 .map(|tool| tool.definition().name().to_owned())
163 .collect::<Vec<_>>(),
164 )
165 .field("provider_count", &self.providers.len())
166 .finish()
167 }
168}
169
170impl Tools {
171 #[must_use]
173 pub fn builder() -> ToolsBuilder {
174 ToolsBuilder::default()
175 }
176
177 #[must_use]
180 pub const fn into_builder(self) -> ToolsBuilder {
181 ToolsBuilder { tools: self }
182 }
183
184 #[must_use]
186 pub const fn exposure(&self) -> ToolExposure {
187 self.exposure
188 }
189
190 #[must_use]
192 pub const fn workspace_enabled(&self) -> bool {
193 self.workspace
194 }
195
196 #[must_use]
198 pub const fn web_search_enabled(&self) -> bool {
199 self.web_search
200 }
201
202 #[must_use]
204 pub const fn image_generation_enabled(&self) -> bool {
205 self.image_generation
206 }
207
208 #[must_use]
214 pub fn for_session(mut self, session_id: &str) -> Self {
215 self.insert_process_environment(CODEX_THREAD_ID_ENV_VAR.into(), session_id.into());
216 self
217 }
218
219 pub(super) fn process_environment(&self) -> Arc<Vec<(OsString, OsString)>> {
220 Arc::clone(&self.process_environment)
221 }
222
223 fn insert_process_environment(&mut self, name: OsString, value: OsString) {
224 let environment = Arc::make_mut(&mut self.process_environment);
225 environment.retain(|(candidate, _)| candidate != &name);
226 environment.push((name, value));
227 }
228
229 pub(super) fn remote_http_client(&self) -> Option<reqwest::Client> {
230 self.remote_http_client.clone()
231 }
232
233 pub fn start_providers(&self) {
235 for provider in &self.providers {
236 provider.start();
237 }
238 }
239}
240
241#[derive(Default)]
243pub struct ToolsBuilder {
244 tools: Tools,
245}
246
247#[derive(Debug, thiserror::Error)]
249pub enum ToolsBuildError {
250 #[error("tool name must not be empty")]
252 EmptyName,
253
254 #[error("working directory override must not be empty")]
256 EmptyWorkingDirectory,
257
258 #[error("default shell override must not be empty")]
260 EmptyDefaultShell,
261
262 #[error("tool name `{0}` is registered more than once")]
264 DuplicateName(Box<str>),
265
266 #[error("tool name `{0}` conflicts with an enabled built-in tool")]
268 BuiltInName(Box<str>),
269
270 #[error("tool name `{0}` is reserved by the Code Mode host")]
272 ReservedName(Box<str>),
273}
274
275impl ToolsBuilder {
276 #[must_use]
282 pub const fn exposure(mut self, exposure: ToolExposure) -> Self {
283 self.tools.exposure = exposure;
284 self
285 }
286
287 #[must_use]
289 pub const fn without_defaults(mut self) -> Self {
290 self.tools.workspace = false;
291 self.tools.web_search = false;
292 self.tools.image_generation = false;
293 self
294 }
295
296 #[must_use]
298 pub const fn workspace(mut self, enabled: bool) -> Self {
299 self.tools.workspace = enabled;
300 self
301 }
302
303 #[must_use]
305 pub const fn web_search(mut self, enabled: bool) -> Self {
306 self.tools.web_search = enabled;
307 self
308 }
309
310 #[must_use]
312 pub const fn image_generation(mut self, enabled: bool) -> Self {
313 self.tools.image_generation = enabled;
314 self
315 }
316
317 #[must_use]
319 pub fn working_directory(mut self, directory: impl Into<Arc<str>>) -> Self {
320 self.tools.working_directory = Some(directory.into());
321 self
322 }
323
324 #[must_use]
326 pub fn default_shell(mut self, shell: impl Into<Arc<str>>) -> Self {
327 self.tools.default_shell = Some(shell.into());
328 self
329 }
330
331 #[must_use]
336 pub fn process_environment<I, K, V>(mut self, variables: I) -> Self
337 where
338 I: IntoIterator<Item = (K, V)>,
339 K: Into<OsString>,
340 V: Into<OsString>,
341 {
342 for (name, value) in variables {
343 self.tools
344 .insert_process_environment(name.into(), value.into());
345 }
346 self
347 }
348
349 #[must_use]
351 pub fn remote_http_client(mut self, client: reqwest::Client) -> Self {
352 self.tools.remote_http_client = Some(client);
353 self
354 }
355
356 #[must_use]
358 pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
359 self.tools.registered.push(RegisteredTool {
360 handler: Arc::new(tool),
361 exposure: None,
362 });
363 self
364 }
365
366 #[must_use]
368 pub fn tool_with_exposure<T: Tool + 'static>(
369 mut self,
370 tool: T,
371 exposure: ToolExposure,
372 ) -> Self {
373 self.tools.registered.push(RegisteredTool {
374 handler: Arc::new(tool),
375 exposure: Some(exposure),
376 });
377 self
378 }
379
380 #[must_use]
382 pub fn provider<P: DynamicToolProvider + 'static>(mut self, provider: P) -> Self {
383 let provider: Arc<dyn DynamicToolProvider> = Arc::new(provider);
384 self.tools.providers.push(provider);
385 self.refresh_provider_direct();
386 self
387 }
388
389 pub fn build(mut self) -> Result<Tools, ToolsBuildError> {
395 self.refresh_provider_direct();
396 if self
397 .tools
398 .working_directory
399 .as_deref()
400 .is_some_and(|directory| directory.trim().is_empty())
401 {
402 return Err(ToolsBuildError::EmptyWorkingDirectory);
403 }
404 if self
405 .tools
406 .default_shell
407 .as_deref()
408 .is_some_and(|shell| shell.trim().is_empty())
409 {
410 return Err(ToolsBuildError::EmptyDefaultShell);
411 }
412 let mut names = HashSet::with_capacity(
413 self.tools
414 .registered
415 .len()
416 .saturating_add(self.tools.provider_direct.len()),
417 );
418 for tool in &self.tools.registered {
419 let definition = tool.handler.definition();
420 let name = definition.name();
421 if name.is_empty() {
422 return Err(ToolsBuildError::EmptyName);
423 }
424 if host_owned_name(name)
425 || (name == "tool_search"
426 && !matches!(definition, ToolDefinition::ToolSearch { .. }))
427 {
428 return Err(ToolsBuildError::ReservedName(name.into()));
429 }
430 if built_in_name(&self.tools, name) {
431 return Err(ToolsBuildError::BuiltInName(name.into()));
432 }
433 if !names.insert(name.to_owned()) {
434 return Err(ToolsBuildError::DuplicateName(name.into()));
435 }
436 }
437 for tool in &self.tools.provider_direct {
438 let definition = tool.definition();
439 let name = definition.name();
440 if name.is_empty() {
441 return Err(ToolsBuildError::EmptyName);
442 }
443 if host_owned_name(name) {
444 return Err(ToolsBuildError::ReservedName(name.into()));
445 }
446 if built_in_name(&self.tools, name) {
447 return Err(ToolsBuildError::BuiltInName(name.into()));
448 }
449 if !names.insert(name.to_owned()) {
450 return Err(ToolsBuildError::DuplicateName(name.into()));
451 }
452 }
453 Ok(self.tools)
454 }
455
456 fn refresh_provider_direct(&mut self) {
457 self.tools.deferred_tools_guidance_enabled = self.tools.providers.iter().any(|provider| {
458 provider
459 .direct_tools()
460 .iter()
461 .any(|tool| matches!(tool.definition(), ToolDefinition::ToolSearch { .. }))
462 });
463 self.tools.provider_direct = self
464 .tools
465 .providers
466 .iter()
467 .flat_map(|provider| provider.direct_tools_for_exposure(self.tools.exposure))
468 .collect();
469 }
470}
471
472fn built_in_name(tools: &Tools, name: &str) -> bool {
473 (tools.workspace
474 && matches!(
475 name,
476 "exec_command" | "write_stdin" | "update_plan" | "apply_patch" | "view_image"
477 ))
478 || (tools.web_search && name == "web__run")
479 || (tools.image_generation && name == "image_gen__imagegen")
480}