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(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ToolActivation {
124    #[default]
125    Always,
126    Internal,
127}
128
129fn is_default_tool_activation(activation: &ToolActivation) -> bool {
130    *activation == ToolActivation::default()
131}
132
133#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
134#[serde(tag = "kind", rename_all = "snake_case")]
135pub enum ToolOutputContract {
136    #[default]
137    Static,
138    FromInputSchema {
139        input_field: String,
140        #[serde(default, skip_serializing_if = "Option::is_none")]
141        default_schema: Option<serde_json::Value>,
142    },
143}
144
145impl ToolOutputContract {
146    pub fn from_input_schema(
147        input_field: impl Into<String>,
148        default_schema: Option<serde_json::Value>,
149    ) -> Self {
150        Self::FromInputSchema {
151            input_field: input_field.into(),
152            default_schema,
153        }
154    }
155
156    pub fn is_static(&self) -> bool {
157        matches!(self, Self::Static)
158    }
159
160    fn return_type_label(&self, static_schema: &serde_json::Value) -> String {
161        match self {
162            Self::Static => compact_schema_label(static_schema),
163            Self::FromInputSchema { .. } => "T".to_string(),
164        }
165    }
166
167    fn type_parameter_suffix(&self) -> Option<String> {
168        match self {
169            Self::Static => None,
170            Self::FromInputSchema { default_schema, .. } => {
171                let default = default_schema
172                    .as_ref()
173                    .map(compact_schema_label)
174                    .unwrap_or_else(|| "any".to_string());
175                Some(format!("<T = {default}>"))
176            }
177        }
178    }
179
180    fn apply_type_witness_parameter(&self, params: &mut [ParameterDoc]) {
181        let Self::FromInputSchema { input_field, .. } = self else {
182            return;
183        };
184        if let Some(param) = params.iter_mut().find(|param| param.name == *input_field) {
185            param.type_label = "TypeSpec<T>".to_string();
186            param.nullable = false;
187            param.default_value = None;
188            param.enum_values.clear();
189            param.minimum = None;
190            param.maximum = None;
191            param.min_length = None;
192            param.max_length = None;
193            param.min_items = None;
194            param.max_items = None;
195            param.item_type = None;
196        }
197    }
198
199    fn return_fields(&self, static_schema: &serde_json::Value) -> Vec<serde_json::Value> {
200        match self {
201            Self::Static => return_field_metadata(static_schema),
202            Self::FromInputSchema { .. } => Vec::new(),
203        }
204    }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
208#[serde(tag = "kind", rename_all = "snake_case")]
209pub enum ToolArgumentProjectionPolicy {
210    #[default]
211    MaterializeProjectedValues,
212    PreserveProjectedRefsInField {
213        field: String,
214    },
215}
216
217impl ToolArgumentProjectionPolicy {
218    pub fn preserve_projected_refs_in_field(field: impl Into<String>) -> Self {
219        Self::PreserveProjectedRefsInField {
220            field: field.into(),
221        }
222    }
223
224    pub fn is_materialize_projected_values(&self) -> bool {
225        matches!(self, Self::MaterializeProjectedValues)
226    }
227}
228
229fn is_default_tool_argument_projection_policy(policy: &ToolArgumentProjectionPolicy) -> bool {
230    policy.is_materialize_projected_values()
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
234#[serde(transparent)]
235pub struct ToolId(String);
236
237impl ToolId {
238    pub fn new(id: impl Into<String>) -> Self {
239        let id = id.into();
240        assert!(!id.trim().is_empty(), "tool id must not be empty");
241        Self(id)
242    }
243
244    pub fn as_str(&self) -> &str {
245        &self.0
246    }
247}
248
249impl<'de> serde::Deserialize<'de> for ToolId {
250    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251    where
252        D: serde::Deserializer<'de>,
253    {
254        let id = <String as serde::Deserialize>::deserialize(deserializer)?;
255        if id.trim().is_empty() {
256            return Err(serde::de::Error::custom("tool id must not be empty"));
257        }
258        Ok(Self(id))
259    }
260}
261
262impl From<String> for ToolId {
263    fn from(id: String) -> Self {
264        Self::new(id)
265    }
266}
267
268impl From<&str> for ToolId {
269    fn from(id: &str) -> Self {
270        Self::new(id)
271    }
272}
273
274impl std::fmt::Display for ToolId {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        f.write_str(&self.0)
277    }
278}
279
280/// Tool metadata exposed to prompts, catalogs, and UI. Catalog membership —
281/// being present in a [`ToolProvider`]'s manifest list — is the execution gate;
282/// there is no per-manifest tier. The optional compact contract is the
283/// catalog-facing projection of the resolved contract; full schemas stay in
284/// [`ToolContract`].
285#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
286pub struct ToolManifest {
287    pub id: ToolId,
288    pub name: String,
289    #[serde(default, skip_serializing_if = "String::is_empty")]
290    pub description: String,
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub compact_contract: Option<CompactToolContract>,
293    #[serde(default, skip_serializing_if = "is_default_tool_activation")]
294    pub activation: ToolActivation,
295    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
296    pub bindings: std::collections::BTreeMap<String, serde_json::Value>,
297    #[serde(
298        default,
299        skip_serializing_if = "is_default_tool_argument_projection_policy"
300    )]
301    pub argument_projection: ToolArgumentProjectionPolicy,
302    #[serde(
303        default = "default_tool_scheduling",
304        skip_serializing_if = "is_default_tool_scheduling"
305    )]
306    pub scheduling: ToolScheduling,
307    #[serde(
308        default = "default_tool_retry_policy",
309        skip_serializing_if = "is_default_tool_retry_policy"
310    )]
311    pub retry_policy: ToolRetryPolicy,
312}
313
314/// Heavy tool contract resolved only when a prompt or call needs schemas/docs.
315#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
316pub struct ToolContract {
317    #[serde(default = "ToolContract::default_input_schema")]
318    pub input_schema: serde_json::Value,
319    #[serde(default)]
320    pub output_schema: serde_json::Value,
321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
322    pub input_schema_projections: Vec<SchemaProjectionOverride>,
323    #[serde(default, skip_serializing_if = "Vec::is_empty")]
324    pub output_schema_projections: Vec<SchemaProjectionOverride>,
325    #[serde(default, skip_serializing_if = "ToolOutputContract::is_static")]
326    pub output_contract: ToolOutputContract,
327    #[serde(default, skip_serializing_if = "Vec::is_empty")]
328    pub examples: Vec<String>,
329}
330
331impl Default for ToolContract {
332    fn default() -> Self {
333        Self {
334            input_schema: Self::default_input_schema(),
335            output_schema: serde_json::Value::Null,
336            input_schema_projections: Vec::new(),
337            output_schema_projections: Vec::new(),
338            output_contract: ToolOutputContract::Static,
339            examples: Vec::new(),
340        }
341    }
342}
343
344impl ToolContract {
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.output_contract.return_fields(&self.output_schema),
393            description: manifest.description.trim().to_string(),
394            examples: compact_examples(&self.examples, example_limit),
395        }
396    }
397
398    pub fn input_signature(&self, manifest: &ToolManifest) -> String {
399        self.input_signature_with_name(manifest, &manifest.name)
400    }
401
402    pub fn input_signature_with_name(
403        &self,
404        _manifest: &ToolManifest,
405        signature_name: &str,
406    ) -> String {
407        let params = self
408            .parameter_docs()
409            .into_iter()
410            .map(|p| p.signature_fragment())
411            .collect::<Vec<_>>();
412        let body = if params.is_empty() {
413            "{}".to_string()
414        } else {
415            format!("{{ {} }}", params.join(", "))
416        };
417        format!(
418            "{}{}({})",
419            signature_name,
420            self.output_contract
421                .type_parameter_suffix()
422                .unwrap_or_default(),
423            body
424        )
425    }
426
427    pub fn output_summary(&self) -> String {
428        self.output_contract.return_type_label(&self.output_schema)
429    }
430
431    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
432        self.parameter_docs()
433            .into_iter()
434            .map(|param| param.into_value())
435            .collect()
436    }
437
438    pub fn model_tool(&self, manifest: &ToolManifest) -> ModelTool {
439        ModelTool {
440            name: manifest.name.clone(),
441            description: manifest.description.clone(),
442            input_schema: self.input_schema.clone(),
443            output_schema: self.output_schema.clone(),
444            input_schema_projections: self.input_schema_projections.clone(),
445            output_schema_projections: self.output_schema_projections.clone(),
446        }
447    }
448
449    fn parameter_docs(&self) -> Vec<ParameterDoc> {
450        let mut params = schema_parameter_docs(&self.input_schema);
451        self.output_contract
452            .apply_type_witness_parameter(&mut params);
453        params
454    }
455}
456
457/// Static authoring helper for tools.
458///
459/// Composes the runtime [`ToolManifest`] and [`ToolContract`] projections. Both
460/// are `#[serde(flatten)]`ed so the serialized JSON shape stays flat (and wire/
461/// persistence compatible); the two structs have disjoint field names.
462#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
463pub struct ToolDefinition {
464    #[serde(flatten)]
465    pub manifest: ToolManifest,
466    #[serde(flatten)]
467    pub contract: ToolContract,
468}
469
470#[derive(Clone, Debug, PartialEq, Eq)]
471pub struct ModelTool {
472    pub name: String,
473    pub description: String,
474    pub input_schema: serde_json::Value,
475    pub output_schema: serde_json::Value,
476    pub input_schema_projections: Vec<SchemaProjectionOverride>,
477    pub output_schema_projections: Vec<SchemaProjectionOverride>,
478}
479
480#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
481pub struct SchemaProjectionOverride {
482    pub profile: String,
483    pub schema: serde_json::Value,
484}
485
486const COMPACT_TOOL_EXAMPLE_LIMIT: usize = 2;
487const COMPACT_TOOL_EXAMPLE_CHAR_LIMIT: usize = 240;
488
489#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
490pub struct CompactToolContract {
491    pub name: String,
492    pub signature: String,
493    pub returns: String,
494    #[serde(default, skip_serializing_if = "Vec::is_empty")]
495    pub parameters: Vec<serde_json::Value>,
496    #[serde(default, skip_serializing_if = "Vec::is_empty")]
497    pub return_fields: Vec<serde_json::Value>,
498    #[serde(default, skip_serializing_if = "String::is_empty")]
499    pub description: String,
500    #[serde(default, skip_serializing_if = "Vec::is_empty")]
501    pub examples: Vec<String>,
502}
503
504impl CompactToolContract {
505    pub fn render_signature_head(&self) -> String {
506        format!("{} -> {}", self.signature.trim(), self.returns.trim())
507    }
508
509    pub fn render_signature(&self) -> String {
510        let mut sections = vec![self.render_signature_head()];
511        let parameter_lines = self
512            .parameters
513            .iter()
514            .filter_map(compact_doc_line)
515            .collect::<Vec<_>>();
516        if !parameter_lines.is_empty() {
517            sections.push(format!("Parameters:\n{}", parameter_lines.join("\n")));
518        }
519        let return_field_lines = self
520            .return_fields
521            .iter()
522            .filter_map(compact_doc_line)
523            .collect::<Vec<_>>();
524        if !return_field_lines.is_empty() {
525            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
526        }
527        sections.join("\n")
528    }
529
530    pub fn render_returns(&self) -> String {
531        let mut sections = Vec::new();
532        let return_field_lines = self
533            .return_fields
534            .iter()
535            .filter_map(compact_doc_line)
536            .collect::<Vec<_>>();
537        if !return_field_lines.is_empty() {
538            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
539        }
540        sections.join("\n")
541    }
542
543    pub fn render_markdown(&self) -> String {
544        let mut sections = vec![format!("### {}", self.render_signature_head())];
545        if !self.description.trim().is_empty() {
546            sections.push(self.description.trim().to_string());
547        }
548        if !self.parameters.is_empty() {
549            sections.push(format!(
550                "Parameters:\n{}",
551                self.parameters
552                    .iter()
553                    .filter_map(compact_doc_line)
554                    .collect::<Vec<_>>()
555                    .join("\n")
556            ));
557        }
558        if !self.return_fields.is_empty() {
559            sections.push(format!(
560                "Return fields:\n{}",
561                self.return_fields
562                    .iter()
563                    .filter_map(compact_doc_line)
564                    .collect::<Vec<_>>()
565                    .join("\n")
566            ));
567        }
568        if !self.examples.is_empty() {
569            sections.push(format!("Examples: {}", self.examples.join("; ")));
570        }
571        sections.join("\n")
572    }
573}
574
575impl ToolDefinition {
576    pub fn raw(
577        id: impl Into<ToolId>,
578        name: impl Into<String>,
579        description: impl Into<String>,
580        input_schema: serde_json::Value,
581        output_schema: serde_json::Value,
582    ) -> Self {
583        Self {
584            manifest: ToolManifest {
585                id: id.into(),
586                name: name.into(),
587                description: description.into(),
588                compact_contract: None,
589                activation: ToolActivation::default(),
590                bindings: std::collections::BTreeMap::new(),
591                argument_projection: ToolArgumentProjectionPolicy::default(),
592                scheduling: default_tool_scheduling(),
593                retry_policy: default_tool_retry_policy(),
594            },
595            contract: ToolContract {
596                input_schema,
597                output_schema,
598                ..ToolContract::default()
599            },
600        }
601    }
602
603    pub fn typed<Args, Output>(
604        id: impl Into<ToolId>,
605        name: impl Into<String>,
606        description: impl Into<String>,
607    ) -> Self
608    where
609        Args: schemars::JsonSchema,
610        Output: schemars::JsonSchema,
611    {
612        Self::raw(
613            id,
614            name,
615            description,
616            schema_for::<Args>(),
617            schema_for::<Output>(),
618        )
619    }
620
621    pub fn with_examples(mut self, examples: Vec<String>) -> Self {
622        self.contract.examples = examples;
623        self
624    }
625
626    pub fn with_activation(mut self, activation: ToolActivation) -> Self {
627        self.manifest.activation = activation;
628        self
629    }
630
631    pub fn with_argument_projection(
632        mut self,
633        argument_projection: ToolArgumentProjectionPolicy,
634    ) -> Self {
635        self.manifest.argument_projection = argument_projection;
636        self
637    }
638
639    pub fn with_scheduling(mut self, scheduling: ToolScheduling) -> Self {
640        self.manifest.scheduling = scheduling;
641        self
642    }
643
644    pub fn with_retry_policy(mut self, retry_policy: ToolRetryPolicy) -> Self {
645        self.manifest.retry_policy = retry_policy;
646        self
647    }
648
649    pub fn with_output_contract(mut self, output_contract: ToolOutputContract) -> Self {
650        self.contract.output_contract = output_contract;
651        self
652    }
653
654    pub fn with_input_schema_projection(
655        mut self,
656        profile: impl Into<String>,
657        schema: serde_json::Value,
658    ) -> Self {
659        let profile = profile.into();
660        self.contract
661            .input_schema_projections
662            .retain(|projection| projection.profile != profile);
663        self.contract
664            .input_schema_projections
665            .push(SchemaProjectionOverride { profile, schema });
666        self
667    }
668
669    pub fn with_output_schema_projection(
670        mut self,
671        profile: impl Into<String>,
672        schema: serde_json::Value,
673    ) -> Self {
674        let profile = profile.into();
675        self.contract
676            .output_schema_projections
677            .retain(|projection| projection.profile != profile);
678        self.contract
679            .output_schema_projections
680            .push(SchemaProjectionOverride { profile, schema });
681        self
682    }
683
684    pub fn with_output_from_input_schema(
685        self,
686        input_field: impl Into<String>,
687        default_schema: Option<serde_json::Value>,
688    ) -> Self {
689        self.with_output_contract(ToolOutputContract::from_input_schema(
690            input_field,
691            default_schema,
692        ))
693    }
694
695    pub fn default_input_schema() -> serde_json::Value {
696        ToolContract::default_input_schema()
697    }
698
699    /// Tool identity. Read very widely, so exposed as a thin accessor over the
700    /// composed [`ToolManifest`].
701    pub fn id(&self) -> &ToolId {
702        &self.manifest.id
703    }
704
705    /// Tool name. Read very widely, so exposed as a thin accessor.
706    pub fn name(&self) -> &str {
707        &self.manifest.name
708    }
709
710    /// Tool description. Read very widely, so exposed as a thin accessor.
711    pub fn description(&self) -> &str {
712        &self.manifest.description
713    }
714
715    pub fn input_signature(&self) -> String {
716        self.contract.input_signature(&self.manifest)
717    }
718
719    pub fn output_summary(&self) -> String {
720        self.contract.output_summary()
721    }
722
723    pub fn signature(&self) -> String {
724        format!("{} -> {}", self.input_signature(), self.output_summary())
725    }
726
727    pub fn compact_contract(&self) -> CompactToolContract {
728        self.compact_contract_with_example_limit(COMPACT_TOOL_EXAMPLE_LIMIT)
729    }
730
731    pub fn compact_contract_with_example_limit(&self, example_limit: usize) -> CompactToolContract {
732        self.contract
733            .compact_contract_with_example_limit(&self.manifest, example_limit)
734    }
735
736    pub fn model_tool(&self) -> ModelTool {
737        self.contract.model_tool(&self.manifest)
738    }
739
740    /// Project the manifest, computing the catalog-facing compact contract from
741    /// the resolved [`ToolContract`].
742    pub fn manifest(&self) -> ToolManifest {
743        let mut manifest = self.manifest.clone();
744        manifest.compact_contract = Some(self.contract.compact_contract(&manifest));
745        manifest
746    }
747
748    pub fn contract(&self) -> ToolContract {
749        self.contract.clone()
750    }
751
752    /// Recompose a definition from its [`ToolManifest`] and [`ToolContract`]
753    /// projections — the inverse of [`ToolDefinition::manifest`]/[`ToolDefinition::contract`].
754    pub fn from_parts(manifest: ToolManifest, contract: ToolContract) -> Self {
755        Self { manifest, contract }
756    }
757
758    pub fn format_tool_docs(tools: &[ToolDefinition]) -> String {
759        Self::format_tool_docs_iter(tools.iter())
760    }
761
762    pub fn format_tool_docs_iter<'a>(
763        tools: impl IntoIterator<Item = &'a ToolDefinition>,
764    ) -> String {
765        tools
766            .into_iter()
767            .map(|tool| tool.compact_contract().render_markdown())
768            .collect::<Vec<_>>()
769            .join("\n\n")
770    }
771
772    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
773        self.parameter_docs()
774            .into_iter()
775            .map(|param| param.into_value())
776            .collect()
777    }
778
779    fn parameter_docs(&self) -> Vec<ParameterDoc> {
780        let mut params = schema_parameter_docs(&self.contract.input_schema);
781        self.contract
782            .output_contract
783            .apply_type_witness_parameter(&mut params);
784        params
785    }
786}
787
788mod schema_docs;
789pub use schema_docs::schema_for;
790use schema_docs::{
791    ParameterDoc, compact_doc_line, compact_examples, compact_schema_label, return_field_metadata,
792    schema_parameter_docs,
793};
794
795mod schema_validation;
796pub use schema_validation::{LashSchema, validate_tool_input};
797
798include!("tool_contract/tests.rs");