Skip to main content

lash_sansio/
tool_contract.rs

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