Skip to main content

lash_sansio/
lib.rs

1pub mod attachment;
2pub mod llm;
3pub mod mode;
4pub mod plugin;
5pub mod prompt;
6pub mod sansio;
7pub mod session;
8pub mod session_model;
9pub mod tool_output;
10pub mod tool_surface;
11pub mod turn;
12
13use std::sync::Arc;
14
15pub const VERSION: &str = env!("CARGO_PKG_VERSION");
16
17pub use attachment::{
18    AttachmentCreateMeta, AttachmentId, AttachmentMeta, AttachmentRef, ImageMediaType, MediaType,
19};
20pub use llm::types::LlmTerminalReason;
21pub use mode::{
22    ModeBuildInput, ModeConfig, ModePreamble, append_assistant_text_part,
23    normalized_response_parts, reasoning_part, turn_limit_exhausted_message,
24};
25pub use plugin::{
26    CheckpointKind, PluginMessage, PluginSurfaceEvent, PromptContribution, PromptContributionGate,
27};
28pub use prompt::{
29    PreparedPrompt, PromptBuildInput, PromptCache, PromptContributionSet, PromptFingerprint,
30    build_prompt, build_prompt_cached, prompt_template_fingerprint, prompt_text_fingerprint,
31    prompt_tool_names_fingerprint,
32};
33pub use sansio::{
34    ChatContextProjector, CheckpointResumeAction, CompletedToolCall, ContextProjector,
35    DriverAction, DriverContextView, Effect, EffectId, LlmCallError, ModeProtocol, PendingToolCall,
36    ProjectorContext, ProtocolDriverHandle, Response, TurnCheckpoint, TurnMachine,
37    TurnMachineConfig, UnitModeProtocol, WaitingExecState, WaitingLlmState, driver_state,
38};
39pub use session::{
40    CompletedTurn, ExecResponse, PromptUsage, SansIoSessionState, TextProjectionMetadata,
41    apply_completed_turn,
42};
43pub use session_model::message::MessageOrigin;
44pub use session_model::{
45    AcceptedInjectedTurnInput, BaseRenderCache, ConversationRecord, ErrorEnvelope,
46    MAIN_AGENT_INTRO, Message, MessageRole, MessageSequence, Part, PartAttachment, PartKind,
47    PromptBuiltin, PromptLayer, PromptPanel, PromptRequest, PromptResponse, PromptSelectionMode,
48    PromptSlot, PromptSlotLayer, PromptTemplate, PromptTemplateEntry, PromptTemplateSection,
49    PruneState, RenderedPrompt, ResolvedPromptLayer, SessionEvent, SessionEventRecord,
50    StateSnapshotEvent, TokenUsage, ToolEvent, TurnFinish, TurnOutcome, TurnStop,
51    default_prompt_template, messages_are_prompt_resume_safe, resolve_prompt_layers, shared_parts,
52};
53pub use tool_output::{
54    ModelToolReturn, ModelToolReturnPart, ToolCallOutcome, ToolCallOutput, ToolCallStatus,
55    ToolCancellation, ToolControl, ToolFailure, ToolFailureClass, ToolFailureSource,
56    ToolRetryDisposition, ToolValue,
57};
58pub use tool_surface::{
59    ToolContractResolver, ToolSurface, ToolSurfaceBuildInput, ToolSurfaceContribution,
60    ToolSurfaceEntry, ToolSurfaceOverride, build_tool_surface,
61};
62pub use turn::{PreparedTurnMachine, SansIoTurnInput, build_turn};
63
64/// Stable string id for the execution backend that owns a session turn.
65#[derive(Clone, Debug, PartialEq, Eq, Hash)]
66pub struct ExecutionMode(std::sync::Arc<str>);
67
68impl ExecutionMode {
69    pub fn new(id: impl Into<std::sync::Arc<str>>) -> Self {
70        Self(id.into())
71    }
72
73    pub fn standard() -> Self {
74        Self::new("standard")
75    }
76
77    pub fn plugin_id(&self) -> &str {
78        &self.0
79    }
80}
81
82impl Default for ExecutionMode {
83    fn default() -> Self {
84        Self::standard()
85    }
86}
87
88impl std::fmt::Display for ExecutionMode {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.write_str(self.plugin_id())
91    }
92}
93
94impl serde::Serialize for ExecutionMode {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: serde::Serializer,
98    {
99        serializer.serialize_str(self.plugin_id())
100    }
101}
102
103impl<'de> serde::Deserialize<'de> for ExecutionMode {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: serde::Deserializer<'de>,
107    {
108        let id = <String as serde::Deserialize>::deserialize(deserializer)?;
109        Ok(Self::new(id))
110    }
111}
112
113pub fn execution_mode_supported(_mode: &ExecutionMode) -> bool {
114    true
115}
116
117pub fn default_execution_mode() -> ExecutionMode {
118    ExecutionMode::default()
119}
120
121/// How a tool's invocations should be scheduled relative to other tools in
122/// the same batch of model-produced tool calls.
123///
124/// Tools that only *read* state (`read_file`, `grep`, `glob`, ...) can run
125/// in parallel safely and should use the default [`ToolExecutionMode::Parallel`].
126/// Tools that *mutate* shared state (`apply_patch`, `exec_command`,
127/// `write_stdin`) should declare
128/// [`ToolExecutionMode::Serial`] so the dispatcher runs them one-at-a-time
129/// and avoids interleaving with each other.
130///
131/// The name is intentionally distinct from the turn-level [`ExecutionMode`]
132/// (which selects the execution driver) so the two concepts don't
133/// collide in scope.
134#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ToolExecutionMode {
137    /// Safe to run concurrently with other parallel tools in the same batch.
138    #[default]
139    Parallel,
140    /// Must run one-at-a-time relative to other serial tools in the batch.
141    Serial,
142}
143
144fn default_tool_execution_mode() -> ToolExecutionMode {
145    ToolExecutionMode::default()
146}
147
148fn is_default_tool_execution_mode(mode: &ToolExecutionMode) -> bool {
149    *mode == ToolExecutionMode::default()
150}
151
152/// Automatic retry policy for a tool's execution.
153///
154/// This is intentionally separate from [`ToolExecutionMode`]: scheduling
155/// decides whether different tool calls may run together, while retry policy
156/// decides whether one failed call may be attempted again inside its scheduled
157/// slot.
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
159#[serde(tag = "type", rename_all = "snake_case")]
160pub enum ToolRetryPolicy {
161    /// Never retry automatically. This is the default for every tool.
162    #[default]
163    Never,
164    /// Retry only failures that explicitly report a safe retry disposition.
165    Safe {
166        max_attempts: u32,
167        base_delay_ms: u64,
168        max_delay_ms: u64,
169    },
170    /// Retry only failures that explicitly report a safe retry disposition,
171    /// and only when the runtime can provide a stable idempotency key.
172    Idempotent {
173        max_attempts: u32,
174        base_delay_ms: u64,
175        max_delay_ms: u64,
176    },
177}
178
179impl ToolRetryPolicy {
180    pub fn safe(max_attempts: u32, base_delay_ms: u64, max_delay_ms: u64) -> Self {
181        Self::Safe {
182            max_attempts,
183            base_delay_ms,
184            max_delay_ms,
185        }
186    }
187
188    pub fn idempotent(max_attempts: u32, base_delay_ms: u64, max_delay_ms: u64) -> Self {
189        Self::Idempotent {
190            max_attempts,
191            base_delay_ms,
192            max_delay_ms,
193        }
194    }
195
196    pub fn max_attempts(self) -> u32 {
197        match self {
198            Self::Never => 1,
199            Self::Safe { max_attempts, .. } | Self::Idempotent { max_attempts, .. } => {
200                max_attempts.max(1)
201            }
202        }
203    }
204
205    pub fn delay_ms_for_retry(self, retry_index: u32, requested_after_ms: Option<u64>) -> u64 {
206        let (base_delay_ms, max_delay_ms) = match self {
207            Self::Never => return 0,
208            Self::Safe {
209                base_delay_ms,
210                max_delay_ms,
211                ..
212            }
213            | Self::Idempotent {
214                base_delay_ms,
215                max_delay_ms,
216                ..
217            } => (base_delay_ms, max_delay_ms),
218        };
219        let multiplier = 1_u64.checked_shl(retry_index).unwrap_or(u64::MAX);
220        let backoff = base_delay_ms.saturating_mul(multiplier);
221        let delay = requested_after_ms.unwrap_or(backoff);
222        if max_delay_ms == 0 {
223            delay
224        } else {
225            delay.min(max_delay_ms)
226        }
227    }
228
229    pub fn requires_idempotency_key(self) -> bool {
230        matches!(self, Self::Idempotent { .. })
231    }
232}
233
234fn default_tool_retry_policy() -> ToolRetryPolicy {
235    ToolRetryPolicy::default()
236}
237
238fn is_default_tool_retry_policy(policy: &ToolRetryPolicy) -> bool {
239    *policy == ToolRetryPolicy::default()
240}
241
242#[derive(
243    Clone,
244    Copy,
245    Debug,
246    Default,
247    PartialEq,
248    Eq,
249    PartialOrd,
250    Ord,
251    serde::Serialize,
252    serde::Deserialize,
253)]
254#[serde(rename_all = "snake_case")]
255pub enum ToolAvailability {
256    /// Keep the tool out of the current surface entirely.
257    ///
258    /// The definition can remain in registry state so host or authority
259    /// overrides survive refreshes, but the model cannot search, see, or call
260    /// the tool.
261    #[default]
262    Off,
263    /// Include the tool in the searchable catalog, but not in the model's
264    /// callable tool list.
265    Searchable,
266    /// Include the tool in the model's callable tool list, without featuring
267    /// it in prompt-side tool documentation.
268    Callable,
269    /// Include the tool in the callable list and feature it in prompt-side
270    /// tool documentation.
271    Showcased,
272}
273
274impl ToolAvailability {
275    pub fn is_searchable(self) -> bool {
276        self >= Self::Searchable
277    }
278
279    pub fn is_callable(self) -> bool {
280        self >= Self::Callable
281    }
282
283    pub fn is_showcased(self) -> bool {
284        self >= Self::Showcased
285    }
286}
287
288#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
289pub struct ToolAvailabilityConfig {
290    pub standard: ToolAvailability,
291    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
292    pub modes: std::collections::HashMap<String, ToolAvailability>,
293}
294
295impl ToolAvailabilityConfig {
296    pub fn same(availability: ToolAvailability) -> Self {
297        Self {
298            standard: availability,
299            modes: std::collections::HashMap::new(),
300        }
301    }
302
303    pub fn showcased() -> Self {
304        Self::same(ToolAvailability::Showcased)
305    }
306
307    pub fn callable() -> Self {
308        Self::same(ToolAvailability::Callable)
309    }
310
311    pub fn off() -> Self {
312        Self::same(ToolAvailability::Off)
313    }
314
315    pub fn for_mode(&self, mode: &ExecutionMode) -> ToolAvailability {
316        self.modes
317            .get(mode.plugin_id())
318            .copied()
319            .unwrap_or(self.standard)
320    }
321}
322
323impl Default for ToolAvailabilityConfig {
324    fn default() -> Self {
325        Self::showcased()
326    }
327}
328
329#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum ToolActivation {
332    #[default]
333    Always,
334    Internal,
335}
336
337fn is_default_tool_availability_config(config: &ToolAvailabilityConfig) -> bool {
338    *config == ToolAvailabilityConfig::default()
339}
340
341fn is_default_tool_activation(activation: &ToolActivation) -> bool {
342    *activation == ToolActivation::default()
343}
344
345#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
346pub struct ToolDiscoveryMetadata {
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub namespace: Option<String>,
349    #[serde(default, skip_serializing_if = "Vec::is_empty")]
350    pub aliases: Vec<String>,
351}
352
353impl ToolDiscoveryMetadata {
354    pub fn is_empty(&self) -> bool {
355        self.namespace.is_none() && self.aliases.is_empty()
356    }
357}
358
359#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
360#[serde(tag = "kind", rename_all = "snake_case")]
361pub enum ToolOutputContract {
362    #[default]
363    Static,
364    FromInputSchema {
365        input_field: String,
366        #[serde(default, skip_serializing_if = "Option::is_none")]
367        default_schema: Option<serde_json::Value>,
368    },
369}
370
371impl ToolOutputContract {
372    pub fn from_input_schema(
373        input_field: impl Into<String>,
374        default_schema: Option<serde_json::Value>,
375    ) -> Self {
376        Self::FromInputSchema {
377            input_field: input_field.into(),
378            default_schema,
379        }
380    }
381
382    pub fn is_static(&self) -> bool {
383        matches!(self, Self::Static)
384    }
385
386    fn return_type_label(&self, static_schema: &serde_json::Value) -> String {
387        match self {
388            Self::Static => compact_schema_label(static_schema),
389            Self::FromInputSchema { .. } => "T".to_string(),
390        }
391    }
392
393    fn type_parameter_suffix(&self) -> Option<String> {
394        match self {
395            Self::Static => None,
396            Self::FromInputSchema { default_schema, .. } => {
397                let default = default_schema
398                    .as_ref()
399                    .map(compact_schema_label)
400                    .unwrap_or_else(|| "any".to_string());
401                Some(format!("<T = {default}>"))
402            }
403        }
404    }
405
406    fn apply_type_witness_parameter(&self, params: &mut [ParameterDoc]) {
407        let Self::FromInputSchema { input_field, .. } = self else {
408            return;
409        };
410        if let Some(param) = params.iter_mut().find(|param| param.name == *input_field) {
411            param.type_label = "TypeSpec<T>".to_string();
412            param.nullable = false;
413            param.default_value = None;
414            param.enum_values.clear();
415            param.minimum = None;
416            param.maximum = None;
417            param.min_length = None;
418            param.max_length = None;
419            param.min_items = None;
420            param.max_items = None;
421            param.item_type = None;
422        }
423    }
424
425    fn return_fields(&self, static_schema: &serde_json::Value) -> Vec<serde_json::Value> {
426        match self {
427            Self::Static => return_field_metadata(static_schema),
428            Self::FromInputSchema { .. } => Vec::new(),
429        }
430    }
431}
432
433/// Cheap tool metadata exposed to prompts, catalogs, UI, and availability checks.
434#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
435pub struct ToolManifest {
436    pub name: String,
437    #[serde(default, skip_serializing_if = "String::is_empty")]
438    pub description: String,
439    #[serde(default, skip_serializing_if = "is_default_tool_availability_config")]
440    pub availability: ToolAvailabilityConfig,
441    #[serde(default, skip_serializing_if = "is_default_tool_activation")]
442    pub activation: ToolActivation,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub availability_override: Option<ToolAvailability>,
445    #[serde(default, skip_serializing_if = "ToolDiscoveryMetadata::is_empty")]
446    pub discovery: ToolDiscoveryMetadata,
447    #[serde(
448        default = "default_tool_execution_mode",
449        skip_serializing_if = "is_default_tool_execution_mode"
450    )]
451    pub execution_mode: ToolExecutionMode,
452    #[serde(
453        default = "default_tool_retry_policy",
454        skip_serializing_if = "is_default_tool_retry_policy"
455    )]
456    pub retry_policy: ToolRetryPolicy,
457}
458
459impl ToolManifest {
460    pub fn effective_availability(&self, mode: &ExecutionMode) -> ToolAvailability {
461        self.availability_override
462            .unwrap_or_else(|| self.availability.for_mode(mode))
463    }
464}
465
466/// Heavy tool contract resolved only when a prompt or call needs schemas/docs.
467#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
468pub struct ToolContract {
469    #[serde(default = "ToolDefinition::default_input_schema")]
470    pub input_schema: serde_json::Value,
471    #[serde(default)]
472    pub output_schema: serde_json::Value,
473    #[serde(default, skip_serializing_if = "Vec::is_empty")]
474    pub input_schema_projections: Vec<SchemaProjectionOverride>,
475    #[serde(default, skip_serializing_if = "Vec::is_empty")]
476    pub output_schema_projections: Vec<SchemaProjectionOverride>,
477    #[serde(default, skip_serializing_if = "ToolOutputContract::is_static")]
478    pub output_contract: ToolOutputContract,
479    #[serde(default, skip_serializing_if = "Vec::is_empty")]
480    pub examples: Vec<String>,
481}
482
483impl ToolContract {
484    pub fn compact_contract(&self, manifest: &ToolManifest) -> CompactToolContract {
485        self.compact_contract_with_example_limit(manifest, COMPACT_TOOL_EXAMPLE_LIMIT)
486    }
487
488    pub fn compact_contract_with_example_limit(
489        &self,
490        manifest: &ToolManifest,
491        example_limit: usize,
492    ) -> CompactToolContract {
493        CompactToolContract {
494            name: manifest.name.clone(),
495            signature: self.input_signature(manifest),
496            returns: self.output_summary(),
497            parameters: self.parameter_metadata(),
498            return_fields: self.output_contract.return_fields(&self.output_schema),
499            description: manifest.description.trim().to_string(),
500            examples: compact_examples(&self.examples, example_limit),
501        }
502    }
503
504    pub fn input_signature(&self, manifest: &ToolManifest) -> String {
505        let params = self
506            .parameter_docs()
507            .into_iter()
508            .map(|p| p.signature_fragment())
509            .collect::<Vec<_>>();
510        format!(
511            "{}{}({})",
512            manifest.name,
513            self.output_contract
514                .type_parameter_suffix()
515                .unwrap_or_default(),
516            params.join(", ")
517        )
518    }
519
520    pub fn output_summary(&self) -> String {
521        self.output_contract.return_type_label(&self.output_schema)
522    }
523
524    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
525        self.parameter_docs()
526            .into_iter()
527            .map(|param| param.into_value())
528            .collect()
529    }
530
531    pub fn model_tool(&self, manifest: &ToolManifest) -> ModelTool {
532        ModelTool {
533            name: manifest.name.clone(),
534            description: manifest.description.clone(),
535            input_schema: self.input_schema.clone(),
536            output_schema: self.output_schema.clone(),
537            input_schema_projections: self.input_schema_projections.clone(),
538            output_schema_projections: self.output_schema_projections.clone(),
539        }
540    }
541
542    fn parameter_docs(&self) -> Vec<ParameterDoc> {
543        let mut params = schema_parameter_docs(&self.input_schema);
544        self.output_contract
545            .apply_type_witness_parameter(&mut params);
546        params
547    }
548}
549
550/// Static authoring helper for tools.
551#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
552pub struct ToolDefinition {
553    pub name: String,
554    #[serde(default, skip_serializing_if = "String::is_empty")]
555    pub description: String,
556    #[serde(default = "ToolDefinition::default_input_schema")]
557    pub input_schema: serde_json::Value,
558    #[serde(default)]
559    pub output_schema: serde_json::Value,
560    #[serde(default, skip_serializing_if = "Vec::is_empty")]
561    pub input_schema_projections: Vec<SchemaProjectionOverride>,
562    #[serde(default, skip_serializing_if = "Vec::is_empty")]
563    pub output_schema_projections: Vec<SchemaProjectionOverride>,
564    #[serde(default, skip_serializing_if = "ToolOutputContract::is_static")]
565    pub output_contract: ToolOutputContract,
566    #[serde(default, skip_serializing_if = "Vec::is_empty")]
567    pub examples: Vec<String>,
568    #[serde(default, skip_serializing_if = "is_default_tool_availability_config")]
569    pub availability: ToolAvailabilityConfig,
570    #[serde(default, skip_serializing_if = "is_default_tool_activation")]
571    pub activation: ToolActivation,
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub availability_override: Option<ToolAvailability>,
574    #[serde(default, skip_serializing_if = "ToolDiscoveryMetadata::is_empty")]
575    pub discovery: ToolDiscoveryMetadata,
576    /// How this tool should be scheduled relative to peers when the model
577    /// emits a batch of tool calls. Defaults to [`ToolExecutionMode::Parallel`].
578    #[serde(
579        default = "default_tool_execution_mode",
580        skip_serializing_if = "is_default_tool_execution_mode"
581    )]
582    pub execution_mode: ToolExecutionMode,
583    /// Whether this tool may be retried automatically after a failure that
584    /// explicitly opts into safe retry.
585    #[serde(
586        default = "default_tool_retry_policy",
587        skip_serializing_if = "is_default_tool_retry_policy"
588    )]
589    pub retry_policy: ToolRetryPolicy,
590}
591
592#[derive(Clone, Debug, PartialEq, Eq)]
593pub struct ModelTool {
594    pub name: String,
595    pub description: String,
596    pub input_schema: serde_json::Value,
597    pub output_schema: serde_json::Value,
598    pub input_schema_projections: Vec<SchemaProjectionOverride>,
599    pub output_schema_projections: Vec<SchemaProjectionOverride>,
600}
601
602#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
603pub struct SchemaProjectionOverride {
604    pub profile: String,
605    pub schema: serde_json::Value,
606}
607
608const COMPACT_TOOL_EXAMPLE_LIMIT: usize = 2;
609const COMPACT_TOOL_EXAMPLE_CHAR_LIMIT: usize = 240;
610
611#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
612pub struct CompactToolContract {
613    pub name: String,
614    pub signature: String,
615    pub returns: String,
616    #[serde(default, skip_serializing_if = "Vec::is_empty")]
617    pub parameters: Vec<serde_json::Value>,
618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
619    pub return_fields: Vec<serde_json::Value>,
620    #[serde(default, skip_serializing_if = "String::is_empty")]
621    pub description: String,
622    #[serde(default, skip_serializing_if = "Vec::is_empty")]
623    pub examples: Vec<String>,
624}
625
626impl CompactToolContract {
627    pub fn render_signature_head(&self) -> String {
628        format!("{} -> {}", self.signature.trim(), self.returns.trim())
629    }
630
631    pub fn render_signature(&self) -> String {
632        let mut sections = vec![self.render_signature_head()];
633        let parameter_lines = self
634            .parameters
635            .iter()
636            .filter_map(compact_doc_line)
637            .collect::<Vec<_>>();
638        if !parameter_lines.is_empty() {
639            sections.push(format!("Parameters:\n{}", parameter_lines.join("\n")));
640        }
641        let return_field_lines = self
642            .return_fields
643            .iter()
644            .filter_map(compact_doc_line)
645            .collect::<Vec<_>>();
646        if !return_field_lines.is_empty() {
647            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
648        }
649        sections.join("\n")
650    }
651
652    pub fn render_returns(&self) -> String {
653        let mut sections = Vec::new();
654        let return_field_lines = self
655            .return_fields
656            .iter()
657            .filter_map(compact_doc_line)
658            .collect::<Vec<_>>();
659        if !return_field_lines.is_empty() {
660            sections.push(format!("Return fields:\n{}", return_field_lines.join("\n")));
661        }
662        sections.join("\n")
663    }
664
665    pub fn render_markdown(&self) -> String {
666        let mut sections = vec![format!("### {}", self.render_signature_head())];
667        if !self.description.trim().is_empty() {
668            sections.push(self.description.trim().to_string());
669        }
670        if !self.parameters.is_empty() {
671            sections.push(format!(
672                "Parameters:\n{}",
673                self.parameters
674                    .iter()
675                    .filter_map(compact_doc_line)
676                    .collect::<Vec<_>>()
677                    .join("\n")
678            ));
679        }
680        if !self.return_fields.is_empty() {
681            sections.push(format!(
682                "Return fields:\n{}",
683                self.return_fields
684                    .iter()
685                    .filter_map(compact_doc_line)
686                    .collect::<Vec<_>>()
687                    .join("\n")
688            ));
689        }
690        if !self.examples.is_empty() {
691            sections.push(format!("Examples: {}", self.examples.join("; ")));
692        }
693        sections.join("\n")
694    }
695}
696
697impl ToolDefinition {
698    pub fn raw(
699        name: impl Into<String>,
700        description: impl Into<String>,
701        input_schema: serde_json::Value,
702        output_schema: serde_json::Value,
703    ) -> Self {
704        Self {
705            name: name.into(),
706            description: description.into(),
707            input_schema,
708            output_schema,
709            input_schema_projections: Vec::new(),
710            output_schema_projections: Vec::new(),
711            output_contract: ToolOutputContract::Static,
712            examples: Vec::new(),
713            availability: ToolAvailabilityConfig::showcased(),
714            activation: ToolActivation::Always,
715            availability_override: None,
716            discovery: ToolDiscoveryMetadata::default(),
717            execution_mode: ToolExecutionMode::Parallel,
718            retry_policy: ToolRetryPolicy::Never,
719        }
720    }
721
722    pub fn typed<Args, Output>(name: impl Into<String>, description: impl Into<String>) -> Self
723    where
724        Args: schemars::JsonSchema,
725        Output: schemars::JsonSchema,
726    {
727        Self::raw(
728            name,
729            description,
730            schema_for::<Args>(),
731            schema_for::<Output>(),
732        )
733    }
734
735    pub fn with_examples(mut self, examples: Vec<String>) -> Self {
736        self.examples = examples;
737        self
738    }
739
740    pub fn with_availability(mut self, availability: ToolAvailabilityConfig) -> Self {
741        self.availability = availability;
742        self
743    }
744
745    pub fn with_activation(mut self, activation: ToolActivation) -> Self {
746        self.activation = activation;
747        self
748    }
749
750    pub fn with_discovery(mut self, discovery: ToolDiscoveryMetadata) -> Self {
751        self.discovery = discovery;
752        self
753    }
754
755    pub fn with_execution_mode(mut self, execution_mode: ToolExecutionMode) -> Self {
756        self.execution_mode = execution_mode;
757        self
758    }
759
760    pub fn with_retry_policy(mut self, retry_policy: ToolRetryPolicy) -> Self {
761        self.retry_policy = retry_policy;
762        self
763    }
764
765    pub fn with_output_contract(mut self, output_contract: ToolOutputContract) -> Self {
766        self.output_contract = output_contract;
767        self
768    }
769
770    pub fn with_input_schema_projection(
771        mut self,
772        profile: impl Into<String>,
773        schema: serde_json::Value,
774    ) -> Self {
775        let profile = profile.into();
776        self.input_schema_projections
777            .retain(|projection| projection.profile != profile);
778        self.input_schema_projections
779            .push(SchemaProjectionOverride { profile, schema });
780        self
781    }
782
783    pub fn with_output_schema_projection(
784        mut self,
785        profile: impl Into<String>,
786        schema: serde_json::Value,
787    ) -> Self {
788        let profile = profile.into();
789        self.output_schema_projections
790            .retain(|projection| projection.profile != profile);
791        self.output_schema_projections
792            .push(SchemaProjectionOverride { profile, schema });
793        self
794    }
795
796    pub fn with_output_from_input_schema(
797        self,
798        input_field: impl Into<String>,
799        default_schema: Option<serde_json::Value>,
800    ) -> Self {
801        self.with_output_contract(ToolOutputContract::from_input_schema(
802            input_field,
803            default_schema,
804        ))
805    }
806
807    pub fn default_input_schema() -> serde_json::Value {
808        serde_json::json!({
809            "type": "object",
810            "properties": {},
811            "additionalProperties": true
812        })
813    }
814
815    pub fn input_signature(&self) -> String {
816        let params = self
817            .parameter_docs()
818            .into_iter()
819            .map(|p| p.signature_fragment())
820            .collect::<Vec<_>>();
821        format!(
822            "{}{}({})",
823            self.name,
824            self.output_contract
825                .type_parameter_suffix()
826                .unwrap_or_default(),
827            params.join(", ")
828        )
829    }
830
831    pub fn output_summary(&self) -> String {
832        self.contract().output_summary()
833    }
834
835    pub fn signature(&self) -> String {
836        format!("{} -> {}", self.input_signature(), self.output_summary())
837    }
838
839    pub fn compact_contract(&self) -> CompactToolContract {
840        self.compact_contract_with_example_limit(COMPACT_TOOL_EXAMPLE_LIMIT)
841    }
842
843    pub fn compact_contract_with_example_limit(&self, example_limit: usize) -> CompactToolContract {
844        self.contract()
845            .compact_contract_with_example_limit(&self.manifest(), example_limit)
846    }
847
848    pub fn effective_availability(&self, mode: &ExecutionMode) -> ToolAvailability {
849        self.availability_override
850            .unwrap_or_else(|| self.availability.for_mode(mode))
851    }
852
853    pub fn model_tool(&self) -> ModelTool {
854        self.contract().model_tool(&self.manifest())
855    }
856
857    pub fn manifest(&self) -> ToolManifest {
858        ToolManifest {
859            name: self.name.clone(),
860            description: self.description.clone(),
861            availability: self.availability.clone(),
862            activation: self.activation,
863            availability_override: self.availability_override,
864            discovery: self.discovery.clone(),
865            execution_mode: self.execution_mode,
866            retry_policy: self.retry_policy,
867        }
868    }
869
870    pub fn contract(&self) -> ToolContract {
871        ToolContract {
872            input_schema: self.input_schema.clone(),
873            output_schema: self.output_schema.clone(),
874            input_schema_projections: self.input_schema_projections.clone(),
875            output_schema_projections: self.output_schema_projections.clone(),
876            output_contract: self.output_contract.clone(),
877            examples: self.examples.clone(),
878        }
879    }
880
881    pub fn from_manifest_and_contract(manifest: ToolManifest, contract: ToolContract) -> Self {
882        Self {
883            name: manifest.name,
884            description: manifest.description,
885            input_schema: contract.input_schema,
886            output_schema: contract.output_schema,
887            input_schema_projections: contract.input_schema_projections,
888            output_schema_projections: contract.output_schema_projections,
889            output_contract: contract.output_contract,
890            examples: contract.examples,
891            availability: manifest.availability,
892            activation: manifest.activation,
893            availability_override: manifest.availability_override,
894            discovery: manifest.discovery,
895            execution_mode: manifest.execution_mode,
896            retry_policy: manifest.retry_policy,
897        }
898    }
899
900    pub fn format_tool_docs(tools: &[ToolDefinition]) -> String {
901        Self::format_tool_docs_iter(tools.iter())
902    }
903
904    pub fn format_tool_docs_iter<'a>(
905        tools: impl IntoIterator<Item = &'a ToolDefinition>,
906    ) -> String {
907        tools
908            .into_iter()
909            .map(|tool| tool.compact_contract().render_markdown())
910            .collect::<Vec<_>>()
911            .join("\n\n")
912    }
913
914    pub fn parameter_metadata(&self) -> Vec<serde_json::Value> {
915        self.parameter_docs()
916            .into_iter()
917            .map(|param| param.into_value())
918            .collect()
919    }
920
921    fn parameter_docs(&self) -> Vec<ParameterDoc> {
922        let mut params = schema_parameter_docs(&self.input_schema);
923        self.output_contract
924            .apply_type_witness_parameter(&mut params);
925        params
926    }
927}
928
929pub fn schema_for<T>() -> serde_json::Value
930where
931    T: schemars::JsonSchema,
932{
933    serde_json::to_value(schemars::schema_for!(T)).unwrap_or_else(|_| serde_json::json!({}))
934}
935
936#[derive(Clone, Debug, PartialEq)]
937struct ParameterDoc {
938    name: String,
939    type_label: String,
940    required: bool,
941    nullable: bool,
942    description: Option<String>,
943    default_value: Option<serde_json::Value>,
944    enum_values: Vec<serde_json::Value>,
945    minimum: Option<serde_json::Value>,
946    maximum: Option<serde_json::Value>,
947    min_length: Option<u64>,
948    max_length: Option<u64>,
949    min_items: Option<u64>,
950    max_items: Option<u64>,
951    item_type: Option<String>,
952}
953
954impl ParameterDoc {
955    fn signature_fragment(&self) -> String {
956        let mut out = if self.required {
957            format!("{}: {}", self.name, self.type_label)
958        } else {
959            format!("{}?: {}", self.name, self.type_label)
960        };
961        let constraints = self.constraint_fragments();
962        if !constraints.is_empty() {
963            out.push(' ');
964            out.push_str(&constraints.join(" "));
965        }
966        if let Some(default) = &self.default_value {
967            out.push_str(" = ");
968            out.push_str(&display_default_value(default));
969        }
970        out
971    }
972
973    fn constraint_fragments(&self) -> Vec<String> {
974        let mut out = Vec::new();
975        if !self.enum_values.is_empty() && !self.type_label.starts_with("enum[") {
976            out.push(format!(
977                "in {}",
978                self.enum_values
979                    .iter()
980                    .map(display_default_value)
981                    .collect::<Vec<_>>()
982                    .join("|")
983            ));
984        }
985        if let Some(minimum) = &self.minimum {
986            out.push(format!(">= {}", display_default_value(minimum)));
987        }
988        if let Some(maximum) = &self.maximum {
989            out.push(format!("<= {}", display_default_value(maximum)));
990        }
991        if let Some(min_length) = self.min_length {
992            out.push(format!("min_len {min_length}"));
993        }
994        if let Some(max_length) = self.max_length {
995            out.push(format!("max_len {max_length}"));
996        }
997        if let Some(min_items) = self.min_items {
998            out.push(format!("min_items {min_items}"));
999        }
1000        if let Some(max_items) = self.max_items {
1001            out.push(format!("max_items {max_items}"));
1002        }
1003        out
1004    }
1005
1006    fn into_value(self) -> serde_json::Value {
1007        let mut out = serde_json::Map::new();
1008        out.insert("name".to_string(), serde_json::json!(self.name));
1009        out.insert("type".to_string(), serde_json::json!(self.type_label));
1010        out.insert("required".to_string(), serde_json::json!(self.required));
1011        if self.nullable {
1012            out.insert("nullable".to_string(), serde_json::json!(true));
1013        }
1014        if let Some(description) = self.description.filter(|value| !value.trim().is_empty()) {
1015            out.insert("description".to_string(), serde_json::json!(description));
1016        }
1017        if let Some(default_value) = self.default_value {
1018            out.insert("default".to_string(), default_value);
1019        }
1020        if !self.enum_values.is_empty() {
1021            out.insert("enum".to_string(), serde_json::json!(self.enum_values));
1022        }
1023        if let Some(value) = self.minimum {
1024            out.insert("minimum".to_string(), value);
1025        }
1026        if let Some(value) = self.maximum {
1027            out.insert("maximum".to_string(), value);
1028        }
1029        if let Some(value) = self.min_length {
1030            out.insert("min_length".to_string(), serde_json::json!(value));
1031        }
1032        if let Some(value) = self.max_length {
1033            out.insert("max_length".to_string(), serde_json::json!(value));
1034        }
1035        if let Some(value) = self.min_items {
1036            out.insert("min_items".to_string(), serde_json::json!(value));
1037        }
1038        if let Some(value) = self.max_items {
1039            out.insert("max_items".to_string(), serde_json::json!(value));
1040        }
1041        if let Some(value) = self.item_type {
1042            out.insert("items".to_string(), serde_json::json!(value));
1043        }
1044        out.insert(
1045            "signature".to_string(),
1046            serde_json::json!(parameter_signature_from_value(&out)),
1047        );
1048        serde_json::Value::Object(out)
1049    }
1050}
1051
1052#[derive(Clone, Debug, PartialEq)]
1053struct FieldDoc {
1054    path: String,
1055    type_label: String,
1056    required: bool,
1057    nullable: bool,
1058    description: Option<String>,
1059    enum_values: Vec<serde_json::Value>,
1060    minimum: Option<serde_json::Value>,
1061    maximum: Option<serde_json::Value>,
1062    min_length: Option<u64>,
1063    max_length: Option<u64>,
1064    min_items: Option<u64>,
1065    max_items: Option<u64>,
1066    item_type: Option<String>,
1067}
1068
1069impl FieldDoc {
1070    fn from_schema(path: String, schema: &serde_json::Value, required: bool) -> Self {
1071        let (type_label, nullable) = schema_type_label_and_nullability(schema);
1072        Self {
1073            path,
1074            type_label,
1075            required,
1076            nullable,
1077            description: schema
1078                .get("description")
1079                .and_then(serde_json::Value::as_str)
1080                .map(str::to_string),
1081            enum_values: schema
1082                .get("enum")
1083                .and_then(serde_json::Value::as_array)
1084                .cloned()
1085                .unwrap_or_default()
1086                .into_iter()
1087                .filter(|value| !value.is_null())
1088                .collect(),
1089            minimum: schema
1090                .get("minimum")
1091                .or_else(|| schema.get("exclusiveMinimum"))
1092                .cloned(),
1093            maximum: schema
1094                .get("maximum")
1095                .or_else(|| schema.get("exclusiveMaximum"))
1096                .cloned(),
1097            min_length: schema.get("minLength").and_then(serde_json::Value::as_u64),
1098            max_length: schema.get("maxLength").and_then(serde_json::Value::as_u64),
1099            min_items: schema.get("minItems").and_then(serde_json::Value::as_u64),
1100            max_items: schema.get("maxItems").and_then(serde_json::Value::as_u64),
1101            item_type: schema
1102                .get("items")
1103                .map(schema_type_label)
1104                .filter(|value| value != "any"),
1105        }
1106    }
1107
1108    fn into_value(self) -> serde_json::Value {
1109        let mut out = serde_json::Map::new();
1110        out.insert("path".to_string(), serde_json::json!(self.path));
1111        out.insert("type".to_string(), serde_json::json!(self.type_label));
1112        out.insert("required".to_string(), serde_json::json!(self.required));
1113        if self.nullable {
1114            out.insert("nullable".to_string(), serde_json::json!(true));
1115        }
1116        if let Some(description) = self.description.filter(|value| !value.trim().is_empty()) {
1117            out.insert("description".to_string(), serde_json::json!(description));
1118        }
1119        if !self.enum_values.is_empty() {
1120            out.insert("enum".to_string(), serde_json::json!(self.enum_values));
1121        }
1122        if let Some(value) = self.minimum {
1123            out.insert("minimum".to_string(), value);
1124        }
1125        if let Some(value) = self.maximum {
1126            out.insert("maximum".to_string(), value);
1127        }
1128        if let Some(value) = self.min_length {
1129            out.insert("min_length".to_string(), serde_json::json!(value));
1130        }
1131        if let Some(value) = self.max_length {
1132            out.insert("max_length".to_string(), serde_json::json!(value));
1133        }
1134        if let Some(value) = self.min_items {
1135            out.insert("min_items".to_string(), serde_json::json!(value));
1136        }
1137        if let Some(value) = self.max_items {
1138            out.insert("max_items".to_string(), serde_json::json!(value));
1139        }
1140        if let Some(value) = self.item_type {
1141            out.insert("items".to_string(), serde_json::json!(value));
1142        }
1143        out.insert(
1144            "signature".to_string(),
1145            serde_json::json!(field_signature_from_value(&out)),
1146        );
1147        serde_json::Value::Object(out)
1148    }
1149}
1150
1151fn schema_parameter_docs(schema: &serde_json::Value) -> Vec<ParameterDoc> {
1152    let required_order = schema
1153        .get("required")
1154        .and_then(serde_json::Value::as_array)
1155        .into_iter()
1156        .flatten()
1157        .filter_map(serde_json::Value::as_str)
1158        .collect::<Vec<_>>();
1159    let required = required_order
1160        .iter()
1161        .copied()
1162        .collect::<std::collections::BTreeSet<_>>();
1163    let Some(properties) = schema
1164        .get("properties")
1165        .and_then(serde_json::Value::as_object)
1166    else {
1167        return Vec::new();
1168    };
1169    let mut params = properties
1170        .iter()
1171        .map(|(name, schema)| parameter_doc(name, schema, required.contains(name.as_str())))
1172        .collect::<Vec<_>>();
1173    params.sort_by(|left, right| {
1174        match (
1175            required_order
1176                .iter()
1177                .position(|name| *name == left.name.as_str()),
1178            required_order
1179                .iter()
1180                .position(|name| *name == right.name.as_str()),
1181        ) {
1182            (Some(left), Some(right)) => left.cmp(&right),
1183            (Some(_), None) => std::cmp::Ordering::Less,
1184            (None, Some(_)) => std::cmp::Ordering::Greater,
1185            (None, None) => left.name.cmp(&right.name),
1186        }
1187    });
1188    params
1189}
1190
1191fn return_field_metadata(schema: &serde_json::Value) -> Vec<serde_json::Value> {
1192    let mut fields = Vec::new();
1193    collect_return_fields("", schema, true, &mut fields);
1194    merge_return_fields(fields)
1195        .into_iter()
1196        .map(FieldDoc::into_value)
1197        .collect()
1198}
1199
1200fn collect_return_fields(
1201    path: &str,
1202    schema: &serde_json::Value,
1203    required: bool,
1204    fields: &mut Vec<FieldDoc>,
1205) {
1206    if let Some(any_of) = schema
1207        .get("anyOf")
1208        .or_else(|| schema.get("oneOf"))
1209        .and_then(serde_json::Value::as_array)
1210    {
1211        if should_emit_return_field(path, schema) {
1212            fields.push(FieldDoc::from_schema(path.to_string(), schema, required));
1213        }
1214        for subschema in any_of {
1215            collect_return_fields(path, subschema, required, fields);
1216        }
1217        return;
1218    }
1219
1220    let schema_type = schema
1221        .get("type")
1222        .and_then(serde_json::Value::as_str)
1223        .map(str::to_string)
1224        .or_else(|| {
1225            schema
1226                .get("type")
1227                .and_then(serde_json::Value::as_array)
1228                .and_then(|types| {
1229                    let non_null = types
1230                        .iter()
1231                        .filter_map(serde_json::Value::as_str)
1232                        .filter(|ty| *ty != "null")
1233                        .collect::<Vec<_>>();
1234                    if non_null.len() == 1 {
1235                        Some(non_null[0].to_string())
1236                    } else {
1237                        None
1238                    }
1239                })
1240        });
1241
1242    match schema_type.as_deref() {
1243        Some("object") => {
1244            if should_emit_return_field(path, schema) {
1245                fields.push(FieldDoc::from_schema(path.to_string(), schema, required));
1246            }
1247            let required_properties = schema
1248                .get("required")
1249                .and_then(serde_json::Value::as_array)
1250                .into_iter()
1251                .flatten()
1252                .filter_map(serde_json::Value::as_str)
1253                .collect::<std::collections::BTreeSet<_>>();
1254            if let Some(properties) = schema
1255                .get("properties")
1256                .and_then(serde_json::Value::as_object)
1257            {
1258                for (name, property_schema) in properties {
1259                    collect_return_fields(
1260                        &join_compact_path(path, name),
1261                        property_schema,
1262                        required_properties.contains(name.as_str()),
1263                        fields,
1264                    );
1265                }
1266            }
1267        }
1268        Some("array") => {
1269            if should_emit_return_field(path, schema) {
1270                fields.push(FieldDoc::from_schema(path.to_string(), schema, required));
1271            }
1272            if let Some(items) = schema.get("items") {
1273                collect_return_fields(&format!("{path}[]"), items, true, fields);
1274            }
1275        }
1276        _ => {
1277            if !path.is_empty() {
1278                fields.push(FieldDoc::from_schema(path.to_string(), schema, required));
1279            }
1280        }
1281    }
1282}
1283
1284fn should_emit_return_field(path: &str, schema: &serde_json::Value) -> bool {
1285    !path.is_empty()
1286        && (schema
1287            .get("description")
1288            .and_then(serde_json::Value::as_str)
1289            .is_some_and(|value| !value.trim().is_empty())
1290            || schema.get("enum").is_some()
1291            || schema.get("minimum").is_some()
1292            || schema.get("maximum").is_some()
1293            || schema.get("minLength").is_some()
1294            || schema.get("maxLength").is_some()
1295            || schema.get("minItems").is_some()
1296            || schema.get("maxItems").is_some())
1297}
1298
1299fn join_compact_path(parent: &str, child: &str) -> String {
1300    if parent.is_empty() {
1301        child.to_string()
1302    } else {
1303        format!("{parent}.{child}")
1304    }
1305}
1306
1307fn merge_return_fields(fields: Vec<FieldDoc>) -> Vec<FieldDoc> {
1308    let mut merged = Vec::<FieldDoc>::new();
1309    for field in fields {
1310        if let Some(existing) = merged
1311            .iter_mut()
1312            .find(|existing| existing.path == field.path)
1313        {
1314            existing.merge(field);
1315        } else {
1316            merged.push(field);
1317        }
1318    }
1319    merged
1320}
1321
1322impl FieldDoc {
1323    fn merge(&mut self, other: FieldDoc) {
1324        self.type_label = merge_type_labels(&self.type_label, &other.type_label);
1325        self.required |= other.required;
1326        self.nullable |= other.nullable || type_label_is_nullable(&other.type_label);
1327        if self.nullable && !type_label_is_nullable(&self.type_label) {
1328            self.type_label = merge_type_labels(&self.type_label, "null");
1329        }
1330        if self
1331            .description
1332            .as_deref()
1333            .is_none_or(|value| value.trim().is_empty())
1334        {
1335            self.description = other.description;
1336        }
1337        for value in other.enum_values {
1338            if !self.enum_values.iter().any(|existing| existing == &value) {
1339                self.enum_values.push(value);
1340            }
1341        }
1342        if self.minimum.is_none() {
1343            self.minimum = other.minimum;
1344        }
1345        if self.maximum.is_none() {
1346            self.maximum = other.maximum;
1347        }
1348        if self.min_length.is_none() {
1349            self.min_length = other.min_length;
1350        }
1351        if self.max_length.is_none() {
1352            self.max_length = other.max_length;
1353        }
1354        if self.min_items.is_none() {
1355            self.min_items = other.min_items;
1356        }
1357        if self.max_items.is_none() {
1358            self.max_items = other.max_items;
1359        }
1360        if self.item_type.is_none() {
1361            self.item_type = other.item_type;
1362        }
1363    }
1364}
1365
1366fn merge_type_labels(left: &str, right: &str) -> String {
1367    let mut labels = Vec::<String>::new();
1368    for label in left.split(" | ").chain(right.split(" | ")) {
1369        let label = label.trim();
1370        if label.is_empty() || label == "any" && (!left.is_empty() || !right.is_empty()) {
1371            continue;
1372        }
1373        if !labels.iter().any(|existing| existing == label) {
1374            labels.push(label.to_string());
1375        }
1376    }
1377    if labels.is_empty() {
1378        return "any".to_string();
1379    }
1380    labels.sort_by(|left, right| match (*left == "null", *right == "null") {
1381        (true, false) => std::cmp::Ordering::Greater,
1382        (false, true) => std::cmp::Ordering::Less,
1383        _ => std::cmp::Ordering::Equal,
1384    });
1385    labels.join(" | ")
1386}
1387
1388fn type_label_is_nullable(label: &str) -> bool {
1389    label.split(" | ").any(|part| part.trim() == "null")
1390}
1391
1392fn parameter_doc(name: &str, schema: &serde_json::Value, required: bool) -> ParameterDoc {
1393    let (type_label, nullable) = schema_type_label_and_nullability(schema);
1394    ParameterDoc {
1395        name: name.to_string(),
1396        type_label,
1397        required,
1398        nullable,
1399        description: schema
1400            .get("description")
1401            .and_then(serde_json::Value::as_str)
1402            .map(str::to_string),
1403        default_value: schema.get("default").cloned(),
1404        enum_values: schema
1405            .get("enum")
1406            .and_then(serde_json::Value::as_array)
1407            .cloned()
1408            .unwrap_or_default()
1409            .into_iter()
1410            .filter(|value| !value.is_null())
1411            .collect(),
1412        minimum: schema
1413            .get("minimum")
1414            .or_else(|| schema.get("exclusiveMinimum"))
1415            .cloned(),
1416        maximum: schema
1417            .get("maximum")
1418            .or_else(|| schema.get("exclusiveMaximum"))
1419            .cloned(),
1420        min_length: schema.get("minLength").and_then(serde_json::Value::as_u64),
1421        max_length: schema.get("maxLength").and_then(serde_json::Value::as_u64),
1422        min_items: schema.get("minItems").and_then(serde_json::Value::as_u64),
1423        max_items: schema.get("maxItems").and_then(serde_json::Value::as_u64),
1424        item_type: schema
1425            .get("items")
1426            .map(schema_type_label)
1427            .filter(|value| value != "any"),
1428    }
1429}
1430
1431fn compact_doc_line(value: &serde_json::Value) -> Option<String> {
1432    let signature = value.get("signature")?.as_str()?.trim();
1433    if signature.is_empty() {
1434        return None;
1435    }
1436    let description = value
1437        .get("description")
1438        .and_then(serde_json::Value::as_str)
1439        .map(str::trim)
1440        .filter(|value| !value.is_empty());
1441    Some(match description {
1442        Some(description) => format!("- `{signature}` — {description}"),
1443        None => format!("- `{signature}`"),
1444    })
1445}
1446
1447fn parameter_signature_from_value(map: &serde_json::Map<String, serde_json::Value>) -> String {
1448    let name = map
1449        .get("name")
1450        .and_then(serde_json::Value::as_str)
1451        .unwrap_or_default();
1452    doc_signature_from_value(name, map)
1453}
1454
1455fn field_signature_from_value(map: &serde_json::Map<String, serde_json::Value>) -> String {
1456    let path = map
1457        .get("path")
1458        .and_then(serde_json::Value::as_str)
1459        .unwrap_or_default();
1460    doc_signature_from_value(path, map)
1461}
1462
1463fn doc_signature_from_value(
1464    name: &str,
1465    map: &serde_json::Map<String, serde_json::Value>,
1466) -> String {
1467    let ty = map
1468        .get("type")
1469        .and_then(serde_json::Value::as_str)
1470        .unwrap_or("any");
1471    let required = map
1472        .get("required")
1473        .and_then(serde_json::Value::as_bool)
1474        .unwrap_or(false);
1475    let mut out = if required {
1476        format!("{name}: {ty}")
1477    } else {
1478        format!("{name}?: {ty}")
1479    };
1480
1481    let mut constraints = Vec::new();
1482    if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array)
1483        && !ty.starts_with("enum[")
1484    {
1485        constraints.push(format!(
1486            "in {}",
1487            values
1488                .iter()
1489                .map(display_default_value)
1490                .collect::<Vec<_>>()
1491                .join("|")
1492        ));
1493    }
1494    if let Some(value) = map.get("minimum") {
1495        constraints.push(format!(">= {}", display_default_value(value)));
1496    }
1497    if let Some(value) = map.get("maximum") {
1498        constraints.push(format!("<= {}", display_default_value(value)));
1499    }
1500    if let Some(value) = map.get("min_length").and_then(serde_json::Value::as_u64) {
1501        constraints.push(format!("min_len {value}"));
1502    }
1503    if let Some(value) = map.get("max_length").and_then(serde_json::Value::as_u64) {
1504        constraints.push(format!("max_len {value}"));
1505    }
1506    if let Some(value) = map.get("min_items").and_then(serde_json::Value::as_u64) {
1507        constraints.push(format!("min_items {value}"));
1508    }
1509    if let Some(value) = map.get("max_items").and_then(serde_json::Value::as_u64) {
1510        constraints.push(format!("max_items {value}"));
1511    }
1512    if !constraints.is_empty() {
1513        out.push(' ');
1514        out.push_str(&constraints.join(" "));
1515    }
1516    if let Some(default) = map.get("default") {
1517        out.push_str(" = ");
1518        out.push_str(&display_default_value(default));
1519    }
1520    out
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525    use super::*;
1526    use serde::ser::{Error as _, Serializer};
1527
1528    #[test]
1529    fn tool_definition_uses_canonical_model_schemas() {
1530        let tool = ToolDefinition::raw(
1531            "mcp__demo__search",
1532            "Search demo server",
1533            serde_json::json!({
1534                "type": "object",
1535                "properties": {
1536                    "query": { "type": "string" },
1537                    "limit": { "type": "integer" }
1538                },
1539                "required": ["query"],
1540                "additionalProperties": false
1541            }),
1542            serde_json::json!({
1543                "type": "object",
1544                "properties": {
1545                    "hits": { "type": "array", "items": { "type": "string" } }
1546                },
1547                "required": ["hits"],
1548                "additionalProperties": false
1549            }),
1550        );
1551
1552        let model_tool = tool.model_tool();
1553        assert_eq!(
1554            model_tool.input_schema["properties"]["limit"]["type"],
1555            serde_json::json!("integer")
1556        );
1557        assert_eq!(
1558            model_tool.output_schema["properties"]["hits"]["type"],
1559            serde_json::json!("array")
1560        );
1561    }
1562
1563    #[test]
1564    fn tool_retry_policy_defaults_to_never_and_is_omitted_from_manifest_json() {
1565        let tool = ToolDefinition::raw(
1566            "demo",
1567            "Demo",
1568            ToolDefinition::default_input_schema(),
1569            serde_json::json!({ "type": "string" }),
1570        );
1571
1572        assert_eq!(tool.retry_policy, ToolRetryPolicy::Never);
1573        let manifest = tool.manifest();
1574        assert_eq!(manifest.retry_policy, ToolRetryPolicy::Never);
1575        let encoded = serde_json::to_value(&manifest).expect("manifest json");
1576        assert!(encoded.get("retry_policy").is_none());
1577    }
1578
1579    #[test]
1580    fn tool_retry_policy_propagates_through_manifest_and_definition_roundtrip() {
1581        let tool = ToolDefinition::raw(
1582            "demo",
1583            "Demo",
1584            ToolDefinition::default_input_schema(),
1585            serde_json::json!({ "type": "string" }),
1586        )
1587        .with_retry_policy(ToolRetryPolicy::safe(3, 10, 100));
1588
1589        let manifest = tool.manifest();
1590        assert_eq!(
1591            manifest.retry_policy,
1592            ToolRetryPolicy::Safe {
1593                max_attempts: 3,
1594                base_delay_ms: 10,
1595                max_delay_ms: 100,
1596            }
1597        );
1598
1599        let roundtrip = ToolDefinition::from_manifest_and_contract(manifest, tool.contract());
1600        assert_eq!(roundtrip.retry_policy, tool.retry_policy);
1601        let encoded = serde_json::to_value(roundtrip.manifest()).expect("manifest json");
1602        assert_eq!(encoded["retry_policy"]["type"], serde_json::json!("safe"));
1603    }
1604
1605    #[test]
1606    fn model_tool_preserves_schema_projection_overrides() {
1607        let tool = ToolDefinition::raw(
1608            "demo",
1609            "Demo",
1610            serde_json::json!({
1611                "type": "object",
1612                "properties": { "raw": { "const": "x" } }
1613            }),
1614            serde_json::json!({ "type": "object" }),
1615        )
1616        .with_input_schema_projection(
1617            "provider.tool_parameters",
1618            serde_json::json!({
1619                "type": "object",
1620                "properties": { "raw": { "type": "string", "enum": ["x"] } }
1621            }),
1622        )
1623        .with_output_schema_projection(
1624            "provider.structured_output",
1625            serde_json::json!({
1626                "type": "object",
1627                "properties": {},
1628                "required": [],
1629                "additionalProperties": false
1630            }),
1631        );
1632
1633        let model_tool = tool.model_tool();
1634        assert_eq!(model_tool.input_schema["properties"]["raw"]["const"], "x");
1635        assert_eq!(
1636            model_tool.input_schema_projections[0].schema["properties"]["raw"]["enum"],
1637            serde_json::json!(["x"])
1638        );
1639        assert_eq!(
1640            model_tool.output_schema_projections[0].profile,
1641            "provider.structured_output"
1642        );
1643    }
1644
1645    #[test]
1646    fn typed_tool_definition_generates_input_and_output_schema() {
1647        #[derive(schemars::JsonSchema)]
1648        #[allow(dead_code)]
1649        enum Mode {
1650            Fast,
1651            Slow,
1652        }
1653
1654        #[derive(schemars::JsonSchema)]
1655        #[allow(dead_code)]
1656        struct Args {
1657            query: String,
1658            #[schemars(range(max = 20))]
1659            page_limit: u8,
1660            #[schemars(length(min = 1, max = 3))]
1661            tags: Vec<String>,
1662            mode: Option<Mode>,
1663        }
1664
1665        #[derive(schemars::JsonSchema)]
1666        #[allow(dead_code)]
1667        struct Output {
1668            answer: String,
1669            #[schemars(range(min = 0))]
1670            confidence: f32,
1671        }
1672
1673        let tool = ToolDefinition::typed::<Args, Output>("demo", "Demo");
1674        let metadata = tool.parameter_metadata();
1675        assert!(metadata.iter().any(|param| {
1676            param["name"] == "page_limit"
1677                && param["type"] == "int"
1678                && param["maximum"].as_f64() == Some(20.0)
1679        }));
1680        assert!(metadata.iter().any(|param| {
1681            param["name"] == "tags"
1682                && param["type"] == "list[str]"
1683                && param["min_items"] == 1
1684                && param["max_items"] == 3
1685        }));
1686        assert!(
1687            metadata
1688                .iter()
1689                .any(|param| { param["name"] == "mode" && param["nullable"] == true })
1690        );
1691        assert_eq!(tool.output_schema["properties"]["answer"]["type"], "string");
1692        assert_eq!(
1693            tool.output_schema["properties"]["confidence"]["minimum"].as_f64(),
1694            Some(0.0)
1695        );
1696    }
1697
1698    #[test]
1699    fn raw_tool_definition_preserves_caller_provided_schemas() {
1700        let input_schema = serde_json::json!({
1701            "type": "object",
1702            "properties": {
1703                "query": { "type": "string", "minLength": 3 }
1704            },
1705            "required": ["query"],
1706            "x-custom": { "keep": true }
1707        });
1708        let output_schema = serde_json::json!({
1709            "type": "object",
1710            "properties": {
1711                "ok": { "type": "boolean" }
1712            },
1713            "required": ["ok"],
1714            "x-result": ["exact"]
1715        });
1716
1717        let tool = ToolDefinition::raw(
1718            "raw_demo",
1719            "Raw demo",
1720            input_schema.clone(),
1721            output_schema.clone(),
1722        );
1723
1724        assert_eq!(tool.input_schema, input_schema);
1725        assert_eq!(tool.output_schema, output_schema);
1726    }
1727
1728    #[test]
1729    fn compact_tool_contract_renders_prompt_and_search_shape_from_schemas() {
1730        let tool = ToolDefinition::raw(
1731            "search_docs",
1732            "Search indexed docs",
1733            serde_json::json!({
1734                "type": "object",
1735                "properties": {
1736                    "query": { "type": "string" },
1737                    "limit": { "type": "integer", "maximum": 10, "default": 5 }
1738                },
1739                "required": ["query"]
1740            }),
1741            serde_json::json!({
1742                "type": "object",
1743                "properties": {
1744                    "matches": {
1745                        "type": "array",
1746                        "items": { "type": "string" }
1747                    },
1748                    "next_page": { "type": ["string", "null"] }
1749                },
1750                "required": ["matches"]
1751            }),
1752        )
1753        .with_examples(vec![
1754            "search_docs(query=\"rust\")".to_string(),
1755            "search_docs(query=\"rust\", limit=3)".to_string(),
1756            "search_docs(query=\"ignored\")".to_string(),
1757        ]);
1758
1759        let contract = tool.compact_contract();
1760        assert_eq!(
1761            contract.signature,
1762            "search_docs(query: str, limit?: int <= 10 = 5)"
1763        );
1764        assert_eq!(
1765            contract.returns,
1766            "record{matches: list[str], next_page?: str | null}"
1767        );
1768        assert_eq!(
1769            contract.parameters,
1770            vec![
1771                serde_json::json!({
1772                    "name": "query",
1773                    "type": "str",
1774                    "required": true,
1775                    "signature": "query: str"
1776                }),
1777                serde_json::json!({
1778                    "name": "limit",
1779                    "type": "int",
1780                    "required": false,
1781                    "default": 5,
1782                    "maximum": 10,
1783                    "signature": "limit?: int <= 10 = 5"
1784                }),
1785            ]
1786        );
1787        assert_eq!(contract.examples.len(), 2);
1788
1789        let docs = ToolDefinition::format_tool_docs(&[tool]);
1790        assert!(docs.contains(
1791            "### search_docs(query: str, limit?: int <= 10 = 5) -> record{matches: list[str], next_page?: str | null}"
1792        ));
1793        assert!(!docs.contains("Returns:"));
1794        assert!(docs.contains("Parameters:\n- `query: str`\n- `limit?: int <= 10 = 5`"));
1795        assert!(docs.contains(
1796            "Examples: search_docs(query=\"rust\"); search_docs(query=\"rust\", limit=3)"
1797        ));
1798    }
1799
1800    #[test]
1801    fn static_output_contract_keeps_existing_compact_docs_and_serde_shape() {
1802        let tool = ToolDefinition::raw(
1803            "read_text",
1804            "Read text",
1805            ToolDefinition::default_input_schema(),
1806            serde_json::json!({ "type": "string" }),
1807        );
1808        let explicit_static = tool
1809            .clone()
1810            .with_output_contract(ToolOutputContract::Static);
1811
1812        assert_eq!(
1813            ToolDefinition::format_tool_docs(std::slice::from_ref(&tool)),
1814            ToolDefinition::format_tool_docs(&[explicit_static])
1815        );
1816        assert_eq!(tool.compact_contract().returns, "str");
1817
1818        let serialized = serde_json::to_value(&tool).expect("serialize");
1819        assert!(serialized.get("output_contract").is_none());
1820        let deserialized: ToolDefinition = serde_json::from_value(serialized).expect("deserialize");
1821        assert!(deserialized.output_contract.is_static());
1822    }
1823
1824    #[test]
1825    fn dynamic_output_contract_renders_schema_from_input_without_return_fields() {
1826        let tool = ToolDefinition::raw(
1827            "spawn_agent",
1828            "Run a subagent",
1829            serde_json::json!({
1830                "type": "object",
1831                "properties": {
1832                    "output": { "type": "object", "additionalProperties": true }
1833                }
1834            }),
1835            serde_json::json!({ "type": "object", "additionalProperties": true }),
1836        )
1837        .with_output_from_input_schema("output", None);
1838
1839        let contract = tool.compact_contract();
1840        assert_eq!(
1841            contract.signature,
1842            "spawn_agent<T = any>(output?: TypeSpec<T>)"
1843        );
1844        assert_eq!(contract.returns, "T");
1845        assert!(contract.return_fields.is_empty());
1846        assert_eq!(contract.render_returns(), "");
1847        assert_eq!(
1848            ToolDefinition::format_tool_docs(&[tool]),
1849            "### spawn_agent<T = any>(output?: TypeSpec<T>) -> T\nRun a subagent\nParameters:\n- `output?: TypeSpec<T>`"
1850        );
1851    }
1852
1853    #[test]
1854    fn dynamic_output_contract_renders_default_schema() {
1855        let tool = ToolDefinition::raw(
1856            "llm_query",
1857            "Run a lightweight LLM query",
1858            serde_json::json!({
1859                "type": "object",
1860                "properties": {
1861                    "task": { "type": "string" },
1862                    "output": { "type": "object", "additionalProperties": true }
1863                },
1864                "required": ["task"]
1865            }),
1866            serde_json::json!({ "type": "object", "additionalProperties": true }),
1867        )
1868        .with_output_from_input_schema("output", Some(serde_json::json!({ "type": "string" })));
1869
1870        let contract = tool.compact_contract();
1871        assert_eq!(
1872            contract.signature,
1873            "llm_query<T = str>(task: str, output?: TypeSpec<T>)"
1874        );
1875        assert_eq!(contract.returns, "T");
1876        assert!(contract.return_fields.is_empty());
1877        assert_eq!(contract.render_returns(), "");
1878    }
1879
1880    #[test]
1881    fn json_schema_loaded_contract_matches_hardcoded_renderer() {
1882        let tool: ToolDefinition = serde_json::from_value(serde_json::json!({
1883            "name": "mcp__appworld__spotify_search_songs",
1884            "description": "[MCP appworld] Search for songs with a query.",
1885            "examples": ["search songs by genre"],
1886            "input_schema": {
1887                "type": "object",
1888                "properties": {
1889                    "access_token": {
1890                        "type": "string",
1891                        "description": "Access token obtained from spotify app login."
1892                    },
1893                    "genre": {
1894                        "type": ["string", "null"],
1895                        "description": "Only include songs from this genre.",
1896                        "default": null
1897                    },
1898                    "page_limit": {
1899                        "type": "integer",
1900                        "description": "Maximum number of songs to return.",
1901                        "minimum": 1,
1902                        "maximum": 20,
1903                        "default": 5
1904                    },
1905                    "sort_by": {
1906                        "type": ["string", "null"],
1907                        "description": "Field to sort by. Prefix with '-' for descending order.",
1908                        "default": null
1909                    }
1910                },
1911                "required": ["access_token"],
1912                "additionalProperties": false
1913            },
1914            "output_schema": {
1915                "anyOf": [
1916                    {
1917                        "type": "object",
1918                        "properties": {
1919                            "response": {
1920                                "type": "array",
1921                                "description": "Matched songs.",
1922                                "items": {
1923                                    "type": "object",
1924                                    "properties": {
1925                                        "album_id": {
1926                                            "type": ["integer", "null"],
1927                                            "description": "Album identifier when the song belongs to an album."
1928                                        },
1929                                        "album_title": { "type": ["string", "null"] },
1930                                        "artists": {
1931                                            "type": "array",
1932                                            "items": {
1933                                                "type": "object",
1934                                                "properties": {
1935                                                    "id": { "type": "integer" },
1936                                                    "name": { "type": "string" }
1937                                                },
1938                                                "required": ["id", "name"]
1939                                            }
1940                                        },
1941                                        "duration": { "type": "integer" },
1942                                        "genre": { "type": "string" },
1943                                        "like_count": { "type": "integer" },
1944                                        "play_count": {
1945                                            "type": "integer",
1946                                            "description": "Number of times the song was played.",
1947                                            "minimum": 0
1948                                        },
1949                                        "rating": { "type": "number" },
1950                                        "release_date": {
1951                                            "type": "string",
1952                                            "description": "Song release date in YYYY-MM-DD format."
1953                                        },
1954                                        "song_id": {
1955                                            "type": "integer",
1956                                            "description": "Stable song identifier."
1957                                        },
1958                                        "title": {
1959                                            "type": "string",
1960                                            "description": "Song title."
1961                                        }
1962                                    },
1963                                    "required": [
1964                                        "album_id",
1965                                        "album_title",
1966                                        "artists",
1967                                        "duration",
1968                                        "genre",
1969                                        "like_count",
1970                                        "play_count",
1971                                        "rating",
1972                                        "release_date",
1973                                        "song_id",
1974                                        "title"
1975                                    ]
1976                                }
1977                            }
1978                        },
1979                        "required": ["response"]
1980                    },
1981                    {
1982                        "type": "object",
1983                        "properties": {
1984                            "response": {
1985                                "type": "object",
1986                                "properties": {
1987                                    "message": {
1988                                        "type": "string",
1989                                        "description": "Failure or status message."
1990                                    }
1991                                },
1992                                "required": ["message"]
1993                            }
1994                        },
1995                        "required": ["response"]
1996                    }
1997                ]
1998            }
1999        }))
2000        .unwrap();
2001
2002        let contract = tool.compact_contract();
2003        assert_eq!(
2004            serde_json::to_value(&contract).unwrap(),
2005            serde_json::json!({
2006                "name": "mcp__appworld__spotify_search_songs",
2007                "signature": "mcp__appworld__spotify_search_songs(access_token: str, genre?: str | null = null, page_limit?: int >= 1 <= 20 = 5, sort_by?: str | null = null)",
2008                "returns": "record{response: list[record{album_id: int | null, album_title: str | null, artists: list[record{id: int, name: str}], duration: int, genre: str, like_count: int, play_count: int, rating: float, release_date: str, song_id: int, title: str}]} | record{response: record{message: str}}",
2009                "parameters": [
2010                    {
2011                        "name": "access_token",
2012                        "type": "str",
2013                        "required": true,
2014                        "description": "Access token obtained from spotify app login.",
2015                        "signature": "access_token: str"
2016                    },
2017                    {
2018                        "name": "genre",
2019                        "type": "str | null",
2020                        "required": false,
2021                        "nullable": true,
2022                        "description": "Only include songs from this genre.",
2023                        "default": null,
2024                        "signature": "genre?: str | null = null"
2025                    },
2026                    {
2027                        "name": "page_limit",
2028                        "type": "int",
2029                        "required": false,
2030                        "description": "Maximum number of songs to return.",
2031                        "default": 5,
2032                        "minimum": 1,
2033                        "maximum": 20,
2034                        "signature": "page_limit?: int >= 1 <= 20 = 5"
2035                    },
2036                    {
2037                        "name": "sort_by",
2038                        "type": "str | null",
2039                        "required": false,
2040                        "nullable": true,
2041                        "description": "Field to sort by. Prefix with '-' for descending order.",
2042                        "default": null,
2043                        "signature": "sort_by?: str | null = null"
2044                    }
2045                ],
2046                "return_fields": [
2047                    {
2048                        "path": "response",
2049                        "type": "list[record]",
2050                        "required": true,
2051                        "description": "Matched songs.",
2052                        "items": "record",
2053                        "signature": "response: list[record]"
2054                    },
2055                    {
2056                        "path": "response[].album_id",
2057                        "type": "int | null",
2058                        "required": true,
2059                        "nullable": true,
2060                        "description": "Album identifier when the song belongs to an album.",
2061                        "signature": "response[].album_id: int | null"
2062                    },
2063                    {
2064                        "path": "response[].album_title",
2065                        "type": "str | null",
2066                        "required": true,
2067                        "nullable": true,
2068                        "signature": "response[].album_title: str | null"
2069                    },
2070                    {
2071                        "path": "response[].artists[].id",
2072                        "type": "int",
2073                        "required": true,
2074                        "signature": "response[].artists[].id: int"
2075                    },
2076                    {
2077                        "path": "response[].artists[].name",
2078                        "type": "str",
2079                        "required": true,
2080                        "signature": "response[].artists[].name: str"
2081                    },
2082                    {
2083                        "path": "response[].duration",
2084                        "type": "int",
2085                        "required": true,
2086                        "signature": "response[].duration: int"
2087                    },
2088                    {
2089                        "path": "response[].genre",
2090                        "type": "str",
2091                        "required": true,
2092                        "signature": "response[].genre: str"
2093                    },
2094                    {
2095                        "path": "response[].like_count",
2096                        "type": "int",
2097                        "required": true,
2098                        "signature": "response[].like_count: int"
2099                    },
2100                    {
2101                        "path": "response[].play_count",
2102                        "type": "int",
2103                        "required": true,
2104                        "description": "Number of times the song was played.",
2105                        "minimum": 0,
2106                        "signature": "response[].play_count: int >= 0"
2107                    },
2108                    {
2109                        "path": "response[].rating",
2110                        "type": "float",
2111                        "required": true,
2112                        "signature": "response[].rating: float"
2113                    },
2114                    {
2115                        "path": "response[].release_date",
2116                        "type": "str",
2117                        "required": true,
2118                        "description": "Song release date in YYYY-MM-DD format.",
2119                        "signature": "response[].release_date: str"
2120                    },
2121                    {
2122                        "path": "response[].song_id",
2123                        "type": "int",
2124                        "required": true,
2125                        "description": "Stable song identifier.",
2126                        "signature": "response[].song_id: int"
2127                    },
2128                    {
2129                        "path": "response[].title",
2130                        "type": "str",
2131                        "required": true,
2132                        "description": "Song title.",
2133                        "signature": "response[].title: str"
2134                    },
2135                    {
2136                        "path": "response.message",
2137                        "type": "str",
2138                        "required": true,
2139                        "description": "Failure or status message.",
2140                        "signature": "response.message: str"
2141                    }
2142                ],
2143                "description": "[MCP appworld] Search for songs with a query.",
2144                "examples": ["search songs by genre"]
2145            })
2146        );
2147
2148        assert_eq!(
2149            contract.render_markdown(),
2150            "### mcp__appworld__spotify_search_songs(access_token: str, genre?: str | null = null, page_limit?: int >= 1 <= 20 = 5, sort_by?: str | null = null) -> record{response: list[record{album_id: int | null, album_title: str | null, artists: list[record{id: int, name: str}], duration: int, genre: str, like_count: int, play_count: int, rating: float, release_date: str, song_id: int, title: str}]} | record{response: record{message: str}}\n[MCP appworld] Search for songs with a query.\nParameters:\n- `access_token: str` — Access token obtained from spotify app login.\n- `genre?: str | null = null` — Only include songs from this genre.\n- `page_limit?: int >= 1 <= 20 = 5` — Maximum number of songs to return.\n- `sort_by?: str | null = null` — Field to sort by. Prefix with '-' for descending order.\nReturn fields:\n- `response: list[record]` — Matched songs.\n- `response[].album_id: int | null` — Album identifier when the song belongs to an album.\n- `response[].album_title: str | null`\n- `response[].artists[].id: int`\n- `response[].artists[].name: str`\n- `response[].duration: int`\n- `response[].genre: str`\n- `response[].like_count: int`\n- `response[].play_count: int >= 0` — Number of times the song was played.\n- `response[].rating: float`\n- `response[].release_date: str` — Song release date in YYYY-MM-DD format.\n- `response[].song_id: int` — Stable song identifier.\n- `response[].title: str` — Song title.\n- `response.message: str` — Failure or status message.\nExamples: search songs by genre"
2151        );
2152        assert_eq!(
2153            contract.render_signature(),
2154            "mcp__appworld__spotify_search_songs(access_token: str, genre?: str | null = null, page_limit?: int >= 1 <= 20 = 5, sort_by?: str | null = null) -> record{response: list[record{album_id: int | null, album_title: str | null, artists: list[record{id: int, name: str}], duration: int, genre: str, like_count: int, play_count: int, rating: float, release_date: str, song_id: int, title: str}]} | record{response: record{message: str}}\nParameters:\n- `access_token: str` — Access token obtained from spotify app login.\n- `genre?: str | null = null` — Only include songs from this genre.\n- `page_limit?: int >= 1 <= 20 = 5` — Maximum number of songs to return.\n- `sort_by?: str | null = null` — Field to sort by. Prefix with '-' for descending order.\nReturn fields:\n- `response: list[record]` — Matched songs.\n- `response[].album_id: int | null` — Album identifier when the song belongs to an album.\n- `response[].album_title: str | null`\n- `response[].artists[].id: int`\n- `response[].artists[].name: str`\n- `response[].duration: int`\n- `response[].genre: str`\n- `response[].like_count: int`\n- `response[].play_count: int >= 0` — Number of times the song was played.\n- `response[].rating: float`\n- `response[].release_date: str` — Song release date in YYYY-MM-DD format.\n- `response[].song_id: int` — Stable song identifier.\n- `response[].title: str` — Song title.\n- `response.message: str` — Failure or status message."
2155        );
2156        assert_eq!(
2157            contract.render_returns(),
2158            "Return fields:\n- `response: list[record]` — Matched songs.\n- `response[].album_id: int | null` — Album identifier when the song belongs to an album.\n- `response[].album_title: str | null`\n- `response[].artists[].id: int`\n- `response[].artists[].name: str`\n- `response[].duration: int`\n- `response[].genre: str`\n- `response[].like_count: int`\n- `response[].play_count: int >= 0` — Number of times the song was played.\n- `response[].rating: float`\n- `response[].release_date: str` — Song release date in YYYY-MM-DD format.\n- `response[].song_id: int` — Stable song identifier.\n- `response[].title: str` — Song title.\n- `response.message: str` — Failure or status message."
2159        );
2160    }
2161
2162    #[test]
2163    fn json_schema_loaded_contract_merges_nullable_anyof_return_fields() {
2164        let tool: ToolDefinition = serde_json::from_value(serde_json::json!({
2165            "name": "mcp__appworld__spotify_show_album_library",
2166            "description": "[MCP appworld] Search or show a list of albums in your album library.",
2167            "examples": ["show album library"],
2168            "input_schema": {
2169                "type": "object",
2170                "properties": {
2171                    "access_token": {
2172                        "type": "string",
2173                        "description": "Access token obtained from spotify app login."
2174                    },
2175                    "page_index": {
2176                        "type": "integer",
2177                        "description": "The index of the page to return.",
2178                        "minimum": 0,
2179                        "default": 0
2180                    },
2181                    "page_limit": {
2182                        "type": "integer",
2183                        "description": "The maximum number of results to return per page.",
2184                        "minimum": 1,
2185                        "maximum": 20,
2186                        "default": 5
2187                    }
2188                },
2189                "required": ["access_token"]
2190            },
2191            "output_schema": {
2192                "type": "object",
2193                "properties": {
2194                    "response": {
2195                        "anyOf": [
2196                            {
2197                                "type": "array",
2198                                "description": "Albums in the user's library.",
2199                                "items": {
2200                                    "type": "object",
2201                                    "properties": {
2202                                        "added_at": {
2203                                            "description": "When the album was added to the library.",
2204                                            "anyOf": [
2205                                                { "type": "string" },
2206                                                { "type": "null" }
2207                                            ]
2208                                        },
2209                                        "album_id": { "type": "integer" },
2210                                        "genre": {
2211                                            "type": "string",
2212                                            "description": "Album genre.",
2213                                            "minLength": 1
2214                                        },
2215                                        "song_ids": {
2216                                            "type": "array",
2217                                            "items": { "type": "integer" }
2218                                        },
2219                                        "title": {
2220                                            "type": "string",
2221                                            "minLength": 1
2222                                        }
2223                                    },
2224                                    "required": ["added_at", "album_id", "genre", "song_ids", "title"]
2225                                }
2226                            },
2227                            {
2228                                "type": "object",
2229                                "properties": {
2230                                    "message": {
2231                                        "type": "string",
2232                                        "description": "Failure or status message."
2233                                    }
2234                                },
2235                                "required": ["message"]
2236                            }
2237                        ]
2238                    }
2239                },
2240                "required": ["response"]
2241            }
2242        }))
2243        .unwrap();
2244
2245        let contract = tool.compact_contract();
2246        assert_eq!(
2247            serde_json::to_value(&contract).unwrap(),
2248            serde_json::json!({
2249                "name": "mcp__appworld__spotify_show_album_library",
2250                "signature": "mcp__appworld__spotify_show_album_library(access_token: str, page_index?: int >= 0 = 0, page_limit?: int >= 1 <= 20 = 5)",
2251                "returns": "record{response: list[record{added_at: null | str, album_id: int, genre: str, song_ids: list[int], title: str}] | record{message: str}}",
2252                "parameters": [
2253                    {
2254                        "name": "access_token",
2255                        "type": "str",
2256                        "required": true,
2257                        "description": "Access token obtained from spotify app login.",
2258                        "signature": "access_token: str"
2259                    },
2260                    {
2261                        "name": "page_index",
2262                        "type": "int",
2263                        "required": false,
2264                        "description": "The index of the page to return.",
2265                        "default": 0,
2266                        "minimum": 0,
2267                        "signature": "page_index?: int >= 0 = 0"
2268                    },
2269                    {
2270                        "name": "page_limit",
2271                        "type": "int",
2272                        "required": false,
2273                        "description": "The maximum number of results to return per page.",
2274                        "default": 5,
2275                        "minimum": 1,
2276                        "maximum": 20,
2277                        "signature": "page_limit?: int >= 1 <= 20 = 5"
2278                    }
2279                ],
2280                "return_fields": [
2281                    {
2282                        "path": "response",
2283                        "type": "list[record]",
2284                        "required": true,
2285                        "description": "Albums in the user's library.",
2286                        "items": "record",
2287                        "signature": "response: list[record]"
2288                    },
2289                    {
2290                        "path": "response[].added_at",
2291                        "type": "str | null",
2292                        "required": true,
2293                        "nullable": true,
2294                        "description": "When the album was added to the library.",
2295                        "signature": "response[].added_at: str | null"
2296                    },
2297                    {
2298                        "path": "response[].album_id",
2299                        "type": "int",
2300                        "required": true,
2301                        "signature": "response[].album_id: int"
2302                    },
2303                    {
2304                        "path": "response[].genre",
2305                        "type": "str",
2306                        "required": true,
2307                        "description": "Album genre.",
2308                        "min_length": 1,
2309                        "signature": "response[].genre: str min_len 1"
2310                    },
2311                    {
2312                        "path": "response[].song_ids[]",
2313                        "type": "int",
2314                        "required": true,
2315                        "signature": "response[].song_ids[]: int"
2316                    },
2317                    {
2318                        "path": "response[].title",
2319                        "type": "str",
2320                        "required": true,
2321                        "min_length": 1,
2322                        "signature": "response[].title: str min_len 1"
2323                    },
2324                    {
2325                        "path": "response.message",
2326                        "type": "str",
2327                        "required": true,
2328                        "description": "Failure or status message.",
2329                        "signature": "response.message: str"
2330                    }
2331                ],
2332                "description": "[MCP appworld] Search or show a list of albums in your album library.",
2333                "examples": ["show album library"]
2334            })
2335        );
2336        assert_eq!(
2337            contract.render_markdown(),
2338            "### mcp__appworld__spotify_show_album_library(access_token: str, page_index?: int >= 0 = 0, page_limit?: int >= 1 <= 20 = 5) -> record{response: list[record{added_at: null | str, album_id: int, genre: str, song_ids: list[int], title: str}] | record{message: str}}\n[MCP appworld] Search or show a list of albums in your album library.\nParameters:\n- `access_token: str` — Access token obtained from spotify app login.\n- `page_index?: int >= 0 = 0` — The index of the page to return.\n- `page_limit?: int >= 1 <= 20 = 5` — The maximum number of results to return per page.\nReturn fields:\n- `response: list[record]` — Albums in the user's library.\n- `response[].added_at: str | null` — When the album was added to the library.\n- `response[].album_id: int`\n- `response[].genre: str min_len 1` — Album genre.\n- `response[].song_ids[]: int`\n- `response[].title: str min_len 1`\n- `response.message: str` — Failure or status message.\nExamples: show album library"
2339        );
2340    }
2341
2342    #[test]
2343    fn tool_result_from_result_serializes_success_values() {
2344        let result: ToolResult = Result::<_, std::io::Error>::Ok(vec!["alpha", "beta"]).into();
2345        assert!(result.is_success());
2346        assert_eq!(
2347            result.value_for_projection(),
2348            serde_json::json!(["alpha", "beta"])
2349        );
2350    }
2351
2352    #[test]
2353    fn tool_result_from_result_formats_errors() {
2354        let result: ToolResult =
2355            Result::<serde_json::Value, _>::Err(std::io::Error::other("nope")).into();
2356        assert!(!result.is_success());
2357        assert_eq!(
2358            result.value_for_projection()["message"],
2359            serde_json::json!("nope")
2360        );
2361    }
2362
2363    #[test]
2364    fn tool_result_from_result_reports_serialize_failures() {
2365        struct BrokenValue;
2366
2367        impl serde::Serialize for BrokenValue {
2368            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
2369            where
2370                S: Serializer,
2371            {
2372                Err(S::Error::custom("boom"))
2373            }
2374        }
2375
2376        let result: ToolResult = Result::<BrokenValue, std::io::Error>::Ok(BrokenValue).into();
2377        assert!(!result.is_success());
2378        assert_eq!(
2379            result.value_for_projection()["message"],
2380            serde_json::json!("Failed to serialize tool result: boom")
2381        );
2382    }
2383
2384    #[test]
2385    fn tool_discovery_metadata_serde_defaults_are_empty() {
2386        let tool: ToolDefinition = serde_json::from_value(serde_json::json!({
2387            "name": "read_file",
2388            "description": "Read a file"
2389        }))
2390        .unwrap();
2391        assert!(tool.discovery.is_empty());
2392    }
2393
2394    #[test]
2395    fn tool_discovery_metadata_does_not_render_prompt_docs() {
2396        let mut with_metadata = ToolDefinition::raw(
2397            "read_file",
2398            "Read a file",
2399            ToolDefinition::default_input_schema(),
2400            serde_json::json!({"type": "string"}),
2401        );
2402        with_metadata.discovery = ToolDiscoveryMetadata {
2403            namespace: Some("filesystem".to_string()),
2404            aliases: vec!["cat".to_string()],
2405        };
2406        let mut without_metadata = with_metadata.clone();
2407        without_metadata.discovery = Default::default();
2408        assert_eq!(
2409            ToolDefinition::format_tool_docs(&[with_metadata]),
2410            ToolDefinition::format_tool_docs(&[without_metadata])
2411        );
2412    }
2413}
2414
2415fn schema_type_label(schema: &serde_json::Value) -> String {
2416    schema_type_label_and_nullability(schema).0
2417}
2418
2419fn compact_schema_label(schema: &serde_json::Value) -> String {
2420    if let Some(any_of) = schema
2421        .get("anyOf")
2422        .or_else(|| schema.get("oneOf"))
2423        .and_then(serde_json::Value::as_array)
2424    {
2425        let labels = any_of
2426            .iter()
2427            .map(compact_schema_label)
2428            .collect::<std::collections::BTreeSet<_>>();
2429        let joined = labels.into_iter().collect::<Vec<_>>().join(" | ");
2430        return if joined.is_empty() {
2431            "any".to_string()
2432        } else {
2433            joined
2434        };
2435    }
2436
2437    if let Some(types) = schema.get("type").and_then(serde_json::Value::as_array) {
2438        let labels = types
2439            .iter()
2440            .filter_map(serde_json::Value::as_str)
2441            .filter(|ty| *ty != "null")
2442            .map(|ty| compact_schema_label(&serde_json::json!({ "type": ty })))
2443            .collect::<std::collections::BTreeSet<_>>();
2444        let mut out = if labels.is_empty() {
2445            "any".to_string()
2446        } else {
2447            labels.into_iter().collect::<Vec<_>>().join(" | ")
2448        };
2449        if types.iter().any(|value| value.as_str() == Some("null")) {
2450            out.push_str(" | null");
2451        }
2452        return out;
2453    }
2454
2455    match schema.get("type").and_then(serde_json::Value::as_str) {
2456        Some("array") => schema
2457            .get("items")
2458            .map(compact_schema_label)
2459            .filter(|value| !value.is_empty())
2460            .map(|item| format!("list[{item}]"))
2461            .unwrap_or_else(|| "list[any]".to_string()),
2462        Some("object") => compact_record_label(schema),
2463        _ => schema_type_label(schema),
2464    }
2465}
2466
2467fn compact_record_label(schema: &serde_json::Value) -> String {
2468    let Some(properties) = schema
2469        .get("properties")
2470        .and_then(serde_json::Value::as_object)
2471    else {
2472        return "record".to_string();
2473    };
2474    if properties.is_empty() {
2475        return "record".to_string();
2476    }
2477
2478    let required = schema
2479        .get("required")
2480        .and_then(serde_json::Value::as_array)
2481        .into_iter()
2482        .flatten()
2483        .filter_map(serde_json::Value::as_str)
2484        .collect::<std::collections::BTreeSet<_>>();
2485    let fields = properties
2486        .iter()
2487        .map(|(name, field_schema)| {
2488            let suffix = if required.contains(name.as_str()) {
2489                ""
2490            } else {
2491                "?"
2492            };
2493            format!("{name}{suffix}: {}", compact_schema_label(field_schema))
2494        })
2495        .collect::<Vec<_>>();
2496    format!("record{{{}}}", fields.join(", "))
2497}
2498
2499fn compact_examples(examples: &[String], limit: usize) -> Vec<String> {
2500    examples
2501        .iter()
2502        .map(|example| example.trim())
2503        .filter(|example| !example.is_empty())
2504        .take(limit)
2505        .map(|example| {
2506            if example.chars().count() <= COMPACT_TOOL_EXAMPLE_CHAR_LIMIT {
2507                return example.to_string();
2508            }
2509            let mut out = example
2510                .chars()
2511                .take(COMPACT_TOOL_EXAMPLE_CHAR_LIMIT.saturating_sub(3))
2512                .collect::<String>();
2513            out.push_str("...");
2514            out
2515        })
2516        .collect()
2517}
2518
2519fn schema_type_label_and_nullability(schema: &serde_json::Value) -> (String, bool) {
2520    if let Some(values) = schema.get("enum").and_then(serde_json::Value::as_array) {
2521        let variants = values
2522            .iter()
2523            .filter(|value| !value.is_null())
2524            .map(display_default_value)
2525            .collect::<Vec<_>>();
2526        let nullable = values.iter().any(serde_json::Value::is_null);
2527        if !variants.is_empty() {
2528            let mut label = format!("enum[{}]", variants.join(", "));
2529            if nullable {
2530                label.push_str(" | null");
2531            }
2532            return (label, nullable);
2533        }
2534    }
2535
2536    if let Some(types) = schema.get("type").and_then(serde_json::Value::as_array) {
2537        let nullable = types.iter().any(|value| value.as_str() == Some("null"));
2538        let non_null = types
2539            .iter()
2540            .filter_map(serde_json::Value::as_str)
2541            .filter(|ty| *ty != "null")
2542            .map(schema_type_name)
2543            .collect::<Vec<_>>();
2544        let mut label = if non_null.is_empty() {
2545            "any".to_string()
2546        } else {
2547            non_null.join(" | ")
2548        };
2549        if nullable {
2550            label.push_str(" | null");
2551        }
2552        return (label, nullable);
2553    }
2554
2555    if let Some(any_of) = schema
2556        .get("anyOf")
2557        .or_else(|| schema.get("oneOf"))
2558        .and_then(serde_json::Value::as_array)
2559    {
2560        let mut nullable = false;
2561        let mut labels = Vec::new();
2562        for subschema in any_of {
2563            let (label, is_nullable) = schema_type_label_and_nullability(subschema);
2564            nullable |= is_nullable || label == "null";
2565            if label != "null" && !labels.iter().any(|existing| existing == &label) {
2566                labels.push(label);
2567            }
2568        }
2569        let mut label = if labels.is_empty() {
2570            "any".to_string()
2571        } else {
2572            labels.join(" | ")
2573        };
2574        if nullable {
2575            label.push_str(" | null");
2576        }
2577        return (label, nullable);
2578    }
2579
2580    let nullable = schema.get("type").and_then(serde_json::Value::as_str) == Some("null");
2581    let label = match schema.get("type").and_then(serde_json::Value::as_str) {
2582        Some("array") => {
2583            let item = schema
2584                .get("items")
2585                .map(schema_type_label)
2586                .filter(|value| !value.is_empty())
2587                .unwrap_or_else(|| "any".to_string());
2588            format!("list[{item}]")
2589        }
2590        Some(ty) => schema_type_name(ty),
2591        None => "any".to_string(),
2592    };
2593    (label, nullable)
2594}
2595
2596fn schema_type_name(ty: &str) -> String {
2597    match ty {
2598        "string" => "str".to_string(),
2599        "integer" => "int".to_string(),
2600        "number" => "float".to_string(),
2601        "boolean" => "bool".to_string(),
2602        "object" => "record".to_string(),
2603        "array" => "list".to_string(),
2604        "null" => "null".to_string(),
2605        _ => "any".to_string(),
2606    }
2607}
2608
2609fn display_default_value(value: &serde_json::Value) -> String {
2610    match value {
2611        serde_json::Value::Null => "null".to_string(),
2612        serde_json::Value::Bool(v) => v.to_string(),
2613        serde_json::Value::Number(v) => v.to_string(),
2614        serde_json::Value::String(v) => format!("{v:?}"),
2615        _ => serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()),
2616    }
2617}
2618
2619#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2620pub struct RlmPrintImage {
2621    pub mime: String,
2622    #[serde(default, skip_serializing_if = "Option::is_none")]
2623    pub reference: Option<AttachmentRef>,
2624    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2625    pub data: Vec<u8>,
2626    pub label: String,
2627    #[serde(default, skip_serializing_if = "Option::is_none")]
2628    pub width: Option<u32>,
2629    #[serde(default, skip_serializing_if = "Option::is_none")]
2630    pub height: Option<u32>,
2631}
2632
2633#[derive(Clone, Debug, PartialEq)]
2634pub struct ToolResult {
2635    pub output: Box<ToolCallOutput>,
2636    pub success: bool,
2637    pub result: serde_json::Value,
2638}
2639
2640impl ToolResult {
2641    pub fn from_output(output: ToolCallOutput) -> Self {
2642        let success = output.is_success();
2643        let result = legacy_tool_result_value(&output);
2644        Self {
2645            output: Box::new(output),
2646            success,
2647            result,
2648        }
2649    }
2650
2651    pub fn ok(result: serde_json::Value) -> Self {
2652        Self::from_output(ToolCallOutput::success(result))
2653    }
2654
2655    pub fn err(result: serde_json::Value) -> Self {
2656        let message = result
2657            .as_str()
2658            .map(ToOwned::to_owned)
2659            .unwrap_or_else(|| result.to_string());
2660        Self::from_output(ToolCallOutput::failure(ToolFailure {
2661            class: ToolFailureClass::Execution,
2662            code: "tool_error".to_string(),
2663            message,
2664            source: ToolFailureSource::Tool,
2665            retry: ToolRetryDisposition::Never,
2666            raw: Some(ToolValue::from(result)),
2667        }))
2668    }
2669
2670    pub fn err_fmt(msg: impl std::fmt::Display) -> Self {
2671        Self::err(serde_json::json!(msg.to_string()))
2672    }
2673
2674    pub fn failure(failure: ToolFailure) -> Self {
2675        Self::from_output(ToolCallOutput::failure(failure))
2676    }
2677
2678    pub fn retryable_failure(
2679        class: ToolFailureClass,
2680        code: impl Into<String>,
2681        message: impl Into<String>,
2682        after_ms: Option<u64>,
2683    ) -> Self {
2684        Self::failure(ToolFailure::safe_retry(class, code, message, after_ms))
2685    }
2686
2687    pub fn cancelled(message: impl Into<String>) -> Self {
2688        Self::from_output(ToolCallOutput::cancelled(ToolCancellation::runtime(
2689            message,
2690        )))
2691    }
2692
2693    pub fn cancelled_with_raw(message: impl Into<String>, raw: serde_json::Value) -> Self {
2694        let mut cancellation = ToolCancellation::runtime(message);
2695        cancellation.raw = Some(ToolValue::from(raw));
2696        Self::from_output(ToolCallOutput::cancelled(cancellation))
2697    }
2698
2699    pub fn with_control(mut self, control: ToolControl) -> Self {
2700        self.output.control = Some(control);
2701        self
2702    }
2703
2704    pub fn is_success(&self) -> bool {
2705        self.success
2706    }
2707
2708    pub fn value_for_projection(&self) -> serde_json::Value {
2709        self.output.value_for_projection()
2710    }
2711
2712    pub fn into_value_for_projection(self) -> serde_json::Value {
2713        self.output.value_for_projection()
2714    }
2715}
2716
2717fn legacy_tool_result_value(output: &ToolCallOutput) -> serde_json::Value {
2718    match &output.outcome {
2719        ToolCallOutcome::Success(value) => value.to_json_value(),
2720        ToolCallOutcome::Failure(failure) => failure
2721            .raw
2722            .as_ref()
2723            .map(ToolValue::to_json_value)
2724            .unwrap_or_else(|| serde_json::Value::String(failure.message.clone())),
2725        ToolCallOutcome::Cancelled(cancellation) => cancellation
2726            .raw
2727            .as_ref()
2728            .map(ToolValue::to_json_value)
2729            .unwrap_or_else(|| serde_json::Value::String(cancellation.message.clone())),
2730    }
2731}
2732
2733impl<T, E> From<Result<T, E>> for ToolResult
2734where
2735    T: serde::Serialize,
2736    E: std::fmt::Display,
2737{
2738    fn from(result: Result<T, E>) -> Self {
2739        match result {
2740            Ok(value) => match serde_json::to_value(value) {
2741                Ok(value) => Self::ok(value),
2742                Err(err) => Self::err_fmt(format_args!("Failed to serialize tool result: {err}")),
2743            },
2744            Err(err) => Self::err_fmt(err),
2745        }
2746    }
2747}
2748
2749#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
2750pub struct ToolCallRecord {
2751    #[serde(default, skip_serializing_if = "Option::is_none")]
2752    pub call_id: Option<String>,
2753    pub tool: String,
2754    pub args: serde_json::Value,
2755    pub output: ToolCallOutput,
2756    pub duration_ms: u64,
2757}
2758
2759pub fn head_tail_truncate(value: &str, max_chars: usize) -> (String, usize) {
2760    let raw_len = value.chars().count();
2761    if max_chars == 0 || raw_len <= max_chars {
2762        return (value.to_string(), raw_len);
2763    }
2764    let head_len = max_chars / 2;
2765    let tail_len = max_chars.saturating_sub(head_len);
2766    let head = value.chars().take(head_len).collect::<String>();
2767    let tail = value
2768        .chars()
2769        .rev()
2770        .take(tail_len)
2771        .collect::<Vec<_>>()
2772        .into_iter()
2773        .rev()
2774        .collect::<String>();
2775    let omitted = raw_len.saturating_sub(head_len + tail_len);
2776    (
2777        format!("{head}\n\n... ({omitted} characters omitted) ...\n\n{tail}"),
2778        raw_len,
2779    )
2780}
2781
2782#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
2783pub struct PromptContext {
2784    pub mode: ExecutionMode,
2785    #[serde(default)]
2786    pub execution_prompt: Arc<str>,
2787    pub tool_names: Arc<Vec<String>>,
2788    pub omitted_tool_count: usize,
2789    pub contributions: Arc<Vec<PromptContribution>>,
2790}
2791
2792impl PromptContext {
2793    pub fn has_tool(&self, tool_name: &str) -> bool {
2794        self.tool_names.iter().any(|name| name == tool_name)
2795    }
2796}