Skip to main content

lash_sansio/
tool_contract.rs

1/// How a tool's invocations should be scheduled relative to other tools in
2/// the same batch of model-produced tool calls.
3///
4/// Tools that only *read* state (`read_file`, `grep`, `glob`, ...) can run
5/// in parallel safely and should use the default [`ToolScheduling::Parallel`].
6/// Tools that *mutate* shared state (`apply_patch`, `exec_command`,
7/// `write_stdin`) should declare
8/// [`ToolScheduling::Serial`] so the dispatcher runs them one-at-a-time
9/// and avoids interleaving with each other.
10///
11/// This controls scheduling within a batch of tool calls; protocol ownership
12/// is selected by the host plugin stack.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ToolScheduling {
16    /// Safe to run concurrently with other parallel tools in the same batch.
17    #[default]
18    Parallel,
19    /// Must run one-at-a-time relative to other serial tools in the batch.
20    Serial,
21}
22
23fn default_tool_scheduling() -> ToolScheduling {
24    ToolScheduling::default()
25}
26
27fn is_default_tool_scheduling(mode: &ToolScheduling) -> bool {
28    *mode == ToolScheduling::default()
29}
30
31/// Automatic retry policy for a tool's execution.
32///
33/// This is intentionally separate from [`ToolScheduling`]: scheduling
34/// decides whether different tool calls may run together, while retry policy
35/// decides whether one failed call may be attempted again inside its scheduled
36/// slot.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum ToolRetryPolicy {
40    /// Never retry automatically. This is the default for every tool.
41    #[default]
42    Never,
43    /// Retry only failures that explicitly report a safe retry disposition.
44    Safe {
45        max_attempts: u32,
46        base_delay_ms: u64,
47        max_delay_ms: u64,
48    },
49    /// Retry only failures that explicitly report a safe retry disposition,
50    /// and only when the runtime can provide a stable replay key.
51    Idempotent {
52        max_attempts: u32,
53        base_delay_ms: u64,
54        max_delay_ms: u64,
55    },
56}
57
58impl ToolRetryPolicy {
59    pub fn safe(max_attempts: u32, base_delay_ms: u64, max_delay_ms: u64) -> Self {
60        Self::Safe {
61            max_attempts,
62            base_delay_ms,
63            max_delay_ms,
64        }
65    }
66
67    pub fn idempotent(max_attempts: u32, base_delay_ms: u64, max_delay_ms: u64) -> Self {
68        Self::Idempotent {
69            max_attempts,
70            base_delay_ms,
71            max_delay_ms,
72        }
73    }
74
75    pub fn max_attempts(self) -> u32 {
76        match self {
77            Self::Never => 1,
78            Self::Safe { max_attempts, .. } | Self::Idempotent { max_attempts, .. } => {
79                max_attempts.max(1)
80            }
81        }
82    }
83
84    pub fn delay_ms_for_retry(self, retry_index: u32, requested_after_ms: Option<u64>) -> u64 {
85        let (base_delay_ms, max_delay_ms) = match self {
86            Self::Never => return 0,
87            Self::Safe {
88                base_delay_ms,
89                max_delay_ms,
90                ..
91            }
92            | Self::Idempotent {
93                base_delay_ms,
94                max_delay_ms,
95                ..
96            } => (base_delay_ms, max_delay_ms),
97        };
98        let multiplier = 1_u64.checked_shl(retry_index).unwrap_or(u64::MAX);
99        let backoff = base_delay_ms.saturating_mul(multiplier);
100        let delay = requested_after_ms.unwrap_or(backoff);
101        if max_delay_ms == 0 {
102            delay
103        } else {
104            delay.min(max_delay_ms)
105        }
106    }
107
108    pub fn requires_replay_key(self) -> bool {
109        matches!(self, Self::Idempotent { .. })
110    }
111}
112
113fn default_tool_retry_policy() -> ToolRetryPolicy {
114    ToolRetryPolicy::default()
115}
116
117fn is_default_tool_retry_policy(policy: &ToolRetryPolicy) -> bool {
118    *policy == ToolRetryPolicy::default()
119}
120
121#[derive(
122    Clone,
123    Copy,
124    Debug,
125    Default,
126    PartialEq,
127    Eq,
128    PartialOrd,
129    Ord,
130    serde::Serialize,
131    serde::Deserialize,
132)]
133#[serde(rename_all = "snake_case")]
134pub enum ToolAvailability {
135    /// Keep the tool out of the current surface entirely.
136    ///
137    /// The definition can remain in registry state so host or authority
138    /// overrides survive refreshes, but the model cannot search, see, or call
139    /// the tool.
140    #[default]
141    Off,
142    /// Include the tool in the searchable catalog, but not in the model's
143    /// callable tool list.
144    Searchable,
145    /// Include the tool in the model's callable tool list, without featuring
146    /// it in prompt-side tool documentation.
147    Callable,
148    /// Include the tool in the callable list and feature it in prompt-side
149    /// tool documentation.
150    Showcased,
151}
152
153impl ToolAvailability {
154    pub fn is_searchable(self) -> bool {
155        self >= Self::Searchable
156    }
157
158    pub fn is_callable(self) -> bool {
159        self >= Self::Callable
160    }
161
162    pub fn is_showcased(self) -> bool {
163        self >= Self::Showcased
164    }
165}
166
167#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168pub struct ToolAvailabilityConfig {
169    pub base: ToolAvailability,
170}
171
172impl ToolAvailabilityConfig {
173    pub fn same(availability: ToolAvailability) -> Self {
174        Self { base: availability }
175    }
176
177    pub fn showcased() -> Self {
178        Self::same(ToolAvailability::Showcased)
179    }
180
181    pub fn callable() -> Self {
182        Self::same(ToolAvailability::Callable)
183    }
184
185    pub fn off() -> Self {
186        Self::same(ToolAvailability::Off)
187    }
188
189    pub fn base(&self) -> ToolAvailability {
190        self.base
191    }
192}
193
194impl Default for ToolAvailabilityConfig {
195    fn default() -> Self {
196        Self::showcased()
197    }
198}
199
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub enum ToolActivation {
203    #[default]
204    Always,
205    Internal,
206}
207
208fn is_default_tool_availability_config(config: &ToolAvailabilityConfig) -> bool {
209    *config == ToolAvailabilityConfig::default()
210}
211
212fn is_default_tool_activation(activation: &ToolActivation) -> bool {
213    *activation == ToolActivation::default()
214}
215
216#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
217#[serde(tag = "kind", rename_all = "snake_case")]
218pub enum ToolOutputContract {
219    #[default]
220    Static,
221    FromInputSchema {
222        input_field: String,
223        #[serde(default, skip_serializing_if = "Option::is_none")]
224        default_schema: Option<serde_json::Value>,
225    },
226}
227
228impl ToolOutputContract {
229    pub fn from_input_schema(
230        input_field: impl Into<String>,
231        default_schema: Option<serde_json::Value>,
232    ) -> Self {
233        Self::FromInputSchema {
234            input_field: input_field.into(),
235            default_schema,
236        }
237    }
238
239    pub fn is_static(&self) -> bool {
240        matches!(self, Self::Static)
241    }
242
243    fn return_type_label(&self, static_schema: &serde_json::Value) -> String {
244        match self {
245            Self::Static => compact_schema_label(static_schema),
246            Self::FromInputSchema { .. } => "T".to_string(),
247        }
248    }
249
250    fn type_parameter_suffix(&self) -> Option<String> {
251        match self {
252            Self::Static => None,
253            Self::FromInputSchema { default_schema, .. } => {
254                let default = default_schema
255                    .as_ref()
256                    .map(compact_schema_label)
257                    .unwrap_or_else(|| "any".to_string());
258                Some(format!("<T = {default}>"))
259            }
260        }
261    }
262
263    fn apply_type_witness_parameter(&self, params: &mut [ParameterDoc]) {
264        let Self::FromInputSchema { input_field, .. } = self else {
265            return;
266        };
267        if let Some(param) = params.iter_mut().find(|param| param.name == *input_field) {
268            param.type_label = "TypeSpec<T>".to_string();
269            param.nullable = false;
270            param.default_value = None;
271            param.enum_values.clear();
272            param.minimum = None;
273            param.maximum = None;
274            param.min_length = None;
275            param.max_length = None;
276            param.min_items = None;
277            param.max_items = None;
278            param.item_type = None;
279        }
280    }
281
282    fn return_fields(&self, static_schema: &serde_json::Value) -> Vec<serde_json::Value> {
283        match self {
284            Self::Static => return_field_metadata(static_schema),
285            Self::FromInputSchema { .. } => Vec::new(),
286        }
287    }
288}
289
290#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
291#[serde(tag = "kind", rename_all = "snake_case")]
292pub enum ToolArgumentProjectionPolicy {
293    #[default]
294    MaterializeProjectedValues,
295    PreserveProjectedRefsInField {
296        field: String,
297    },
298}
299
300impl ToolArgumentProjectionPolicy {
301    pub fn preserve_projected_refs_in_field(field: impl Into<String>) -> Self {
302        Self::PreserveProjectedRefsInField {
303            field: field.into(),
304        }
305    }
306
307    pub fn is_materialize_projected_values(&self) -> bool {
308        matches!(self, Self::MaterializeProjectedValues)
309    }
310}
311
312fn is_default_tool_argument_projection_policy(policy: &ToolArgumentProjectionPolicy) -> bool {
313    policy.is_materialize_projected_values()
314}
315
316#[derive(
317    Clone,
318    Debug,
319    Default,
320    PartialEq,
321    Eq,
322    PartialOrd,
323    Ord,
324    Hash,
325    serde::Serialize,
326    serde::Deserialize,
327)]
328#[serde(transparent)]
329pub struct ToolId(String);
330
331impl ToolId {
332    pub fn new(id: impl Into<String>) -> Self {
333        let id = id.into();
334        assert!(!id.trim().is_empty(), "tool id must not be empty");
335        Self(id)
336    }
337
338    pub fn default_for_name(name: &str) -> Self {
339        Self::new(format!("tool:{name}"))
340    }
341
342    pub fn as_str(&self) -> &str {
343        &self.0
344    }
345}
346
347impl From<String> for ToolId {
348    fn from(id: String) -> Self {
349        Self::new(id)
350    }
351}
352
353impl From<&str> for ToolId {
354    fn from(id: &str) -> Self {
355        Self::new(id)
356    }
357}
358
359impl std::fmt::Display for ToolId {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        f.write_str(&self.0)
362    }
363}
364
365/// Tool metadata exposed to prompts, catalogs, UI, and availability checks.
366/// The optional compact contract is the catalog-facing projection of the
367/// resolved contract; full schemas stay in [`ToolContract`].
368#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
369pub struct ToolManifest {
370    pub id: ToolId,
371    pub name: String,
372    #[serde(default, skip_serializing_if = "String::is_empty")]
373    pub description: String,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub compact_contract: Option<CompactToolContract>,
376    #[serde(default, skip_serializing_if = "is_default_tool_availability_config")]
377    pub availability: ToolAvailabilityConfig,
378    #[serde(default, skip_serializing_if = "is_default_tool_activation")]
379    pub activation: ToolActivation,
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub availability_override: Option<ToolAvailability>,
382    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
383    pub bindings: std::collections::BTreeMap<String, serde_json::Value>,
384    #[serde(
385        default,
386        skip_serializing_if = "is_default_tool_argument_projection_policy"
387    )]
388    pub argument_projection: ToolArgumentProjectionPolicy,
389    #[serde(
390        default = "default_tool_scheduling",
391        skip_serializing_if = "is_default_tool_scheduling"
392    )]
393    pub scheduling: ToolScheduling,
394    #[serde(
395        default = "default_tool_retry_policy",
396        skip_serializing_if = "is_default_tool_retry_policy"
397    )]
398    pub retry_policy: ToolRetryPolicy,
399}
400
401impl ToolManifest {
402    pub fn effective_availability(&self) -> ToolAvailability {
403        self.availability_override
404            .unwrap_or_else(|| self.availability.base())
405    }
406}
407
408/// Heavy tool contract resolved only when a prompt or call needs schemas/docs.
409#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
410pub struct ToolContract {
411    #[serde(default = "ToolContract::default_input_schema")]
412    pub input_schema: serde_json::Value,
413    #[serde(default)]
414    pub output_schema: serde_json::Value,
415    #[serde(default, skip_serializing_if = "Vec::is_empty")]
416    pub input_schema_projections: Vec<SchemaProjectionOverride>,
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub output_schema_projections: Vec<SchemaProjectionOverride>,
419    #[serde(default, skip_serializing_if = "ToolOutputContract::is_static")]
420    pub output_contract: ToolOutputContract,
421    #[serde(default, skip_serializing_if = "Vec::is_empty")]
422    pub examples: Vec<String>,
423}
424
425impl Default for ToolContract {
426    fn default() -> Self {
427        Self {
428            input_schema: Self::default_input_schema(),
429            output_schema: serde_json::Value::Null,
430            input_schema_projections: Vec::new(),
431            output_schema_projections: Vec::new(),
432            output_contract: ToolOutputContract::Static,
433            examples: Vec::new(),
434        }
435    }
436}
437
438impl ToolContract {
439    pub fn default_input_schema() -> serde_json::Value {
440        serde_json::json!({
441            "type": "object",
442            "properties": {},
443            "additionalProperties": true
444        })
445    }
446
447    pub fn compact_contract(&self, manifest: &ToolManifest) -> CompactToolContract {
448        self.compact_contract_with_example_limit(manifest, COMPACT_TOOL_EXAMPLE_LIMIT)
449    }
450
451    pub fn compact_contract_with_example_limit(
452        &self,
453        manifest: &ToolManifest,
454        example_limit: usize,
455    ) -> CompactToolContract {
456        CompactToolContract {
457            name: manifest.name.clone(),
458            signature: self.input_signature(manifest),
459            returns: self.output_summary(),
460            parameters: self.parameter_metadata(),
461            return_fields: self.output_contract.return_fields(&self.output_schema),
462            description: manifest.description.trim().to_string(),
463            examples: compact_examples(&self.examples, example_limit),
464        }
465    }
466
467    pub fn input_signature(&self, manifest: &ToolManifest) -> String {
468        let params = self
469            .parameter_docs()
470            .into_iter()
471            .map(|p| p.signature_fragment())
472            .collect::<Vec<_>>();
473        let body = if params.is_empty() {
474            "{}".to_string()
475        } else {
476            format!("{{ {} }}", params.join(", "))
477        };
478        format!(
479            "{}{}({})",
480            manifest.name,
481            self.output_contract
482                .type_parameter_suffix()
483                .unwrap_or_default(),
484            body
485        )
486    }
487
488    pub fn output_summary(&self) -> String {
489        self.output_contract.return_type_label(&self.output_schema)
490    }
491
492    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
493        self.parameter_docs()
494            .into_iter()
495            .map(|param| param.into_value())
496            .collect()
497    }
498
499    pub fn model_tool(&self, manifest: &ToolManifest) -> ModelTool {
500        ModelTool {
501            name: manifest.name.clone(),
502            description: manifest.description.clone(),
503            input_schema: self.input_schema.clone(),
504            output_schema: self.output_schema.clone(),
505            input_schema_projections: self.input_schema_projections.clone(),
506            output_schema_projections: self.output_schema_projections.clone(),
507        }
508    }
509
510    fn parameter_docs(&self) -> Vec<ParameterDoc> {
511        let mut params = schema_parameter_docs(&self.input_schema);
512        self.output_contract
513            .apply_type_witness_parameter(&mut params);
514        params
515    }
516}
517
518/// Static authoring helper for tools.
519///
520/// Composes the runtime [`ToolManifest`] and [`ToolContract`] projections. Both
521/// are `#[serde(flatten)]`ed so the serialized JSON shape stays flat (and wire/
522/// persistence compatible); the two structs have disjoint field names.
523#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
524pub struct ToolDefinition {
525    #[serde(flatten)]
526    pub manifest: ToolManifest,
527    #[serde(flatten)]
528    pub contract: ToolContract,
529}
530
531#[derive(Clone, Debug, PartialEq, Eq)]
532pub struct ModelTool {
533    pub name: String,
534    pub description: String,
535    pub input_schema: serde_json::Value,
536    pub output_schema: serde_json::Value,
537    pub input_schema_projections: Vec<SchemaProjectionOverride>,
538    pub output_schema_projections: Vec<SchemaProjectionOverride>,
539}
540
541#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
542pub struct SchemaProjectionOverride {
543    pub profile: String,
544    pub schema: serde_json::Value,
545}
546
547const COMPACT_TOOL_EXAMPLE_LIMIT: usize = 2;
548const COMPACT_TOOL_EXAMPLE_CHAR_LIMIT: usize = 240;
549
550#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
551pub struct CompactToolContract {
552    pub name: String,
553    pub signature: String,
554    pub returns: String,
555    #[serde(default, skip_serializing_if = "Vec::is_empty")]
556    pub parameters: Vec<serde_json::Value>,
557    #[serde(default, skip_serializing_if = "Vec::is_empty")]
558    pub return_fields: Vec<serde_json::Value>,
559    #[serde(default, skip_serializing_if = "String::is_empty")]
560    pub description: String,
561    #[serde(default, skip_serializing_if = "Vec::is_empty")]
562    pub examples: Vec<String>,
563}
564
565impl CompactToolContract {
566    pub fn render_signature_head(&self) -> String {
567        format!("{} -> {}", self.signature.trim(), self.returns.trim())
568    }
569
570    pub fn render_signature(&self) -> String {
571        let mut sections = vec![self.render_signature_head()];
572        let parameter_lines = self
573            .parameters
574            .iter()
575            .filter_map(compact_doc_line)
576            .collect::<Vec<_>>();
577        if !parameter_lines.is_empty() {
578            sections.push(format!("Parameters:\n{}", parameter_lines.join("\n")));
579        }
580        let return_field_lines = self
581            .return_fields
582            .iter()
583            .filter_map(compact_doc_line)
584            .collect::<Vec<_>>();
585        if !return_field_lines.is_empty() {
586            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
587        }
588        sections.join("\n")
589    }
590
591    pub fn render_returns(&self) -> String {
592        let mut sections = Vec::new();
593        let return_field_lines = self
594            .return_fields
595            .iter()
596            .filter_map(compact_doc_line)
597            .collect::<Vec<_>>();
598        if !return_field_lines.is_empty() {
599            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
600        }
601        sections.join("\n")
602    }
603
604    pub fn render_markdown(&self) -> String {
605        let mut sections = vec![format!("### {}", self.render_signature_head())];
606        if !self.description.trim().is_empty() {
607            sections.push(self.description.trim().to_string());
608        }
609        if !self.parameters.is_empty() {
610            sections.push(format!(
611                "Parameters:\n{}",
612                self.parameters
613                    .iter()
614                    .filter_map(compact_doc_line)
615                    .collect::<Vec<_>>()
616                    .join("\n")
617            ));
618        }
619        if !self.return_fields.is_empty() {
620            sections.push(format!(
621                "Return fields:\n{}",
622                self.return_fields
623                    .iter()
624                    .filter_map(compact_doc_line)
625                    .collect::<Vec<_>>()
626                    .join("\n")
627            ));
628        }
629        if !self.examples.is_empty() {
630            sections.push(format!("Examples: {}", self.examples.join("; ")));
631        }
632        sections.join("\n")
633    }
634}
635
636impl ToolDefinition {
637    pub fn raw_with_id(
638        id: impl Into<ToolId>,
639        name: impl Into<String>,
640        description: impl Into<String>,
641        input_schema: serde_json::Value,
642        output_schema: serde_json::Value,
643    ) -> Self {
644        Self {
645            manifest: ToolManifest {
646                id: id.into(),
647                name: name.into(),
648                description: description.into(),
649                compact_contract: None,
650                ..ToolManifest::default()
651            },
652            contract: ToolContract {
653                input_schema,
654                output_schema,
655                ..ToolContract::default()
656            },
657        }
658    }
659
660    pub fn raw_named(
661        name: impl Into<String>,
662        description: impl Into<String>,
663        input_schema: serde_json::Value,
664        output_schema: serde_json::Value,
665    ) -> Self {
666        let name = name.into();
667        Self::raw_with_id(
668            ToolId::default_for_name(&name),
669            name,
670            description,
671            input_schema,
672            output_schema,
673        )
674    }
675
676    pub fn typed_with_id<Args, Output>(
677        id: impl Into<ToolId>,
678        name: impl Into<String>,
679        description: impl Into<String>,
680    ) -> Self
681    where
682        Args: schemars::JsonSchema,
683        Output: schemars::JsonSchema,
684    {
685        Self::raw_with_id(
686            id,
687            name,
688            description,
689            schema_for::<Args>(),
690            schema_for::<Output>(),
691        )
692    }
693
694    pub fn typed<Args, Output>(name: impl Into<String>, description: impl Into<String>) -> Self
695    where
696        Args: schemars::JsonSchema,
697        Output: schemars::JsonSchema,
698    {
699        let name = name.into();
700        Self::typed_with_id::<Args, Output>(ToolId::default_for_name(&name), name, description)
701    }
702
703    pub fn raw(
704        id: impl Into<ToolId>,
705        name: impl Into<String>,
706        description: impl Into<String>,
707        input_schema: serde_json::Value,
708        output_schema: serde_json::Value,
709    ) -> Self {
710        Self::raw_with_id(id, name, description, input_schema, output_schema)
711    }
712
713    pub fn with_examples(mut self, examples: Vec<String>) -> Self {
714        self.contract.examples = examples;
715        self
716    }
717
718    pub fn with_availability(mut self, availability: ToolAvailabilityConfig) -> Self {
719        self.manifest.availability = availability;
720        self
721    }
722
723    pub fn with_activation(mut self, activation: ToolActivation) -> Self {
724        self.manifest.activation = activation;
725        self
726    }
727
728    pub fn with_argument_projection(
729        mut self,
730        argument_projection: ToolArgumentProjectionPolicy,
731    ) -> Self {
732        self.manifest.argument_projection = argument_projection;
733        self
734    }
735
736    pub fn with_scheduling(mut self, scheduling: ToolScheduling) -> Self {
737        self.manifest.scheduling = scheduling;
738        self
739    }
740
741    pub fn with_retry_policy(mut self, retry_policy: ToolRetryPolicy) -> Self {
742        self.manifest.retry_policy = retry_policy;
743        self
744    }
745
746    pub fn with_output_contract(mut self, output_contract: ToolOutputContract) -> Self {
747        self.contract.output_contract = output_contract;
748        self
749    }
750
751    pub fn with_input_schema_projection(
752        mut self,
753        profile: impl Into<String>,
754        schema: serde_json::Value,
755    ) -> Self {
756        let profile = profile.into();
757        self.contract
758            .input_schema_projections
759            .retain(|projection| projection.profile != profile);
760        self.contract
761            .input_schema_projections
762            .push(SchemaProjectionOverride { profile, schema });
763        self
764    }
765
766    pub fn with_output_schema_projection(
767        mut self,
768        profile: impl Into<String>,
769        schema: serde_json::Value,
770    ) -> Self {
771        let profile = profile.into();
772        self.contract
773            .output_schema_projections
774            .retain(|projection| projection.profile != profile);
775        self.contract
776            .output_schema_projections
777            .push(SchemaProjectionOverride { profile, schema });
778        self
779    }
780
781    pub fn with_output_from_input_schema(
782        self,
783        input_field: impl Into<String>,
784        default_schema: Option<serde_json::Value>,
785    ) -> Self {
786        self.with_output_contract(ToolOutputContract::from_input_schema(
787            input_field,
788            default_schema,
789        ))
790    }
791
792    pub fn default_input_schema() -> serde_json::Value {
793        ToolContract::default_input_schema()
794    }
795
796    /// Tool identity. Read very widely, so exposed as a thin accessor over the
797    /// composed [`ToolManifest`].
798    pub fn id(&self) -> &ToolId {
799        &self.manifest.id
800    }
801
802    /// Tool name. Read very widely, so exposed as a thin accessor.
803    pub fn name(&self) -> &str {
804        &self.manifest.name
805    }
806
807    /// Tool description. Read very widely, so exposed as a thin accessor.
808    pub fn description(&self) -> &str {
809        &self.manifest.description
810    }
811
812    pub fn input_signature(&self) -> String {
813        self.contract.input_signature(&self.manifest)
814    }
815
816    pub fn output_summary(&self) -> String {
817        self.contract.output_summary()
818    }
819
820    pub fn signature(&self) -> String {
821        format!("{} -> {}", self.input_signature(), self.output_summary())
822    }
823
824    pub fn compact_contract(&self) -> CompactToolContract {
825        self.compact_contract_with_example_limit(COMPACT_TOOL_EXAMPLE_LIMIT)
826    }
827
828    pub fn compact_contract_with_example_limit(&self, example_limit: usize) -> CompactToolContract {
829        self.contract
830            .compact_contract_with_example_limit(&self.manifest, example_limit)
831    }
832
833    pub fn effective_availability(&self) -> ToolAvailability {
834        self.manifest.effective_availability()
835    }
836
837    pub fn model_tool(&self) -> ModelTool {
838        self.contract.model_tool(&self.manifest)
839    }
840
841    /// Project the manifest, computing the catalog-facing compact contract from
842    /// the resolved [`ToolContract`].
843    pub fn manifest(&self) -> ToolManifest {
844        let mut manifest = self.manifest.clone();
845        manifest.compact_contract = Some(self.contract.compact_contract(&manifest));
846        manifest
847    }
848
849    pub fn contract(&self) -> ToolContract {
850        self.contract.clone()
851    }
852
853    /// Recompose a definition from its [`ToolManifest`] and [`ToolContract`]
854    /// projections — the inverse of [`ToolDefinition::manifest`]/[`ToolDefinition::contract`].
855    pub fn from_parts(manifest: ToolManifest, contract: ToolContract) -> Self {
856        Self { manifest, contract }
857    }
858
859    pub fn format_tool_docs(tools: &[ToolDefinition]) -> String {
860        Self::format_tool_docs_iter(tools.iter())
861    }
862
863    pub fn format_tool_docs_iter<'a>(
864        tools: impl IntoIterator<Item = &'a ToolDefinition>,
865    ) -> String {
866        tools
867            .into_iter()
868            .map(|tool| tool.compact_contract().render_markdown())
869            .collect::<Vec<_>>()
870            .join("\n\n")
871    }
872
873    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
874        self.parameter_docs()
875            .into_iter()
876            .map(|param| param.into_value())
877            .collect()
878    }
879
880    fn parameter_docs(&self) -> Vec<ParameterDoc> {
881        let mut params = schema_parameter_docs(&self.contract.input_schema);
882        self.contract
883            .output_contract
884            .apply_type_witness_parameter(&mut params);
885        params
886    }
887}
888
889mod schema_docs;
890pub use schema_docs::schema_for;
891use schema_docs::{
892    ParameterDoc, compact_doc_line, compact_examples, compact_schema_label, return_field_metadata,
893    schema_parameter_docs,
894};
895
896mod schema_validation;
897pub use schema_validation::{LashSchema, validate_tool_input};
898
899include!("tool_contract/tests.rs");