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#[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum ToolExecutionMode {
137 #[default]
139 Parallel,
140 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
159#[serde(tag = "type", rename_all = "snake_case")]
160pub enum ToolRetryPolicy {
161 #[default]
163 Never,
164 Safe {
166 max_attempts: u32,
167 base_delay_ms: u64,
168 max_delay_ms: u64,
169 },
170 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 #[default]
262 Off,
263 Searchable,
266 Callable,
269 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#[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#[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#[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 #[serde(
579 default = "default_tool_execution_mode",
580 skip_serializing_if = "is_default_tool_execution_mode"
581 )]
582 pub execution_mode: ToolExecutionMode,
583 #[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}