1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use futures_util::{StreamExt, stream};
5use tea_control::CancellationScope;
6#[cfg(feature = "model-projection")]
7use tea_model::{HostedToolOptions, ModelRequestError, ModelSpec, ModelToolDefinition};
8#[cfg(feature = "model-projection")]
9use tea_protocol::ModelId;
10use thiserror::Error;
11
12use crate::{
13 BoxToolExecutionStream, CompiledToolSchema, SchemaCompilationError, SchemaValidationFailure,
14 ToolExecutionEvent, ToolExecutionFailure, ToolExecutor, ToolInvocation, ToolName,
15 ToolResourceError, ToolResourceResolver, ToolSpec, ValidatedToolInvocation,
16};
17use tea_protocol::ToolPresentation;
18
19#[derive(Debug)]
20struct RegisteredTool {
21 spec: Arc<ToolSpec>,
22 input: CompiledToolSchema,
23 output: CompiledToolSchema,
24 binding: ToolBinding,
25}
26
27type ClientBindingRefs<'a> = (&'a Arc<dyn ToolResourceResolver>, &'a Arc<dyn ToolExecutor>);
28
29#[cfg(feature = "model-projection")]
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ToolRoutePreference {
33 PreferHosted,
35 ForceClient,
37}
38
39#[derive(Debug, Clone)]
41pub enum ToolBinding {
42 Client {
44 resolver: Arc<dyn ToolResourceResolver>,
46 executor: Arc<dyn ToolExecutor>,
48 },
49 #[cfg(feature = "model-projection")]
51 Hosted {
52 options: HostedToolOptions,
54 },
55 #[cfg(feature = "model-projection")]
57 Hybrid {
58 options: HostedToolOptions,
60 resolver: Arc<dyn ToolResourceResolver>,
62 executor: Arc<dyn ToolExecutor>,
64 preference: ToolRoutePreference,
66 },
67}
68
69impl ToolBinding {
70 #[must_use]
72 pub fn client(
73 resolver: Arc<dyn ToolResourceResolver>,
74 executor: Arc<dyn ToolExecutor>,
75 ) -> Self {
76 Self::Client { resolver, executor }
77 }
78
79 #[cfg(feature = "model-projection")]
81 #[must_use]
82 pub const fn hosted(options: HostedToolOptions) -> Self {
83 Self::Hosted { options }
84 }
85
86 #[cfg(feature = "model-projection")]
88 #[must_use]
89 pub fn hybrid(
90 options: HostedToolOptions,
91 preference: ToolRoutePreference,
92 resolver: Arc<dyn ToolResourceResolver>,
93 executor: Arc<dyn ToolExecutor>,
94 ) -> Self {
95 Self::Hybrid {
96 options,
97 resolver,
98 executor,
99 preference,
100 }
101 }
102
103 #[must_use]
105 pub const fn has_client_execution(&self) -> bool {
106 match self {
107 Self::Client { .. } => true,
108 #[cfg(feature = "model-projection")]
109 Self::Hosted { .. } => false,
110 #[cfg(feature = "model-projection")]
111 Self::Hybrid { .. } => true,
112 }
113 }
114
115 fn client_parts(&self) -> Option<ClientBindingRefs<'_>> {
116 match self {
117 Self::Client { resolver, executor } => Some((resolver, executor)),
118 #[cfg(feature = "model-projection")]
119 Self::Hosted { .. } => None,
120 #[cfg(feature = "model-projection")]
121 Self::Hybrid {
122 resolver, executor, ..
123 } => Some((resolver, executor)),
124 }
125 }
126
127 #[cfg(feature = "model-projection")]
128 const fn hosted_options(&self) -> Option<&HostedToolOptions> {
129 match self {
130 Self::Client { .. } => None,
131 Self::Hosted { options } | Self::Hybrid { options, .. } => Some(options),
132 }
133 }
134}
135
136#[derive(Debug, Default)]
138pub struct ToolRegistry {
139 tools: BTreeMap<ToolName, RegisteredTool>,
140}
141
142impl ToolRegistry {
143 #[must_use]
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn register(
155 &mut self,
156 spec: ToolSpec,
157 resolver: Arc<dyn ToolResourceResolver>,
158 executor: Arc<dyn ToolExecutor>,
159 ) -> Result<(), ToolRegistryError> {
160 self.register_binding(spec, ToolBinding::client(resolver, executor))
161 }
162
163 pub fn register_binding(
170 &mut self,
171 spec: ToolSpec,
172 binding: ToolBinding,
173 ) -> Result<(), ToolRegistryError> {
174 if let Some(existing) = self.tools.get(spec.name()) {
175 return if existing.spec.version() == spec.version() {
176 Err(ToolRegistryError::DuplicateTool)
177 } else {
178 Err(ToolRegistryError::VersionConflict)
179 };
180 }
181 #[cfg(feature = "model-projection")]
182 if binding
183 .hosted_options()
184 .is_some_and(|options| spec.name().as_str() != options.kind().name())
185 {
186 return Err(ToolRegistryError::HostedToolNameMismatch);
187 }
188 let input = CompiledToolSchema::compile(spec.input_schema().clone())?;
189 let output = CompiledToolSchema::compile(spec.output_schema().clone())?;
190 let name = spec.name().clone();
191 self.tools.insert(
192 name,
193 RegisteredTool {
194 spec: Arc::new(spec),
195 input,
196 output,
197 binding,
198 },
199 );
200 Ok(())
201 }
202
203 #[cfg(feature = "model-projection")]
209 pub fn register_hosted(
210 &mut self,
211 spec: ToolSpec,
212 options: HostedToolOptions,
213 ) -> Result<(), ToolRegistryError> {
214 self.register_binding(spec, ToolBinding::hosted(options))
215 }
216
217 #[cfg(feature = "model-projection")]
223 pub fn register_hybrid(
224 &mut self,
225 spec: ToolSpec,
226 options: HostedToolOptions,
227 preference: ToolRoutePreference,
228 resolver: Arc<dyn ToolResourceResolver>,
229 executor: Arc<dyn ToolExecutor>,
230 ) -> Result<(), ToolRegistryError> {
231 self.register_binding(
232 spec,
233 ToolBinding::hybrid(options, preference, resolver, executor),
234 )
235 }
236
237 pub fn names(&self) -> impl Iterator<Item = &ToolName> {
239 self.tools.keys()
240 }
241
242 pub fn specs(&self) -> impl Iterator<Item = &ToolSpec> {
244 self.tools.values().map(|tool| tool.spec.as_ref())
245 }
246
247 #[cfg(feature = "model-projection")]
254 pub fn model_definitions(
255 &self,
256 model: &ModelSpec,
257 ) -> Result<Vec<ModelToolDefinition>, ToolRegistryError> {
258 self.tools
259 .values()
260 .map(|tool| project_model_definition(tool, model))
261 .collect()
262 }
263
264 pub fn validate(
270 &self,
271 invocation: ToolInvocation,
272 ) -> Result<ValidatedToolInvocation, ToolRegistryError> {
273 let registered = self
274 .tools
275 .get(invocation.name())
276 .ok_or(ToolRegistryError::UnknownTool)?;
277 let (resolver, _) = registered
278 .binding
279 .client_parts()
280 .ok_or(ToolRegistryError::HostedToolNotClientExecutable)?;
281 registered
282 .input
283 .validate(invocation.arguments())
284 .map_err(ToolRegistryError::InvalidArguments)?;
285 let mut resources = resolver.resolve(invocation.name(), invocation.arguments())?;
286 resources.sort();
287 resources.dedup();
288 if resources.len() > crate::MAX_TOOL_RESOURCES {
289 return Err(ToolRegistryError::Resources(
290 ToolResourceError::TooManyResources,
291 ));
292 }
293 Ok(ValidatedToolInvocation::new(
294 invocation,
295 Arc::clone(®istered.spec),
296 resources,
297 ))
298 }
299
300 pub fn execute(
307 &self,
308 invocation: ToolInvocation,
309 cancellation: CancellationScope,
310 ) -> Result<BoxToolExecutionStream, ToolRegistryError> {
311 let validated = self.validate(invocation)?;
312 self.execute_validated(validated, cancellation)
313 }
314
315 pub fn execute_validated(
326 &self,
327 invocation: ValidatedToolInvocation,
328 cancellation: CancellationScope,
329 ) -> Result<BoxToolExecutionStream, ToolRegistryError> {
330 let registered = self
331 .tools
332 .get(invocation.name())
333 .filter(|tool| tool.spec.as_ref() == invocation.spec())
334 .ok_or(ToolRegistryError::UnknownTool)?;
335 let (_, executor) = registered
336 .binding
337 .client_parts()
338 .ok_or(ToolRegistryError::HostedToolNotClientExecutable)?;
339 let upstream = executor.execute(invocation, cancellation);
340 let output = registered.output.clone();
341 let state = ExecutionValidationState {
342 upstream,
343 output,
344 done: false,
345 };
346 Ok(Box::pin(stream::unfold(state, |mut state| async move {
347 if state.done {
348 return None;
349 }
350 let Some(event) = state.upstream.next().await else {
351 state.done = true;
352 return Some((
353 ToolExecutionEvent::Failed(ToolExecutionFailure::internal_contract()),
354 state,
355 ));
356 };
357 let event = match event {
358 ToolExecutionEvent::Finished(result) => {
359 state.done = true;
360 if state.output.validate(result.output()).is_ok() {
361 ToolExecutionEvent::Finished(result)
362 } else {
363 ToolExecutionEvent::Failed(ToolExecutionFailure::invalid_output())
364 }
365 }
366 ToolExecutionEvent::Failed(failure) => {
367 state.done = true;
368 ToolExecutionEvent::Failed(failure)
369 }
370 ToolExecutionEvent::Progress(progress) => ToolExecutionEvent::Progress(progress),
371 };
372 Some((event, state))
373 })))
374 }
375
376 #[must_use]
383 pub fn preview_validated(
384 &self,
385 invocation: &ValidatedToolInvocation,
386 ) -> Option<ToolPresentation> {
387 self.tools
388 .get(invocation.name())
389 .filter(|tool| tool.spec.as_ref() == invocation.spec())
390 .and_then(|tool| tool.binding.client_parts())
391 .and_then(|(_, executor)| executor.preview(invocation))
392 }
393}
394
395#[cfg(feature = "model-projection")]
396fn project_model_definition(
397 tool: &RegisteredTool,
398 model: &ModelSpec,
399) -> Result<ModelToolDefinition, ToolRegistryError> {
400 let capabilities = model.capabilities();
401 match &tool.binding {
402 ToolBinding::Client { .. } => {
403 if capabilities.supports_tools() {
404 function_definition(tool)
405 } else {
406 Err(no_supported_tool_route(tool, model))
407 }
408 }
409 ToolBinding::Hosted { options } => {
410 if capabilities.supports_hosted_tool(options.kind()) {
411 hosted_definition(tool, options)
412 } else {
413 Err(no_supported_tool_route(tool, model))
414 }
415 }
416 ToolBinding::Hybrid {
417 options,
418 preference,
419 ..
420 } => {
421 let select_hosted = matches!(preference, ToolRoutePreference::PreferHosted)
422 && capabilities.supports_hosted_tool(options.kind());
423 if select_hosted {
424 hosted_definition(tool, options)
425 } else if capabilities.supports_tools() {
426 function_definition(tool)
427 } else {
428 Err(no_supported_tool_route(tool, model))
429 }
430 }
431 }
432}
433
434#[cfg(feature = "model-projection")]
435fn no_supported_tool_route(tool: &RegisteredTool, model: &ModelSpec) -> ToolRegistryError {
436 ToolRegistryError::NoSupportedToolRoute {
437 tool: tool.spec.name().clone(),
438 model: model.model_id().clone(),
439 }
440}
441
442#[cfg(feature = "model-projection")]
443fn function_definition(tool: &RegisteredTool) -> Result<ModelToolDefinition, ToolRegistryError> {
444 ModelToolDefinition::new(
445 tool.spec.name().as_str(),
446 tool.spec.description(),
447 tool.spec.input_schema().clone(),
448 )
449 .map_err(ToolRegistryError::ModelProjection)
450}
451
452#[cfg(feature = "model-projection")]
453fn hosted_definition(
454 tool: &RegisteredTool,
455 options: &HostedToolOptions,
456) -> Result<ModelToolDefinition, ToolRegistryError> {
457 ModelToolDefinition::hosted(
458 tool.spec.description(),
459 tool.spec.input_schema().clone(),
460 options.clone(),
461 )
462 .map_err(ToolRegistryError::ModelProjection)
463}
464
465struct ExecutionValidationState {
466 upstream: BoxToolExecutionStream,
467 output: CompiledToolSchema,
468 done: bool,
469}
470
471#[derive(Debug, PartialEq, Eq, Error)]
473pub enum ToolRegistryError {
474 #[error("tool is not registered")]
476 UnknownTool,
477 #[error("tool is already registered")]
479 DuplicateTool,
480 #[error("tool name has a version conflict")]
482 VersionConflict,
483 #[cfg(feature = "model-projection")]
485 #[error("hosted tool name does not match its capability kind")]
486 HostedToolNameMismatch,
487 #[cfg(feature = "model-projection")]
489 #[error(
490 "active tool {tool} has no execution route supported by selected model {model}; declare the model capability or configure a supported client route"
491 )]
492 NoSupportedToolRoute {
493 tool: ToolName,
495 model: ModelId,
497 },
498 #[error("hosted tool has no client execution route")]
500 HostedToolNotClientExecutable,
501 #[cfg(feature = "model-projection")]
503 #[error("tool cannot be projected into the model request: {0}")]
504 ModelProjection(ModelRequestError),
505 #[error("tool schema cannot compile: {0}")]
507 Schema(#[from] SchemaCompilationError),
508 #[error("tool arguments are invalid: {0}")]
510 InvalidArguments(SchemaValidationFailure),
511 #[error("tool resource resolution failed: {0}")]
513 Resources(#[from] ToolResourceError),
514}