Skip to main content

tokitai_core/
lib.rs

1//! # Tokitai Core
2//!
3//! Core types and traits for the Tokitai AI tool integration library. All
4//! tool information is generated at compile time, so the core has zero
5//! runtime dependencies and is `no_std`-compatible (with the `serde`
6//! feature disabled).
7//!
8//! See [`ToolDefinition`], [`ToolProvider`], and the [`tokitai`](https://crates.io/crates/tokitai)
9//! crate for the high-level overview.
10//!
11//! # Example
12//!
13//! ```rust
14//! use tokitai_core::{ToolDefinition, ParamType};
15//!
16//! let tool = ToolDefinition::new(
17//!     "add",
18//!     "Add two numbers together",
19//!     r#"{"type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}},"required":["a","b"]}"#
20//! );
21//! assert_eq!(tool.name, "add");
22//!
23//! assert_eq!(ParamType::from_rust_type("i32"), Some(ParamType::Integer));
24//! assert_eq!(ParamType::from_rust_type("Vec<i32>"), Some(ParamType::Array));
25//! ```
26//!
27//! # Features
28//!
29//! | Feature | Description |
30//! |---------|-------------|
31//! | `serde` (default) | Enable `serde`/`serde_json` integration |
32//!
33//! For `no_std` usage, depend on the crate with `default-features = false`.
34//!
35//! # License
36//!
37//! Dual-licensed under either of:
38//!
39//! - Apache License, Version 2.0
40//! - MIT License
41//!
42//! at your option.
43
44#![cfg_attr(not(feature = "serde"), no_std)]
45#![deny(missing_docs)]
46#![allow(dead_code)]
47
48#[cfg(feature = "serde")]
49extern crate serde;
50
51#[cfg(feature = "serde")]
52extern crate alloc;
53
54#[cfg(feature = "serde")]
55pub use serde_types::*;
56
57#[cfg(feature = "serde")]
58pub use config::{ToolConfig, ToolConfigRegistry, GLOBAL_CONFIG_REGISTRY};
59
60// T-010: runtime-mutable tool registry. The trait is independent of
61// `ToolProvider` so macro-generated providers stay free of runtime
62// state. See [`dynamic::DynamicToolProvider`] and
63// [`dynamic::DynamicToolRegistry`].
64#[cfg(feature = "serde")]
65pub mod dynamic;
66#[cfg(feature = "serde")]
67pub use dynamic::{
68    is_tenant_denied, DynamicHandler, DynamicToolProvider, DynamicToolRegistry,
69    TENANT_DENIED_KIND_HINT,
70};
71
72/// A tool that an AI system can invoke.
73///
74/// Normally generated by the `#[tool]` macro; rarely created by hand.
75///
76/// # Example
77///
78/// ```rust
79/// use tokitai_core::ToolDefinition;
80///
81/// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#);
82/// assert_eq!(tool.name, "add");
83/// ```
84#[derive(Debug, Clone)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub struct ToolDefinition {
87    /// Tool name used for identification during AI calls
88    #[cfg(feature = "serde")]
89    pub name: alloc::string::String,
90    #[cfg(not(feature = "serde"))]
91    pub name: &'static str,
92    /// Tool description helping AI understand its purpose
93    #[cfg(feature = "serde")]
94    pub description: alloc::string::String,
95    #[cfg(not(feature = "serde"))]
96    pub description: &'static str,
97    /// Input parameter JSON Schema (compile-time generated string)
98    #[cfg(feature = "serde")]
99    pub input_schema: alloc::string::String,
100    #[cfg(not(feature = "serde"))]
101    pub input_schema: &'static str,
102    /// Tool version (optional)
103    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
104    #[cfg(feature = "serde")]
105    pub version: Option<alloc::string::String>,
106    #[cfg(not(feature = "serde"))]
107    pub version: Option<&'static str>,
108    /// Version since when the tool is deprecated (optional)
109    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
110    #[cfg(feature = "serde")]
111    pub deprecated_since: Option<alloc::string::String>,
112    #[cfg(not(feature = "serde"))]
113    pub deprecated_since: Option<&'static str>,
114    /// Version when the tool will be removed (optional)
115    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
116    #[cfg(feature = "serde")]
117    pub remove_in: Option<alloc::string::String>,
118    #[cfg(not(feature = "serde"))]
119    pub remove_in: Option<&'static str>,
120    /// Tool that replaces this deprecated tool (optional)
121    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
122    #[cfg(feature = "serde")]
123    pub replaced_by: Option<alloc::string::String>,
124    #[cfg(not(feature = "serde"))]
125    pub replaced_by: Option<&'static str>,
126    /// True when the description was supplied explicitly via
127    /// `#[tool(desc = "...")]` at compile time. The runtime
128    /// configuration system (`tokitai!`) will NOT override an
129    /// explicit description — see [`crate::config::CONFIG_PRIORITY_ORDER`].
130    #[cfg(feature = "serde")]
131    pub description_explicit: bool,
132    #[cfg(not(feature = "serde"))]
133    pub description_explicit: bool,
134    /// T-016: baked few-shot examples. Each entry is a JSON object
135    /// of the shape `{ "input": ..., "output": ... }` (one entry
136    /// per `#[tool(example = call!(...))]` /
137    /// `#[tool(examples = [call!(...), ...])]` element on the
138    /// method). When non-empty, the rendered
139    /// `input_schema`/`to_openai_function()` /
140    /// `to_anthropic_tool()` / `to_mcp_tool()` output carries an
141    /// `examples` array carrying these entries so the LLM sees a
142    /// typed, signature-synced example it cannot drift away from.
143    ///
144    /// The field is populated at `LazyLock` initialization by the
145    /// `#[tool]` macro; downstream consumers do not need to set it
146    /// directly.
147    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
148    #[cfg(feature = "serde")]
149    pub baked_examples: Option<serde_json::Value>,
150    #[cfg(not(feature = "serde"))]
151    pub baked_examples: Option<&'static str>,
152    /// T-046: optional free-form usage hint shown to the model alongside
153    /// the description. Useful for nudging specific invocation patterns
154    /// (e.g. "Always pass `limit` <= 100"). See the
155    /// `tokitai-llm` `ToolHintPlacement` enum for delivery options.
156    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
157    #[cfg(feature = "serde")]
158    pub usage_hint: Option<alloc::string::String>,
159    #[cfg(not(feature = "serde"))]
160    pub usage_hint: Option<&'static str>,
161    /// T-020: lower bound of the tool's schema-evolution interval
162    /// (inclusive). Set from `#[tool(since = "1.0")]` on the
163    /// method. The dispatcher serves the tool only when
164    /// `current_version()` falls inside the `[since, until)`
165    /// half-open interval; methods without `since` / `until`
166    /// attributes are always served. The strings are compared
167    /// using the parse helper when possible, falling back
168    /// to lexicographic order otherwise.
169    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
170    #[cfg(feature = "serde")]
171    pub since: Option<alloc::string::String>,
172    #[cfg(not(feature = "serde"))]
173    pub since: Option<&'static str>,
174    /// T-020: upper bound of the tool's schema-evolution interval
175    /// (exclusive). Set from `#[tool(until = "2.0")]` on the
176    /// method. The dispatcher hides the tool when
177    /// `current_version() >= until` (same ordering rules as
178    /// the `since` field above).
179    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
180    #[cfg(feature = "serde")]
181    pub until: Option<alloc::string::String>,
182    #[cfg(not(feature = "serde"))]
183    pub until: Option<&'static str>,
184}
185
186/// Single source of truth for the priority order of configuration
187/// sources that can supply a tool's description.
188///
189/// The table is also the spec for `tokitai_core::config::CONFIG_PRIORITY_ORDER`
190/// (see that module for the full, const-fn version). Keeping the two
191/// declarations in lockstep is enforced by a doc test:
192/// `cargo test -p tokitai-core CONFIG_PRIORITY_ORDER`.
193///
194/// Index 0 is the highest-priority source (wins on conflict); the last
195/// index is the lowest (used only as a fallback).
196pub const CONFIG_PRIORITY_DOC: &[&str] = &[
197    "#[tool(desc = \"...\")]  (compile-time, attribute-supplied)",
198    "doc comment  (compile-time, /// lines above the method)",
199    "tokitai! config block  (runtime, applies via GLOBAL_CONFIG_REGISTRY)",
200    "synthesized default  (compile-time, \"调用 <method> 方法\")",
201];
202
203/// Compile-time storage for tool definition data, used by the
204/// `#[tool]` macro and converted to a `ToolDefinition` at zero cost
205/// via [`ToolDefinition::from_const`].
206#[doc(hidden)]
207pub struct ToolDefinitionConst {
208    pub name: &'static str,
209    pub description: &'static str,
210    pub input_schema: &'static str,
211}
212
213impl ToolDefinition {
214    /// Build a tool definition from compile-time constants. Optimized for the
215    /// `#[tool]` macro: no allocation, just `'static` -> owned-string copies.
216    ///
217    /// # Example
218    ///
219    /// ```rust
220    /// use tokitai_core::{ToolDefinition, ToolDefinitionConst};
221    ///
222    /// const TOOL_DATA: ToolDefinitionConst = ToolDefinitionConst {
223    ///     name: "get_weather",
224    ///     description: "Get weather information for a specified city",
225    ///     input_schema: r#"{"type":"object"}"#,
226    /// };
227    ///
228    /// let tool = ToolDefinition::from_const(TOOL_DATA);
229    /// ```
230    #[inline(always)]
231    pub fn from_const(data: ToolDefinitionConst) -> Self {
232        Self {
233            #[cfg(feature = "serde")]
234            name: data.name.into(),
235            #[cfg(not(feature = "serde"))]
236            name: data.name,
237            #[cfg(feature = "serde")]
238            description: data.description.into(),
239            #[cfg(not(feature = "serde"))]
240            description: data.description,
241            #[cfg(feature = "serde")]
242            input_schema: data.input_schema.into(),
243            #[cfg(not(feature = "serde"))]
244            input_schema: data.input_schema,
245            version: None,
246            deprecated_since: None,
247            remove_in: None,
248            replaced_by: None,
249            description_explicit: false,
250            baked_examples: None,
251            usage_hint: None,
252            since: None,
253            until: None,
254        }
255    }
256
257    /// Create a new tool definition at runtime. Each argument can be any
258    /// type that implements `Into<String>`.
259    ///
260    /// # Example
261    ///
262    /// ```rust
263    /// use tokitai_core::ToolDefinition;
264    ///
265    /// let tool = ToolDefinition::new(
266    ///     "get_weather",
267    ///     "Get weather information for a specified city",
268    ///     r#"{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}"#
269    /// );
270    /// ```
271    #[cfg(feature = "serde")]
272    pub fn new(
273        name: impl Into<alloc::string::String>,
274        description: impl Into<alloc::string::String>,
275        input_schema: impl Into<alloc::string::String>,
276    ) -> Self {
277        Self {
278            name: name.into(),
279            description: description.into(),
280            input_schema: input_schema.into(),
281            version: None,
282            deprecated_since: None,
283            remove_in: None,
284            replaced_by: None,
285            description_explicit: false,
286            baked_examples: None,
287            usage_hint: None,
288            since: None,
289            until: None,
290        }
291    }
292
293    /// `no_std` constructor: all three strings must be `'static`.
294    ///
295    /// # Example
296    ///
297    /// ```rust
298    /// use tokitai_core::ToolDefinition;
299    ///
300    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#);
301    /// assert_eq!(tool.name, "add");
302    /// ```
303    #[cfg(not(feature = "serde"))]
304    pub fn new(name: &'static str, description: &'static str, input_schema: &'static str) -> Self {
305        Self {
306            name,
307            description,
308            input_schema,
309            version: None,
310            deprecated_since: None,
311            remove_in: None,
312            replaced_by: None,
313            description_explicit: false,
314            baked_examples: None,
315            usage_hint: None,
316            since: None,
317            until: None,
318        }
319    }
320
321    /// Mark the tool's description as having been supplied explicitly via
322    /// `#[tool(desc = "...")]` at compile time.
323    ///
324    /// This makes the description "freeze": subsequent calls to
325    /// [`ToolDefinition::apply_configs`] with a `ToolConfig::Desc` will be
326    /// ignored. The flag is set by the `#[tool]` macro when the user
327    /// supplies `desc = "..."` on the method attribute, and powers the
328    /// priority table exposed by
329    /// [`crate::config::CONFIG_PRIORITY_ORDER`].
330    ///
331    /// # Example
332    ///
333    /// ```rust
334    /// use tokitai_core::ToolDefinition;
335    ///
336    /// let tool = ToolDefinition::new("add", "Add two numbers", "{}")
337    ///     .with_description_explicit();
338    /// assert!(tool.description_explicit);
339    /// ```
340    #[must_use]
341    pub fn with_description_explicit(mut self) -> Self {
342        self.description_explicit = true;
343        self
344    }
345
346    /// T-046: attach a usage hint that the provider layer can
347    /// auto-inject into the system prompt or as a separate message.
348    /// See the `tokitai-llm` `ToolHintPlacement` enum for delivery options.
349    ///
350    /// # Example
351    ///
352    /// ```rust
353    /// use tokitai_core::ToolDefinition;
354    ///
355    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#)
356    ///     .with_usage_hint("Always pass both `a` and `b` as i64.");
357    /// ```
358    #[cfg(feature = "serde")]
359    pub fn with_usage_hint(mut self, hint: impl Into<alloc::string::String>) -> Self {
360        self.usage_hint = Some(hint.into());
361        self
362    }
363
364    /// T-046: `no_std` variant.
365    #[cfg(not(feature = "serde"))]
366    pub fn with_usage_hint(mut self, hint: &'static str) -> Self {
367        self.usage_hint = Some(hint);
368        self
369    }
370
371    /// T-016: attach a `serde_json::Value` carrying the
372    /// `examples` array (one `{ "input": ..., "output": ... }`
373    /// entry per baked example). The macro emits a call to this
374    /// method only when the user wrote
375    /// `#[tool(example = call!(...))]` (or the plural form) on a
376    /// method, so existing tools are unchanged. When the value
377    /// is `None` or an empty array, the schema's `examples` field
378    /// is omitted entirely.
379    #[cfg(feature = "serde")]
380    #[must_use]
381    pub fn with_baked_examples(mut self, examples: serde_json::Value) -> Self {
382        if let serde_json::Value::Array(ref arr) = examples {
383            if arr.is_empty() {
384                self.baked_examples = None;
385                return self;
386            }
387        }
388        self.baked_examples = Some(examples);
389        self
390    }
391
392    /// T-016: `no_std` variant of [`with_baked_examples`]. Takes
393    /// a `'static str` JSON literal; callers in `no_std`
394    /// environments do not have access to `serde_json`, so they
395    /// must pre-render the examples array. Today the macro
396    /// always renders through the `serde`-feature path, so this
397    /// stub is unreachable in practice; it exists so the type
398    /// stays `cfg`-complete.
399    #[cfg(not(feature = "serde"))]
400    #[must_use]
401    pub fn with_baked_examples(mut self, _examples: &'static str) -> Self {
402        // No-op in `no_std`: the macro always uses the
403        // serde-feature variant. This stub keeps the public API
404        // symmetric across feature flags.
405        self
406    }
407
408    /// Set the tool version.
409    ///
410    /// # Example
411    ///
412    /// ```rust
413    /// use tokitai_core::ToolDefinition;
414    ///
415    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#)
416    ///     .with_version("1.2.0");
417    /// assert_eq!(tool.version.as_deref(), Some("1.2.0"));
418    /// ```
419    #[cfg(feature = "serde")]
420    pub fn with_version(mut self, version: impl Into<alloc::string::String>) -> Self {
421        self.version = Some(version.into());
422        self
423    }
424
425    /// `no_std` version setter: `version` must be `'static`.
426    ///
427    /// # Example
428    ///
429    /// ```rust
430    /// use tokitai_core::ToolDefinition;
431    ///
432    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#)
433    ///     .with_version("1.2.0");
434    /// assert_eq!(tool.version, Some("1.2.0"));
435    /// ```
436    #[cfg(not(feature = "serde"))]
437    pub fn with_version(mut self, version: &'static str) -> Self {
438        self.version = Some(version);
439        self
440    }
441
442    /// T-020: set the lower bound of the schema-evolution interval.
443    /// The dispatcher serves the tool only when `current_version()`
444    /// is at or after `since` (and before `until`, if set).
445    ///
446    /// # Example
447    ///
448    /// ```rust
449    /// use tokitai_core::ToolDefinition;
450    ///
451    /// let tool = ToolDefinition::new("add", "Add two numbers", "{}")
452    ///     .with_since("1.0.0");
453    /// assert_eq!(tool.since.as_deref(), Some("1.0.0"));
454    /// ```
455    #[cfg(feature = "serde")]
456    #[must_use]
457    pub fn with_since(mut self, since: impl Into<alloc::string::String>) -> Self {
458        self.since = Some(since.into());
459        self
460    }
461
462    /// `no_std` `since` setter: `since` must be `'static`.
463    #[cfg(not(feature = "serde"))]
464    #[must_use]
465    pub fn with_since(mut self, since: &'static str) -> Self {
466        self.since = Some(since);
467        self
468    }
469
470    /// T-020: set the upper bound (exclusive) of the
471    /// schema-evolution interval. When `current_version() >=
472    /// until`, the dispatcher hides the tool.
473    ///
474    /// # Example
475    ///
476    /// ```rust
477    /// use tokitai_core::ToolDefinition;
478    ///
479    /// let tool = ToolDefinition::new("legacy", "Old API", "{}")
480    ///     .with_since("1.0.0")
481    ///     .with_until("2.0.0");
482    /// assert_eq!(tool.until.as_deref(), Some("2.0.0"));
483    /// ```
484    #[cfg(feature = "serde")]
485    #[must_use]
486    pub fn with_until(mut self, until: impl Into<alloc::string::String>) -> Self {
487        self.until = Some(until.into());
488        self
489    }
490
491    /// `no_std` `until` setter: `until` must be `'static`.
492    #[cfg(not(feature = "serde"))]
493    #[must_use]
494    pub fn with_until(mut self, until: &'static str) -> Self {
495        self.until = Some(until);
496        self
497    }
498
499    /// T-020: returns `true` when `current_version` falls inside
500    /// the tool's `[since, until)` half-open interval.
501    ///
502    /// Returns `true` when:
503    /// * `since` is `None` and `until` is `None` (unversioned tool,
504    ///   always served — backwards-compatible default).
505    /// * `current_version` is `None` (no program-wide version
506    ///   configured; the dispatcher falls open to honour the
507    ///   legacy `tool_definitions()` contract).
508    /// * `current_version >= since` AND `current_version < until`,
509    ///   compared via `parse_semver` (the private helper used by
510    ///   `remove_in`/`since`/`until`) when possible and
511    ///   lexicographically otherwise (so CalVer / commit-SHA
512    ///   strings still produce a deterministic dispatch).
513    ///
514    /// Returns `false` when one bound is set and `current_version`
515    /// falls outside the half-open window.
516    ///
517    /// # Example
518    ///
519    /// ```rust
520    /// use tokitai_core::ToolDefinition;
521    ///
522    /// let tool = ToolDefinition::new("legacy", "Old API", "{}")
523    ///     .with_since("1.0.0")
524    ///     .with_until("2.0.0");
525    /// assert!(tool.is_in_interval(Some("1.5.0")));
526    /// assert!(!tool.is_in_interval(Some("2.0.0")));
527    /// assert!(!tool.is_in_interval(Some("0.9.0")));
528    /// assert!(tool.is_in_interval(None));
529    /// ```
530    pub fn is_in_interval(&self, current_version: Option<&str>) -> bool {
531        // Both bounds unset => always served.
532        if self.since.is_none() && self.until.is_none() {
533            return true;
534        }
535        // No program-wide version => fail open so existing
536        // tool_definitions() callers keep working unchanged.
537        let Some(current) = current_version else {
538            return true;
539        };
540        // Lower bound: current >= since (when since is set).
541        if let Some(since) = self.since.as_deref() {
542            if !version_gte(current, since) {
543                return false;
544            }
545        }
546        // Upper bound: current < until (when until is set).
547        if let Some(until) = self.until.as_deref() {
548            if version_gte(current, until) {
549                return false;
550            }
551        }
552        true
553    }
554
555    /// Mark the tool as deprecated.
556    ///
557    /// # Example
558    ///
559    /// ```rust
560    /// use tokitai_core::ToolDefinition;
561    ///
562    /// let tool = ToolDefinition::new("multiply", "Multiply two numbers", r#"{"type":"object"}"#)
563    ///     .with_deprecated("0.5.0", "0.7.0", "add_repeated");
564    /// assert_eq!(tool.deprecated_since.as_deref(), Some("0.5.0"));
565    /// assert_eq!(tool.remove_in.as_deref(), Some("0.7.0"));
566    /// assert_eq!(tool.replaced_by.as_deref(), Some("add_repeated"));
567    /// ```
568    #[cfg(feature = "serde")]
569    pub fn with_deprecated(
570        mut self,
571        deprecated_since: impl Into<alloc::string::String>,
572        remove_in: impl Into<alloc::string::String>,
573        replaced_by: impl Into<alloc::string::String>,
574    ) -> Self {
575        self.deprecated_since = Some(deprecated_since.into());
576        self.remove_in = Some(remove_in.into());
577        self.replaced_by = Some(replaced_by.into());
578        self
579    }
580
581    /// `no_std` deprecation setter: all three strings must be `'static`.
582    ///
583    /// # Example
584    ///
585    /// ```rust
586    /// use tokitai_core::ToolDefinition;
587    ///
588    /// let tool = ToolDefinition::new("multiply", "Multiply two numbers", r#"{"type":"object"}"#)
589    ///     .with_deprecated("0.5.0", "0.7.0", "add_repeated");
590    /// assert_eq!(tool.deprecated_since, Some("0.5.0"));
591    /// assert_eq!(tool.remove_in, Some("0.7.0"));
592    /// assert_eq!(tool.replaced_by, Some("add_repeated"));
593    /// ```
594    #[cfg(not(feature = "serde"))]
595    pub fn with_deprecated(
596        mut self,
597        deprecated_since: &'static str,
598        remove_in: &'static str,
599        replaced_by: &'static str,
600    ) -> Self {
601        self.deprecated_since = Some(deprecated_since);
602        self.remove_in = Some(remove_in);
603        self.replaced_by = Some(replaced_by);
604        self
605    }
606
607    /// Serialize this tool definition to a JSON string.
608    ///
609    /// # Example
610    ///
611    /// ```rust
612    /// use tokitai_core::ToolDefinition;
613    ///
614    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#);
615    /// let json = tool.to_json().unwrap();
616    /// assert!(json.contains(r#""name":"add""#));
617    /// ```
618    ///
619    /// # Errors
620    ///
621    /// Returns a `serde_json::Error` if serialization fails.
622    #[cfg(feature = "serde")]
623    pub fn to_json(&self) -> Result<String, serde_json::Error> {
624        serde_json::to_string(self)
625    }
626
627    /// Serialize this tool definition to a `serde_json::Value`.
628    ///
629    /// # Example
630    ///
631    /// ```rust
632    /// use tokitai_core::ToolDefinition;
633    /// use serde_json::json;
634    ///
635    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#);
636    /// let value = tool.to_value().unwrap();
637    /// assert_eq!(value["name"], json!("add"));
638    /// ```
639    ///
640    /// # Errors
641    ///
642    /// Returns a `serde_json::Error` if serialization fails.
643    #[cfg(feature = "serde")]
644    pub fn to_value(&self) -> Result<serde_json::Value, serde_json::Error> {
645        serde_json::to_value(self)
646    }
647
648    /// Return the input schema pretty-printed as a JSON string.
649    ///
650    /// # Example
651    ///
652    /// ```rust
653    /// use tokitai_core::ToolDefinition;
654    ///
655    /// let tool = ToolDefinition::new(
656    ///     "add",
657    ///     "Add two numbers",
658    ///     r#"{"type":"object","properties":{"a":{"type":"integer"}}}"#,
659    /// );
660    /// let pretty = tool.input_schema_pretty().unwrap();
661    /// assert!(pretty.contains('\n'));
662    /// ```
663    ///
664    /// # Errors
665    ///
666    /// Returns a `serde_json::Error` if `input_schema` is not valid JSON or
667    /// if pretty-printing fails.
668    #[cfg(feature = "serde")]
669    pub fn input_schema_pretty(&self) -> Result<String, serde_json::Error> {
670        let value: serde_json::Value = serde_json::from_str(&self.input_schema)?;
671        serde_json::to_string_pretty(&value)
672    }
673
674    /// Parse the input schema into a `serde_json::Value`.
675    ///
676    /// # Example
677    ///
678    /// ```rust
679    /// use tokitai_core::ToolDefinition;
680    /// use serde_json::json;
681    ///
682    /// let tool = ToolDefinition::new("add", "Add two numbers", r#"{"type":"object"}"#);
683    /// let schema = tool.input_schema_value().unwrap();
684    /// assert_eq!(schema, json!({"type": "object"}));
685    /// ```
686    ///
687    /// # Errors
688    ///
689    /// Returns a `serde_json::Error` if `input_schema` is not valid JSON.
690    #[cfg(feature = "serde")]
691    pub fn input_schema_value(&self) -> Result<serde_json::Value, serde_json::Error> {
692        serde_json::from_str(&self.input_schema)
693    }
694
695    /// Apply a list of runtime configuration items to this tool definition.
696    ///
697    /// Used by the configuration system to override compile-time defaults.
698    ///
699    /// # Example
700    ///
701    /// ```rust
702    /// use tokitai_core::{ToolDefinition, ToolConfig};
703    ///
704    /// let mut tool = ToolDefinition::new("test", "Original description", "{}");
705    /// tool.apply_configs(&[
706    ///     ToolConfig::Desc("Overridden description".to_string()),
707    /// ]);
708    /// assert_eq!(tool.description, "Overridden description");
709    /// ```
710    #[cfg(feature = "serde")]
711    pub fn apply_configs(&mut self, configs: &[ToolConfig]) {
712        for config in configs {
713            match config {
714                ToolConfig::Desc(desc) => {
715                    // T-002: respect the priority table.
716                    //   `#[tool(desc = "...")]` (compile-time) wins
717                    //   over the `tokitai!` config (runtime). If the
718                    //   description was supplied explicitly at
719                    //   compile time we leave it alone.
720                    if !self.description_explicit {
721                        self.description = desc.clone();
722                    }
723                }
724                ToolConfig::Tags(tags) => {
725                    // Add tags to the schema
726                    if let Ok(mut schema) =
727                        serde_json::from_str::<serde_json::Value>(&self.input_schema)
728                    {
729                        if let Some(obj) = schema.as_object_mut() {
730                            obj.insert("tags".to_string(), serde_json::json!(tags));
731                        }
732                        self.input_schema = schema.to_string();
733                    }
734                }
735                ToolConfig::ParamDesc { name, desc } => {
736                    self.apply_param_desc(name, desc);
737                }
738                ToolConfig::ParamExample { name, example } => {
739                    self.apply_param_example(name, example);
740                }
741                ToolConfig::ParamDefault { name, default } => {
742                    self.apply_param_default(name, default);
743                }
744                ToolConfig::ParamRequired { name, required } => {
745                    self.apply_param_required(name, *required);
746                }
747                ToolConfig::ParamMin { name, min } => {
748                    self.apply_param_constraint(name, "minimum", serde_json::json!(min));
749                }
750                ToolConfig::ParamMax { name, max } => {
751                    self.apply_param_constraint(name, "maximum", serde_json::json!(max));
752                }
753                ToolConfig::ParamMinLength { name, min_length } => {
754                    self.apply_param_constraint(name, "minLength", serde_json::json!(min_length));
755                }
756                ToolConfig::ParamMaxLength { name, max_length } => {
757                    self.apply_param_constraint(name, "maxLength", serde_json::json!(max_length));
758                }
759                ToolConfig::ParamPattern { name, pattern } => {
760                    self.apply_param_constraint(name, "pattern", serde_json::json!(pattern));
761                }
762                ToolConfig::ParamMinItems { name, min_items } => {
763                    self.apply_param_constraint(name, "minItems", serde_json::json!(min_items));
764                }
765                ToolConfig::ParamMaxItems { name, max_items } => {
766                    self.apply_param_constraint(name, "maxItems", serde_json::json!(max_items));
767                }
768                ToolConfig::ParamMultipleOf { name, multiple_of } => {
769                    self.apply_param_constraint(name, "multipleOf", serde_json::json!(multiple_of));
770                }
771            }
772        }
773    }
774
775    /// Set the `description` field of the named parameter in the JSON schema.
776    #[cfg(feature = "serde")]
777    fn apply_param_desc(&mut self, name: &str, desc: &str) {
778        if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
779            if let Some(obj) = schema.as_object_mut() {
780                if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
781                    if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
782                        param.insert("description".to_string(), serde_json::json!(desc));
783                    }
784                }
785            }
786            self.input_schema = schema.to_string();
787        }
788    }
789
790    /// Set the `example` field of the named parameter in the JSON schema.
791    #[cfg(feature = "serde")]
792    fn apply_param_example(&mut self, name: &str, example: &serde_json::Value) {
793        if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
794            if let Some(obj) = schema.as_object_mut() {
795                if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
796                    if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
797                        param.insert("example".to_string(), example.clone());
798                    }
799                }
800            }
801            self.input_schema = schema.to_string();
802        }
803    }
804
805    /// Set the `default` field of the named parameter in the JSON schema.
806    #[cfg(feature = "serde")]
807    fn apply_param_default(&mut self, name: &str, default: &serde_json::Value) {
808        if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
809            if let Some(obj) = schema.as_object_mut() {
810                if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
811                    if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
812                        param.insert("default".to_string(), default.clone());
813                    }
814                }
815            }
816            self.input_schema = schema.to_string();
817        }
818    }
819
820    /// Toggle the `required` flag of the named parameter in the JSON schema.
821    #[cfg(feature = "serde")]
822    fn apply_param_required(&mut self, name: &str, required: bool) {
823        if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
824            if let Some(obj) = schema.as_object_mut() {
825                // Update required array
826                let required_arr = obj
827                    .entry("required".to_string())
828                    .or_insert_with(|| serde_json::json!([]))
829                    .as_array_mut();
830
831                if let Some(req_arr) = required_arr {
832                    let name_json = serde_json::json!(name);
833                    if required && !req_arr.contains(&name_json) {
834                        req_arr.push(name_json);
835                    } else if !required {
836                        req_arr.retain(|v| v != &name_json);
837                    }
838                }
839
840                // Also update parameter schema
841                if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
842                    if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
843                        // Note: individual param "required" is not standard JSON Schema
844                        // but we can add it for documentation purposes
845                        param.insert("required".to_string(), serde_json::json!(required));
846                    }
847                }
848            }
849            self.input_schema = schema.to_string();
850        }
851    }
852
853    /// Insert a JSON-Schema constraint (e.g. `minimum`, `pattern`) for a
854    /// parameter by name.
855    #[cfg(feature = "serde")]
856    fn apply_param_constraint(
857        &mut self,
858        name: &str,
859        constraint_key: &str,
860        value: serde_json::Value,
861    ) {
862        if let Ok(mut schema) = serde_json::from_str::<serde_json::Value>(&self.input_schema) {
863            if let Some(obj) = schema.as_object_mut() {
864                if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
865                    if let Some(param) = props.get_mut(name).and_then(|v| v.as_object_mut()) {
866                        param.insert(constraint_key.to_string(), value);
867                    }
868                }
869            }
870            self.input_schema = schema.to_string();
871        }
872    }
873
874    /// Convert this tool definition into the OpenAI function-calling format.
875    ///
876    /// Spec: <https://platform.openai.com/docs/guides/function-calling>
877    ///
878    /// Returns a JSON object of the form:
879    /// ```json
880    /// {
881    ///   "type": "function",
882    ///   "function": {
883    ///     "name": "<tool name>",
884    ///     "description": "<tool description>",
885    ///     "parameters": { /* full JSON Schema */ }
886    ///   }
887    /// }
888    /// ```
889    ///
890    /// If the stored `input_schema` is not valid JSON, the `parameters`
891    /// field will be emitted as an empty object (`{}`) so the surrounding
892    /// envelope remains a valid OpenAI tool descriptor.
893    ///
894    /// T-013: when the tool has a deprecation marker, the description
895    /// is suffixed with a `[DEPRECATED ...]` annotation. The OpenAI
896    /// spec does not standardize a `_meta` envelope, so we surface the
897    /// deprecation as visible text the LLM can read.
898    ///
899    /// # Example
900    ///
901    /// ```rust
902    /// use tokitai_core::ToolDefinition;
903    /// use serde_json::json;
904    ///
905    /// let tool = ToolDefinition::new(
906    ///     "add",
907    ///     "Add two numbers",
908    ///     r#"{"type":"object","properties":{"a":{"type":"integer"}}}"#,
909    /// );
910    /// let openai = tool.to_openai_function();
911    /// assert_eq!(openai["type"], json!("function"));
912    /// assert_eq!(openai["function"]["name"], json!("add"));
913    /// ```
914    #[cfg(feature = "serde")]
915    pub fn to_openai_function(&self) -> serde_json::Value {
916        let parameters = self.merge_baked_examples_into(self.parse_input_schema_or_empty());
917        let description = self.deprecated_description_suffix();
918        serde_json::json!({
919            "type": "function",
920            "function": {
921                "name": self.name,
922                "description": description,
923                "parameters": parameters,
924            }
925        })
926    }
927
928    /// Convert this tool definition into the Anthropic tool-use format.
929    ///
930    /// Spec: <https://docs.anthropic.com/en/docs/build-with-claude/tool-use>
931    ///
932    /// Returns a JSON object of the form:
933    /// ```json
934    /// {
935    ///   "name": "<tool name>",
936    ///   "description": "<tool description>",
937    ///   "input_schema": { /* full JSON Schema */ }
938    /// }
939    /// ```
940    ///
941    /// If the stored `input_schema` is not valid JSON, the `input_schema`
942    /// field will be emitted as an empty object (`{}`) so the surrounding
943    /// envelope remains a valid Anthropic tool descriptor.
944    ///
945    /// T-013: when the tool has a deprecation marker, the description
946    /// is suffixed with a `[DEPRECATED ...]` annotation that the LLM
947    /// can read. Anthropic does not standardize a top-level
948    /// `_meta.deprecated` field for tool definitions, so the
949    /// description suffix is the supported carrier.
950    ///
951    /// # Example
952    ///
953    /// ```rust
954    /// use tokitai_core::ToolDefinition;
955    /// use serde_json::json;
956    ///
957    /// let tool = ToolDefinition::new(
958    ///     "add",
959    ///     "Add two numbers",
960    ///     r#"{"type":"object","properties":{"a":{"type":"integer"}}}"#,
961    /// );
962    /// let anthropic = tool.to_anthropic_tool();
963    /// assert_eq!(anthropic["name"], json!("add"));
964    /// assert!(anthropic["input_schema"].is_object());
965    /// ```
966    #[cfg(feature = "serde")]
967    pub fn to_anthropic_tool(&self) -> serde_json::Value {
968        let input_schema = self.merge_baked_examples_into(self.parse_input_schema_or_empty());
969        let description = self.deprecated_description_suffix();
970        serde_json::json!({
971            "name": self.name,
972            "description": description,
973            "input_schema": input_schema,
974        })
975    }
976
977    /// Convert this tool definition into the Model Context Protocol (MCP)
978    /// tool definition format.
979    ///
980    /// Spec: <https://modelcontextprotocol.io/>
981    ///
982    /// Returns a JSON object of the form:
983    /// ```json
984    /// {
985    ///   "name": "<tool name>",
986    ///   "description": "<tool description>",
987    ///   "inputSchema": { /* full JSON Schema */ }
988    /// }
989    /// ```
990    ///
991    /// If the stored `input_schema` is not valid JSON, the `inputSchema`
992    /// field will be emitted as an empty object (`{}`) so the surrounding
993    /// envelope remains a valid MCP tool descriptor.
994    ///
995    /// T-013: when the tool has a deprecation marker, the envelope
996    /// includes a `_meta` object with `deprecated`, `deprecatedSince`,
997    /// `removeIn`, and `replacedBy` fields. MCP-aware clients can
998    /// surface these directly to the user; the description is also
999    /// suffixed with `[DEPRECATED ...]` for older clients.
1000    ///
1001    /// # Example
1002    ///
1003    /// ```rust
1004    /// use tokitai_core::ToolDefinition;
1005    /// use serde_json::json;
1006    ///
1007    /// let tool = ToolDefinition::new(
1008    ///     "add",
1009    ///     "Add two numbers",
1010    ///     r#"{"type":"object","properties":{"a":{"type":"integer"}}}"#,
1011    /// );
1012    /// let mcp = tool.to_mcp_tool();
1013    /// assert_eq!(mcp["name"], json!("add"));
1014    /// assert!(mcp["inputSchema"].is_object());
1015    /// ```
1016    #[cfg(feature = "serde")]
1017    pub fn to_mcp_tool(&self) -> serde_json::Value {
1018        let input_schema = self.merge_baked_examples_into(self.parse_input_schema_or_empty());
1019        let description = self.deprecated_description_suffix();
1020        let mut envelope = serde_json::json!({
1021            "name": self.name,
1022            "description": description,
1023            "inputSchema": input_schema,
1024        });
1025        // T-013: surface structured deprecation metadata on the MCP
1026        // envelope. The object key is `_meta` per the MCP 2025-06-18
1027        // spec; the absence of any deprecation field means the tool
1028        // is current.
1029        if self.deprecated_since.is_some() || self.remove_in.is_some() || self.replaced_by.is_some()
1030        {
1031            let mut meta = serde_json::Map::new();
1032            meta.insert("deprecated".to_string(), serde_json::Value::Bool(true));
1033            if let Some(since) = self.deprecated_since.as_deref() {
1034                meta.insert(
1035                    "deprecatedSince".to_string(),
1036                    serde_json::Value::String(since.to_string()),
1037                );
1038            }
1039            if let Some(remove_in) = self.remove_in.as_deref() {
1040                meta.insert(
1041                    "removeIn".to_string(),
1042                    serde_json::Value::String(remove_in.to_string()),
1043                );
1044            }
1045            if let Some(replaced_by) = self.replaced_by.as_deref() {
1046                meta.insert(
1047                    "replacedBy".to_string(),
1048                    serde_json::Value::String(replaced_by.to_string()),
1049                );
1050            }
1051            envelope["_meta"] = serde_json::Value::Object(meta);
1052        }
1053        envelope
1054    }
1055
1056    /// T-013: helper for the provider-envelope emitters. Returns
1057    /// `self.description` (no copy when no deprecation is set) or
1058    /// `self.description` suffixed with a `[DEPRECATED ...]` marker
1059    /// the LLM can read. Kept private so callers always go through
1060    /// `to_openai_function` / `to_anthropic_tool` / `to_mcp_tool`.
1061    #[cfg(feature = "serde")]
1062    fn merge_baked_examples_into(&self, mut schema: serde_json::Value) -> serde_json::Value {
1063        if let Some(serde_json::Value::Array(arr)) = self.baked_examples.as_ref() {
1064            if !arr.is_empty() {
1065                if let serde_json::Value::Object(map) = &mut schema {
1066                    map.insert(
1067                        "examples".to_string(),
1068                        serde_json::Value::Array(arr.clone()),
1069                    );
1070                }
1071            }
1072        }
1073        schema
1074    }
1075    #[cfg(feature = "serde")]
1076    fn deprecated_description_suffix(&self) -> alloc::string::String {
1077        if self.deprecated_since.is_none() && self.remove_in.is_none() && self.replaced_by.is_none()
1078        {
1079            return self.description.clone();
1080        }
1081        let mut suffix = alloc::string::String::from(" [DEPRECATED");
1082        if let Some(since) = self.deprecated_since.as_deref() {
1083            suffix.push_str(&alloc::format!(" since={}", since));
1084        }
1085        if let Some(remove_in) = self.remove_in.as_deref() {
1086            suffix.push_str(&alloc::format!(" remove_in={}", remove_in));
1087        }
1088        if let Some(replaced_by) = self.replaced_by.as_deref() {
1089            if !replaced_by.is_empty() {
1090                suffix.push_str(&alloc::format!(" replaced_by={}", replaced_by));
1091            }
1092        }
1093        suffix.push(']');
1094        let mut out = self.description.clone();
1095        out.push_str(&suffix);
1096        out
1097    }
1098
1099    /// Parse `input_schema` into a `serde_json::Value`, falling back to an
1100    /// empty object on parse failure. Used by the multi-format exporters
1101    /// (`to_openai_function`, `to_anthropic_tool`, `to_mcp_tool`) so the
1102    /// outer envelope is always well-formed JSON regardless of the inner
1103    /// schema state.
1104    #[cfg(feature = "serde")]
1105    fn parse_input_schema_or_empty(&self) -> serde_json::Value {
1106        serde_json::from_str::<serde_json::Value>(&self.input_schema)
1107            .unwrap_or_else(|_| serde_json::json!({}))
1108    }
1109
1110    /// T-013: returns `true` when this tool's `remove_in` version is at
1111    /// or before `current_version` and the version strings are
1112    /// comparable as SemVer (three numeric components separated by
1113    /// `.`). Returns `false` when:
1114    ///
1115    /// * `remove_in` is `None`
1116    /// * `current_version` is `None` (no version gating configured at
1117    ///   the dispatcher level)
1118    /// * either string fails to parse as SemVer — we fail open so a
1119    ///   typo in a version string never silently removes a live tool.
1120    ///
1121    /// # Example
1122    ///
1123    /// ```rust
1124    /// use tokitai_core::ToolDefinition;
1125    ///
1126    /// let tool = ToolDefinition::new("legacy", "Old API", "{}")
1127    ///     .with_deprecated("1.0.0", "2.0.0", "modern");
1128    /// assert!(tool.is_removed(Some("2.0.0")));
1129    /// assert!(tool.is_removed(Some("3.0.0")));
1130    /// assert!(!tool.is_removed(Some("1.5.0")));
1131    /// assert!(!tool.is_removed(None));
1132    /// ```
1133    pub fn is_removed(&self, current_version: Option<&str>) -> bool {
1134        let (Some(remove_in), Some(current)) = (self.remove_in.as_deref(), current_version) else {
1135            return false;
1136        };
1137        match (parse_semver(remove_in), parse_semver(current)) {
1138            (Some(rm), Some(cur)) => cur >= rm,
1139            _ => false,
1140        }
1141    }
1142
1143    /// T-013: returns the structured `Removed` error a caller should
1144    /// receive when `is_removed(current)` is `true`. The error message
1145    /// includes the `remove_in` version and, when set, the
1146    /// `replaced_by` successor so the LLM client (or a human reader)
1147    /// has the context to retry with the new name.
1148    ///
1149    /// # Example
1150    ///
1151    /// ```rust
1152    /// use tokitai_core::ToolDefinition;
1153    /// use tokitai_core::ToolErrorKind;
1154    ///
1155    /// let tool = ToolDefinition::new("legacy", "Old API", "{}")
1156    ///     .with_deprecated("1.0.0", "2.0.0", "modern");
1157    /// let err = tool.removed_error(Some("2.5.0"));
1158    /// assert_eq!(err.kind, ToolErrorKind::Removed);
1159    /// assert!(err.message.contains("2.0.0"));
1160    /// assert!(err.message.contains("modern"));
1161    /// ```
1162    #[cfg(feature = "serde")]
1163    pub fn removed_error(&self, current_version: Option<&str>) -> ToolError {
1164        let remove_in = self.remove_in.as_deref().unwrap_or("?");
1165        let message = match (current_version, self.replaced_by.as_deref()) {
1166            (Some(cur), Some(repl)) if !repl.is_empty() => alloc::format!(
1167                "tool `{}` was removed in version {} (current: {}); use `{}` instead",
1168                self.name,
1169                remove_in,
1170                cur,
1171                repl
1172            ),
1173            (Some(cur), _) => alloc::format!(
1174                "tool `{}` was removed in version {} (current: {})",
1175                self.name,
1176                remove_in,
1177                cur
1178            ),
1179            (None, Some(repl)) if !repl.is_empty() => alloc::format!(
1180                "tool `{}` was removed in version {}; use `{}` instead",
1181                self.name,
1182                remove_in,
1183                repl
1184            ),
1185            (None, _) => {
1186                alloc::format!("tool `{}` was removed in version {}", self.name, remove_in)
1187            }
1188        };
1189        ToolError::removed(message)
1190    }
1191}
1192
1193/// Parse a SemVer-like version string (three numeric components
1194/// separated by `.`) into a `(u32, u32, u32)` tuple. Returns `None`
1195/// on any malformed input — we fail open elsewhere so a typo in a
1196/// version string never silently removes a live tool.
1197pub(crate) fn parse_semver(s: &str) -> Option<(u32, u32, u32)> {
1198    let s = s.trim();
1199    // Strip an optional `v` prefix and any pre-release / build
1200    // metadata, since neither affects ordering for T-013's
1201    // purposes (major.minor.patch only).
1202    let s = s.strip_prefix('v').unwrap_or(s);
1203    let core = s.split(['-', '+']).next().unwrap_or(s);
1204    let mut parts = core.split('.');
1205    let major = parts.next()?.parse::<u32>().ok()?;
1206    let minor = parts.next()?.parse::<u32>().ok()?;
1207    let patch = parts.next()?.parse::<u32>().ok()?;
1208    if parts.next().is_some() {
1209        return None;
1210    }
1211    Some((major, minor, patch))
1212}
1213
1214/// T-020: ordered comparison helper used by `is_in_interval`.
1215/// Returns `true` when `current >= other` under SemVer ordering
1216/// when both strings parse as SemVer, otherwise lexicographic
1217/// `>=` on the trimmed strings. Failing back to lexicographic
1218/// order keeps CalVer (e.g. `2026.06`) and commit-SHA strings
1219/// dispatch deterministically — every well-formed version
1220/// has a total order, even when neither side is SemVer.
1221pub(crate) fn version_gte(current: &str, other: &str) -> bool {
1222    match (parse_semver(current), parse_semver(other)) {
1223        (Some(a), Some(b)) => a >= b,
1224        _ => current.trim() >= other.trim(),
1225    }
1226}
1227
1228/// T-013: process-wide current version used to gate `remove_in`
1229/// removal at the dispatcher. Set via [`set_current_version`]; if
1230/// never set, the macro's `__call_*` wrappers run tools regardless
1231/// of their `remove_in` field. Wrapped in a `Mutex` so test
1232/// binaries (and programs that bump their version dynamically) can
1233/// override the slot; the production hot path takes a brief read
1234/// lock per call.
1235#[cfg(feature = "serde")]
1236static CURRENT_VERSION: std::sync::Mutex<Option<alloc::string::String>> =
1237    std::sync::Mutex::new(None);
1238
1239/// T-013: install a process-wide current version. The `#[tool]`
1240/// macro's sync wrapper compares this value against each tool's
1241/// `remove_in` field; when `remove_in <= current` the call returns
1242/// `ToolError::Removed` and the user is directed to `replaced_by`.
1243///
1244/// Call once at program startup, or whenever the running version
1245/// changes. If the version is unknown, simply do not call this
1246/// function — the call path stays open.
1247///
1248/// # Example
1249///
1250/// ```rust,ignore
1251/// use tokitai_core::set_current_version;
1252///
1253/// set_current_version("2.5.0");
1254/// ```
1255#[cfg(feature = "serde")]
1256pub fn set_current_version(version: impl Into<alloc::string::String>) {
1257    if let Ok(mut guard) = CURRENT_VERSION.lock() {
1258        *guard = Some(version.into());
1259    }
1260}
1261
1262/// T-013: clear any previously-registered current version. After
1263/// this call the macro stops gating `remove_in` until
1264/// [`set_current_version`] is called again. Useful in tests that
1265/// need to exercise the "no gating" path mid-suite.
1266#[cfg(feature = "serde")]
1267pub fn clear_current_version() {
1268    if let Ok(mut guard) = CURRENT_VERSION.lock() {
1269        *guard = None;
1270    }
1271}
1272
1273/// T-013: return the currently registered program version, or
1274/// `None` when [`set_current_version`] was never called.
1275///
1276/// # Example
1277///
1278/// ```rust
1279/// use tokitai_core::current_version;
1280///
1281/// // Without a registered version the result is `None`.
1282/// // (This doctest runs in a fresh process so it asserts `None`.)
1283/// let _ = current_version();
1284/// ```
1285#[cfg(feature = "serde")]
1286pub fn current_version() -> Option<alloc::string::String> {
1287    CURRENT_VERSION.lock().ok().and_then(|guard| guard.clone())
1288}
1289
1290impl core::fmt::Display for ToolDefinition {
1291    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1292        write!(f, "{}: {}", self.name, self.description)
1293    }
1294}
1295
1296/// JSON Schema type for a tool parameter.
1297///
1298/// # Example
1299///
1300/// ```rust
1301/// use tokitai_core::ParamType;
1302///
1303/// assert_eq!(ParamType::from_rust_type("String"), Some(ParamType::String));
1304/// assert_eq!(ParamType::from_rust_type("i32"), Some(ParamType::Integer));
1305/// assert_eq!(ParamType::Integer.as_str(), "integer");
1306/// ```
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1309#[repr(u8)]
1310pub enum ParamType {
1311    /// String type
1312    String = 0,
1313    /// Integer type
1314    Integer = 1,
1315    /// Number type (floating point)
1316    Number = 2,
1317    /// Boolean type
1318    Boolean = 3,
1319    /// Array type
1320    Array = 4,
1321    /// Object type
1322    Object = 5,
1323}
1324
1325impl ParamType {
1326    /// Return the JSON-Schema keyword for this variant (e.g. `"string"`).
1327    ///
1328    /// # Example
1329    ///
1330    /// ```rust
1331    /// use tokitai_core::ParamType;
1332    ///
1333    /// assert_eq!(ParamType::String.as_str(), "string");
1334    /// assert_eq!(ParamType::Integer.as_str(), "integer");
1335    /// ```
1336    pub fn as_str(&self) -> &'static str {
1337        match self {
1338            ParamType::String => "string",
1339            ParamType::Integer => "integer",
1340            ParamType::Number => "number",
1341            ParamType::Boolean => "boolean",
1342            ParamType::Array => "array",
1343            ParamType::Object => "object",
1344        }
1345    }
1346
1347    /// Best-effort mapping from a Rust type name to a JSON-Schema type.
1348    /// Returns `None` for `Option<T>` (use [`FromJsonValue`] for those).
1349    ///
1350    /// # Example
1351    ///
1352    /// ```rust
1353    /// use tokitai_core::ParamType;
1354    ///
1355    /// assert_eq!(ParamType::from_rust_type("String"), Some(ParamType::String));
1356    /// assert_eq!(ParamType::from_rust_type("i32"), Some(ParamType::Integer));
1357    /// assert_eq!(ParamType::from_rust_type("f64"), Some(ParamType::Number));
1358    /// assert_eq!(ParamType::from_rust_type("bool"), Some(ParamType::Boolean));
1359    /// assert_eq!(ParamType::from_rust_type("Vec<i32>"), Some(ParamType::Array));
1360    /// ```
1361    pub fn from_rust_type(type_name: &str) -> Option<Self> {
1362        match type_name {
1363            "String" | "str" => Some(ParamType::String),
1364            "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128"
1365            | "usize" | "isize" => Some(ParamType::Integer),
1366            "f32" | "f64" => Some(ParamType::Number),
1367            "bool" => Some(ParamType::Boolean),
1368            _ => {
1369                if type_name.starts_with("Vec<") {
1370                    Some(ParamType::Array)
1371                } else if type_name.starts_with("Option<") {
1372                    None
1373                } else {
1374                    Some(ParamType::Object)
1375                }
1376            }
1377        }
1378    }
1379}
1380
1381/// A single parameter definition for a tool.
1382///
1383/// # Example
1384///
1385/// ```rust
1386/// use tokitai_core::{ToolParameter, ParamType};
1387///
1388/// let param = ToolParameter::new(
1389///     "city",
1390///     ParamType::String,
1391///     "Name of the city",
1392///     true, // required
1393/// );
1394/// ```
1395#[derive(Debug, Clone)]
1396#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1397pub struct ToolParameter {
1398    /// Parameter name
1399    pub name: &'static str,
1400    /// Parameter type
1401    #[cfg_attr(feature = "serde", serde(rename = "type"))]
1402    pub param_type: ParamType,
1403    /// Parameter description
1404    pub description: &'static str,
1405    /// Whether the parameter is required
1406    pub required: bool,
1407}
1408
1409impl ToolParameter {
1410    /// Build a parameter definition.
1411    ///
1412    /// # Example
1413    ///
1414    /// ```rust
1415    /// use tokitai_core::{ToolParameter, ParamType};
1416    ///
1417    /// let param = ToolParameter::new("limit", ParamType::Integer, "Number of results to return", false);
1418    /// ```
1419    pub fn new(
1420        name: &'static str,
1421        param_type: ParamType,
1422        description: &'static str,
1423        required: bool,
1424    ) -> Self {
1425        Self {
1426            name,
1427            param_type,
1428            description,
1429            required,
1430        }
1431    }
1432}
1433
1434/// Error returned by tool invocations.
1435///
1436/// # Example
1437///
1438/// ```rust
1439/// use tokitai_core::{ToolError, ToolErrorKind};
1440///
1441/// let error = ToolError::validation_error("Missing required parameter 'city'");
1442/// assert_eq!(error.kind, ToolErrorKind::ValidationError);
1443/// ```
1444#[derive(Debug, Clone)]
1445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1446pub struct ToolError {
1447    /// Error type classification
1448    pub kind: ToolErrorKind,
1449    /// Error message
1450    #[cfg(feature = "serde")]
1451    pub message: crate::serde_types::String,
1452    #[cfg(not(feature = "serde"))]
1453    pub message: &'static str,
1454}
1455
1456#[cfg(feature = "serde")]
1457impl std::error::Error for ToolError {}
1458
1459#[cfg(feature = "serde")]
1460impl std::fmt::Display for ToolError {
1461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1462        write!(f, "ToolError: {:?} - {}", self.kind, self.message)
1463    }
1464}
1465
1466#[cfg(not(feature = "serde"))]
1467impl ToolError {
1468    /// Create a new error with the given `kind` and `message`.
1469    ///
1470    /// # Example
1471    ///
1472    /// ```rust
1473    /// use tokitai_core::{ToolError, ToolErrorKind};
1474    ///
1475    /// let err = ToolError::new(ToolErrorKind::ValidationError, "field 'name' is required");
1476    /// assert_eq!(err.kind, ToolErrorKind::ValidationError);
1477    /// ```
1478    pub fn new(kind: ToolErrorKind, message: &'static str) -> Self {
1479        Self { kind, message }
1480    }
1481
1482    /// Shortcut to build a `ValidationError` variant with the given message.
1483    ///
1484    /// # Example
1485    ///
1486    /// ```rust
1487    /// use tokitai_core::{ToolError, ToolErrorKind};
1488    ///
1489    /// let err = ToolError::validation_error("field 'name' is required");
1490    /// assert_eq!(err.kind, ToolErrorKind::ValidationError);
1491    /// ```
1492    pub fn validation_error(message: &'static str) -> Self {
1493        Self {
1494            kind: ToolErrorKind::ValidationError,
1495            message,
1496        }
1497    }
1498
1499    /// Shortcut to build a `NotFound` variant with the given tool name.
1500    ///
1501    /// # Example
1502    ///
1503    /// ```rust
1504    /// use tokitai_core::{ToolError, ToolErrorKind};
1505    ///
1506    /// let err = ToolError::not_found("unknown_tool");
1507    /// assert_eq!(err.kind, ToolErrorKind::NotFound);
1508    /// assert_eq!(err.message, "unknown_tool");
1509    /// ```
1510    pub fn not_found(message: &'static str) -> Self {
1511        Self {
1512            kind: ToolErrorKind::NotFound,
1513            message,
1514        }
1515    }
1516
1517    /// Shortcut to build an `InternalError` variant with the given message.
1518    ///
1519    /// # Example
1520    ///
1521    /// ```rust
1522    /// use tokitai_core::{ToolError, ToolErrorKind};
1523    ///
1524    /// let err = ToolError::internal_error("downstream timed out");
1525    /// assert_eq!(err.kind, ToolErrorKind::InternalError);
1526    /// ```
1527    pub fn internal_error(message: &'static str) -> Self {
1528        Self {
1529            kind: ToolErrorKind::InternalError,
1530            message,
1531        }
1532    }
1533
1534    /// Shortcut to build a `Removed` variant (T-013) with the given
1535    /// message. Returned by the macro's `__call_*` wrapper when the
1536    /// tool's `remove_in` version is at or before the program's
1537    /// current version.
1538    ///
1539    /// # Example
1540    ///
1541    /// ```rust
1542    /// use tokitai_core::{ToolError, ToolErrorKind};
1543    ///
1544    /// let err = ToolError::removed("tool removed in 1.0.0; use new_thing");
1545    /// assert_eq!(err.kind, ToolErrorKind::Removed);
1546    /// ```
1547    pub fn removed(message: &'static str) -> Self {
1548        Self {
1549            kind: ToolErrorKind::Removed,
1550            message,
1551        }
1552    }
1553
1554    /// T-019: shortcut to build a `Truncated` variant for
1555    /// `#[tool(result_truncate_bytes = N)]` runs that exceeded the
1556    /// declared byte budget. The diagnostic message identifies
1557    /// the result as a truncated payload so the LLM-side harness
1558    /// can decide whether to retry with a narrower input.
1559    ///
1560    /// # Example
1561    ///
1562    /// ```rust
1563    /// use tokitai_core::{ToolError, ToolErrorKind};
1564    ///
1565    /// let err = ToolError::truncated();
1566    /// assert_eq!(err.kind, ToolErrorKind::Truncated);
1567    /// ```
1568    pub fn truncated() -> Self {
1569        Self {
1570            kind: ToolErrorKind::Truncated,
1571            message: "tool result exceeded the result_truncate_bytes budget",
1572        }
1573    }
1574}
1575
1576#[cfg(feature = "serde")]
1577impl ToolError {
1578    /// Create a new error with the given `kind` and `message`.
1579    ///
1580    /// # Example
1581    ///
1582    /// ```rust
1583    /// use tokitai_core::{ToolError, ToolErrorKind};
1584    ///
1585    /// let err = ToolError::new(ToolErrorKind::ValidationError, "field 'name' is required");
1586    /// assert_eq!(err.kind, ToolErrorKind::ValidationError);
1587    /// ```
1588    pub fn new(kind: ToolErrorKind, message: impl Into<crate::serde_types::String>) -> Self {
1589        Self {
1590            kind,
1591            message: message.into(),
1592        }
1593    }
1594
1595    /// Shortcut to build a `ValidationError` variant with the given message.
1596    ///
1597    /// # Example
1598    ///
1599    /// ```rust
1600    /// use tokitai_core::{ToolError, ToolErrorKind};
1601    ///
1602    /// let err = ToolError::validation_error("field 'name' is required");
1603    /// assert_eq!(err.kind, ToolErrorKind::ValidationError);
1604    /// ```
1605    pub fn validation_error(message: impl Into<crate::serde_types::String>) -> Self {
1606        Self {
1607            kind: ToolErrorKind::ValidationError,
1608            message: message.into(),
1609        }
1610    }
1611
1612    /// Shortcut to build a `NotFound` variant with the given tool name.
1613    ///
1614    /// # Example
1615    ///
1616    /// ```rust
1617    /// use tokitai_core::{ToolError, ToolErrorKind};
1618    ///
1619    /// let err = ToolError::not_found("unknown_tool");
1620    /// assert_eq!(err.kind, ToolErrorKind::NotFound);
1621    /// ```
1622    pub fn not_found(message: impl Into<crate::serde_types::String>) -> Self {
1623        Self {
1624            kind: ToolErrorKind::NotFound,
1625            message: message.into(),
1626        }
1627    }
1628
1629    /// Shortcut to build an `InternalError` variant with the given message.
1630    ///
1631    /// # Example
1632    ///
1633    /// ```rust
1634    /// use tokitai_core::{ToolError, ToolErrorKind};
1635    ///
1636    /// let err = ToolError::internal_error("downstream timed out");
1637    /// assert_eq!(err.kind, ToolErrorKind::InternalError);
1638    /// ```
1639    pub fn internal_error(message: impl Into<crate::serde_types::String>) -> Self {
1640        Self {
1641            kind: ToolErrorKind::InternalError,
1642            message: message.into(),
1643        }
1644    }
1645
1646    /// Shortcut to build a `Removed` variant with the given message.
1647    /// T-013: returned by the macro's `__call_*` wrapper when the
1648    /// tool's `remove_in` version is at or before the program's
1649    /// current version.
1650    ///
1651    /// # Example
1652    ///
1653    /// ```rust
1654    /// use tokitai_core::{ToolError, ToolErrorKind};
1655    ///
1656    /// let err = ToolError::removed("tool removed in 1.0.0; use new_thing");
1657    /// assert_eq!(err.kind, ToolErrorKind::Removed);
1658    /// ```
1659    pub fn removed(message: impl Into<crate::serde_types::String>) -> Self {
1660        Self {
1661            kind: ToolErrorKind::Removed,
1662            message: message.into(),
1663        }
1664    }
1665
1666    /// T-019: shortcut to build a `Truncated` variant for
1667    /// `#[tool(result_truncate_bytes = N)]` runs that exceeded the
1668    /// declared byte budget. The `message` includes the original
1669    /// and kept byte counts so a downstream log scraper can
1670    /// decide whether the dropped bytes matter.
1671    ///
1672    /// # Example
1673    ///
1674    /// ```rust
1675    /// use tokitai_core::{ToolError, ToolErrorKind};
1676    ///
1677    /// let err = ToolError::truncated_with(8000, 4096);
1678    /// assert_eq!(err.kind, ToolErrorKind::Truncated);
1679    /// assert!(err.message.contains("8000"));
1680    /// assert!(err.message.contains("4096"));
1681    /// ```
1682    pub fn truncated_with(original_bytes: usize, kept_bytes: usize) -> Self {
1683        Self {
1684            kind: ToolErrorKind::Truncated,
1685            message: format!(
1686                "tool result exceeded the result_truncate_bytes budget: \
1687                 original {} bytes, kept {} bytes",
1688                original_bytes, kept_bytes
1689            ),
1690        }
1691    }
1692}
1693
1694/// Classification of a [`ToolError`] for structured error handling.
1695///
1696/// # Example
1697///
1698/// ```rust
1699/// use tokitai_core::ToolErrorKind;
1700///
1701/// // The six classifications:
1702/// assert_ne!(ToolErrorKind::ValidationError, ToolErrorKind::NotFound);
1703/// assert_ne!(ToolErrorKind::InternalError, ToolErrorKind::TypeError);
1704/// assert_ne!(ToolErrorKind::Removed, ToolErrorKind::NotFound);
1705/// assert_ne!(ToolErrorKind::Truncated, ToolErrorKind::InternalError);
1706/// ```
1707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1708#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1709#[repr(u8)]
1710pub enum ToolErrorKind {
1711    /// Validation error - parameter validation failed
1712    ValidationError = 0,
1713    /// Not found - requested tool does not exist
1714    NotFound = 1,
1715    /// Internal error - tool execution failed
1716    InternalError = 2,
1717    /// Type error - parameter type mismatch
1718    TypeError = 3,
1719    /// Removed - the tool's `remove_in` version is at or before the
1720    /// program's current version (T-013). Callers should consult
1721    /// `ToolError::message` (and `replaced_by` if present) to
1722    /// discover the successor.
1723    Removed = 4,
1724    /// T-019: the tool's serialized result exceeded the per-method
1725    /// `result_truncate_bytes` budget. The error's `message`
1726    /// carries the diagnostic (`"original N bytes, kept M bytes"`)
1727    /// and the macro also emits a `tracing::warn!` when the
1728    /// `trace` feature is on. The original payload is dropped;
1729    /// downstream callers should treat this as a partial answer
1730    /// and (where the consumer's contract allows) call the tool
1731    /// again with a narrower input.
1732    Truncated = 5,
1733}
1734
1735/// Compile-time tool registry trait.
1736///
1737/// Auto-implemented by the `#[tool]` macro for any `impl` block it processes.
1738///
1739/// # Example
1740///
1741/// ```rust,ignore
1742/// use tokitai_core::ToolProvider;
1743/// // After `#[tool]` on a type:
1744/// // let tools = Calculator::tool_definitions();
1745/// // let count = Calculator::tool_count();
1746/// // let tool  = Calculator::find_tool("add");
1747/// ```
1748pub trait ToolProvider {
1749    /// All tool definitions produced by this provider.
1750    fn tool_definitions() -> &'static [ToolDefinition];
1751
1752    /// Number of tools produced by this provider.
1753    fn tool_count() -> usize {
1754        Self::tool_definitions().len()
1755    }
1756
1757    /// Look up a tool definition by its `name`.
1758    fn find_tool(name: &str) -> Option<&'static ToolDefinition> {
1759        Self::tool_definitions().iter().find(|t| t.name == name)
1760    }
1761}
1762
1763/// Runtime tool invocation trait. Auto-implemented by the `#[tool]` macro.
1764///
1765/// # Example
1766///
1767/// ```rust,ignore
1768/// use tokitai_core::{ToolProvider, ToolCaller};
1769/// use serde_json::json;
1770///
1771/// let calc = Calculator;
1772/// let result = calc.call_tool("add", &json!({"a": 10, "b": 20})).unwrap();
1773/// assert_eq!(result, json!(30));
1774/// ```
1775#[cfg(feature = "serde")]
1776pub trait ToolCaller {
1777    /// Invoke a tool by `name` with the given JSON `args`.
1778    ///
1779    /// # Example
1780    ///
1781    /// ```rust,ignore
1782    /// use tokitai_core::ToolCaller;
1783    /// use serde_json::json;
1784    ///
1785    /// // After `#[tool]` has been applied to Calculator:
1786    /// // let result = calc.call_tool("add", &json!({"a": 1, "b": 2})).unwrap();
1787    /// // assert_eq!(result, json!(3));
1788    /// ```
1789    ///
1790    /// # Errors
1791    ///
1792    /// Returns a [`ToolError`] of kind [`ToolErrorKind::NotFound`] if no tool
1793    /// with the given name is registered, or of kind [`ToolErrorKind::ValidationError`]
1794    /// / [`ToolErrorKind::InternalError`] if argument parsing or tool
1795    /// execution fails.
1796    fn call_tool(
1797        &self,
1798        name: &str,
1799        args: &crate::serde_types::Value,
1800    ) -> Result<crate::serde_types::Value, ToolError>;
1801}
1802
1803/// # From Json Value Trait
1804///
1805/// Parses JSON values into Rust types. Implemented once per type and used by
1806/// the `#[tool]` macro to extract typed parameters from JSON arguments.
1807///
1808/// # Example
1809///
1810/// ```rust
1811/// use tokitai_core::FromJsonValue;
1812/// use serde_json::json;
1813///
1814/// let args = json!({"count": 42, "name": "test"});
1815/// let count = i64::from_json_value(&args, "count").unwrap();
1816/// let name = String::from_json_value(&args, "name").unwrap();
1817/// assert_eq!(count, 42);
1818/// assert_eq!(name, "test");
1819/// ```
1820#[cfg(feature = "serde")]
1821pub trait FromJsonValue: Sized {
1822    /// Extract a typed value for `key` from a JSON arguments object.
1823    ///
1824    /// # Example
1825    ///
1826    /// ```rust
1827    /// use tokitai_core::FromJsonValue;
1828    /// use serde_json::json;
1829    ///
1830    /// let args = json!({"count": 42});
1831    /// let count = i64::from_json_value(&args, "count").unwrap();
1832    /// assert_eq!(count, 42);
1833    /// ```
1834    ///
1835    /// # Errors
1836    ///
1837    /// Returns a [`ToolError`] of kind [`ToolErrorKind::ValidationError`] when
1838    /// `key` is missing or its value does not match `Self`'s expected type.
1839    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError>;
1840
1841    /// Extract a typed value for `key`, or `None` if extraction fails.
1842    ///
1843    /// Equivalent to `Self::from_json_value(args, key).ok()`. Useful for
1844    /// optional parameters.
1845    ///
1846    /// # Example
1847    ///
1848    /// ```rust
1849    /// use tokitai_core::FromJsonValue;
1850    /// use serde_json::json;
1851    ///
1852    /// let args = json!({});
1853    /// let missing: Option<i64> = i64::from_json_value_opt(&args, "absent");
1854    /// assert_eq!(missing, None);
1855    ///
1856    /// let args = json!({"count": 7});
1857    /// let present = i64::from_json_value_opt(&args, "count");
1858    /// assert_eq!(present, Some(7));
1859    /// ```
1860    fn from_json_value_opt(args: &crate::serde_types::Value, key: &str) -> Option<Self> {
1861        Self::from_json_value(args, key).ok()
1862    }
1863}
1864
1865// ============== Primitive type impls ==============
1866
1867#[cfg(feature = "serde")]
1868impl FromJsonValue for i64 {
1869    #[inline(always)]
1870    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1871        args.get(key)
1872            .ok_or_else(|| {
1873                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1874            })?
1875            .as_i64()
1876            .ok_or_else(|| {
1877                ToolError::validation_error(format!(
1878                    "Parameter '{}' has wrong type, expected integer",
1879                    key
1880                ))
1881            })
1882    }
1883}
1884
1885#[cfg(feature = "serde")]
1886impl FromJsonValue for i32 {
1887    #[inline(always)]
1888    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1889        args.get(key)
1890            .ok_or_else(|| {
1891                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1892            })?
1893            .as_i64()
1894            .map(|v| v as i32)
1895            .ok_or_else(|| {
1896                ToolError::validation_error(format!(
1897                    "Parameter '{}' has wrong type, expected integer",
1898                    key
1899                ))
1900            })
1901    }
1902}
1903
1904#[cfg(feature = "serde")]
1905impl FromJsonValue for u64 {
1906    #[inline(always)]
1907    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1908        args.get(key)
1909            .ok_or_else(|| {
1910                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1911            })?
1912            .as_u64()
1913            .ok_or_else(|| {
1914                ToolError::validation_error(format!(
1915                    "Parameter '{}' has wrong type, expected unsigned integer",
1916                    key
1917                ))
1918            })
1919    }
1920}
1921
1922#[cfg(feature = "serde")]
1923impl FromJsonValue for u32 {
1924    #[inline(always)]
1925    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1926        args.get(key)
1927            .ok_or_else(|| {
1928                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1929            })?
1930            .as_u64()
1931            .map(|v| v as u32)
1932            .ok_or_else(|| {
1933                ToolError::validation_error(format!(
1934                    "Parameter '{}' has wrong type, expected unsigned integer",
1935                    key
1936                ))
1937            })
1938    }
1939}
1940
1941#[cfg(feature = "serde")]
1942impl FromJsonValue for f64 {
1943    #[inline(always)]
1944    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1945        args.get(key)
1946            .ok_or_else(|| {
1947                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1948            })?
1949            .as_f64()
1950            .ok_or_else(|| {
1951                ToolError::validation_error(format!(
1952                    "Parameter '{}' has wrong type, expected number",
1953                    key
1954                ))
1955            })
1956    }
1957}
1958
1959#[cfg(feature = "serde")]
1960impl FromJsonValue for f32 {
1961    #[inline(always)]
1962    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1963        args.get(key)
1964            .ok_or_else(|| {
1965                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1966            })?
1967            .as_f64()
1968            .map(|v| v as f32)
1969            .ok_or_else(|| {
1970                ToolError::validation_error(format!(
1971                    "Parameter '{}' has wrong type, expected number",
1972                    key
1973                ))
1974            })
1975    }
1976}
1977
1978#[cfg(feature = "serde")]
1979impl FromJsonValue for bool {
1980    #[inline(always)]
1981    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
1982        args.get(key)
1983            .ok_or_else(|| {
1984                ToolError::validation_error(format!("Missing required parameter '{}'", key))
1985            })?
1986            .as_bool()
1987            .ok_or_else(|| {
1988                ToolError::validation_error(format!(
1989                    "Parameter '{}' has wrong type, expected boolean",
1990                    key
1991                ))
1992            })
1993    }
1994}
1995
1996#[cfg(feature = "serde")]
1997impl FromJsonValue for String {
1998    #[inline(always)]
1999    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
2000        args.get(key)
2001            .ok_or_else(|| {
2002                ToolError::validation_error(format!("Missing required parameter '{}'", key))
2003            })?
2004            .as_str()
2005            .map(|s| s.to_string())
2006            .ok_or_else(|| {
2007                ToolError::validation_error(format!(
2008                    "Parameter '{}' has wrong type, expected string",
2009                    key
2010                ))
2011            })
2012    }
2013}
2014
2015// ============== &str zero-copy support ==============
2016// Special handling: needs a lifetime, so we expose a standalone function.
2017
2018/// Borrow a string parameter without copying.
2019///
2020/// # Example
2021///
2022/// ```rust
2023/// use tokitai_core::from_json_value_str;
2024/// use serde_json::json;
2025///
2026/// let args = json!({"city": "Paris"});
2027/// let city: &str = from_json_value_str(&args, "city").unwrap();
2028/// assert_eq!(city, "Paris");
2029/// ```
2030///
2031/// # Errors
2032///
2033/// Returns a [`ToolError`] of kind [`ToolErrorKind::ValidationError`] when
2034/// `key` is missing or its value is not a string.
2035#[cfg(feature = "serde")]
2036#[inline(always)]
2037pub fn from_json_value_str<'a>(
2038    args: &'a crate::serde_types::Value,
2039    key: &str,
2040) -> Result<&'a str, ToolError> {
2041    args.get(key)
2042        .ok_or_else(|| {
2043            ToolError::validation_error(format!("Missing required parameter '{}'", key))
2044        })?
2045        .as_str()
2046        .ok_or_else(|| {
2047            ToolError::validation_error(format!("Parameter '{}' type error, expected string", key))
2048        })
2049}
2050
2051// ============== Option impls ==============
2052
2053#[cfg(feature = "serde")]
2054impl<T: FromJsonValue> FromJsonValue for Option<T> {
2055    #[inline(always)]
2056    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
2057        Ok(T::from_json_value_opt(args, key))
2058    }
2059}
2060
2061// ============== Vec impls ==============
2062
2063#[cfg(feature = "serde")]
2064impl<T: serde::de::DeserializeOwned> FromJsonValue for Vec<T> {
2065    #[inline(always)]
2066    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
2067        let value = args.get(key).ok_or_else(|| {
2068            ToolError::validation_error(format!("Missing required parameter '{}'", key))
2069        })?;
2070        serde_json::from_value(value.clone()).map_err(|e| {
2071            ToolError::validation_error(format!("Parameter '{}' has wrong type: {}", key, e))
2072        })
2073    }
2074}
2075
2076// ============== Helper: parse any DeserializeOwned type ==============
2077// For unsupported custom types, users can deserialize manually inside their method.
2078
2079/// Parse a `DeserializeOwned` value for `key`. Useful for custom types not
2080/// covered by the [`FromJsonValue`] blanket impls.
2081///
2082/// # Example
2083///
2084/// ```rust
2085/// use tokitai_core::from_json_value_generic;
2086/// use serde_json::json;
2087/// use serde::Deserialize;
2088///
2089/// #[derive(Deserialize, Debug, PartialEq)]
2090/// struct Point { x: i32, y: i32 }
2091///
2092/// let args = json!({"p": {"x": 1, "y": 2}});
2093/// let p: Point = from_json_value_generic(&args, "p").unwrap();
2094/// assert_eq!(p, Point { x: 1, y: 2 });
2095/// ```
2096///
2097/// # Errors
2098///
2099/// Returns a [`ToolError`] of kind [`ToolErrorKind::ValidationError`] when
2100/// `key` is missing or its value cannot be deserialized into `T`.
2101#[cfg(feature = "serde")]
2102#[inline(always)]
2103pub fn from_json_value_generic<T: serde::de::DeserializeOwned>(
2104    args: &crate::serde_types::Value,
2105    key: &str,
2106) -> Result<T, ToolError> {
2107    let value = args.get(key).ok_or_else(|| {
2108        ToolError::validation_error(format!("Missing required parameter '{}'", key))
2109    })?;
2110    serde_json::from_value(value.clone())
2111        .map_err(|e| ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e)))
2112}
2113
2114// ============== HashMap impls ==============
2115#[cfg(feature = "serde")]
2116impl<V: serde::de::DeserializeOwned> FromJsonValue for std::collections::HashMap<String, V> {
2117    #[inline(always)]
2118    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
2119        let value = args.get(key).ok_or_else(|| {
2120            ToolError::validation_error(format!("Missing required parameter '{}'", key))
2121        })?;
2122        serde_json::from_value(value.clone()).map_err(|e| {
2123            ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e))
2124        })
2125    }
2126}
2127
2128// ============== BTreeMap impls ==============
2129#[cfg(feature = "serde")]
2130impl<V: serde::de::DeserializeOwned> FromJsonValue for std::collections::BTreeMap<String, V> {
2131    #[inline(always)]
2132    fn from_json_value(args: &crate::serde_types::Value, key: &str) -> Result<Self, ToolError> {
2133        let value = args.get(key).ok_or_else(|| {
2134            ToolError::validation_error(format!("Missing required parameter '{}'", key))
2135        })?;
2136        serde_json::from_value(value.clone()).map_err(|e| {
2137            ToolError::validation_error(format!("Parameter '{}' type error: {}", key, e))
2138        })
2139    }
2140}
2141
2142/// Runtime configuration types used to override compile-time tool metadata.
2143#[cfg(feature = "serde")]
2144pub mod config;
2145
2146/// # Runtime-Agnostic Async Executor
2147///
2148/// Bridge for the `#[tool]` macro's sync-from-async path that decouples
2149/// Tokitai from Tokio. The macro's sync wrapper delegates to a registered
2150/// [`AsyncExecutor`]; install one with [`set_async_executor`] to use
2151/// `async-std`, `smol`, `embassy`, or any custom executor.
2152///
2153/// ## Default behaviour
2154///
2155/// 1. Custom executor registered via [`set_async_executor`] -> use it.
2156/// 2. Otherwise, if inside a Tokio runtime, use the active
2157///    `Handle::block_on` (preserves backward compatibility).
2158/// 3. Otherwise, return a [`ToolError`] with a descriptive English message.
2159///
2160/// ## No-`std` fallback
2161///
2162/// When the `serde` feature is disabled the crate is `no_std` and no
2163/// executor is reachable; [`block_on_async`] always returns a
2164/// `ToolError::InternalError` so the macro can still type-check.
2165///
2166/// [`block_on_async`]: crate::block_on_async
2167///
2168/// This trait is **object-safe** so the user-registered executor can be
2169/// stored as `&'static dyn AsyncExecutor`. The type-erased
2170/// [`AsyncExecutor::block_on_dyn`] takes a boxed future and returns a boxed
2171/// output; typed convenience is provided by [`AsyncExecutorExt`].
2172///
2173/// ## T-003 per-call override seam
2174///
2175/// Implementations may override [`AsyncExecutor::block_on_for`] to return
2176/// `Some(...)` when an ambient executor (per-`call_tool`, per-thread, etc.)
2177/// is reachable. The `#[tool]` macro's sync-from-async wrapper probes
2178/// `block_on_for` first, then the global slot set via
2179/// [`set_async_executor`], then the active Tokio runtime. This is how
2180/// `async-std` / `smol` / `embassy` users supply an executor without
2181/// mutating global state.
2182pub trait AsyncExecutor: Send + Sync {
2183    /// Object-safe entry point: drive a boxed future to completion on the
2184    /// current thread and return its output as a boxed `Any`. Most users
2185    /// implement [`AsyncExecutorExt::block_on`] and let the blanket impl
2186    /// derive this; override it directly only for full control.
2187    ///
2188    /// # Example
2189    ///
2190    /// ```rust,ignore
2191    /// use tokitai_core::AsyncExecutor;
2192    /// use core::future::Future;
2193    /// use core::pin::Pin;
2194    /// use core::any::Any;
2195    ///
2196    /// struct ThreadPoolExecutor;
2197    /// impl AsyncExecutor for ThreadPoolExecutor {
2198    ///     fn block_on_dyn(
2199    ///         &self,
2200    ///         future: Pin<Box<dyn Future<Output = ()> + Send>>,
2201    ///     ) -> Box<dyn Any + Send> {
2202    ///         // ... drive the future to completion ...
2203    ///         # Box::new(())
2204    ///     }
2205    /// }
2206    /// ```
2207    fn block_on_dyn(
2208        &self,
2209        future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
2210    ) -> Box<dyn core::any::Any + Send>;
2211
2212    /// Per-call override seam (T-003). The `#[tool]` macro's sync
2213    /// wrapper probes this *before* the global slot, so users who do not
2214    /// want a process-wide side effect can supply an executor locally.
2215    ///
2216    /// Return `None` to indicate "no ambient executor; fall through to
2217    /// the global slot / Tokio probe." The default implementation always
2218    /// returns `None`; runtime-aware executors (async-std, smol, embassy)
2219    /// override it to return `Some(...)` when their runtime is reachable
2220    /// on the current thread.
2221    ///
2222    /// # Example
2223    ///
2224    /// ```rust,ignore
2225    /// use tokitai_core::AsyncExecutor;
2226    ///
2227    /// struct AsyncStdExecutor;
2228    /// impl AsyncExecutor for AsyncStdExecutor {
2229    ///     fn block_on_dyn(
2230    ///         &self,
2231    ///         future: core::pin::Pin<
2232    ///             Box<dyn core::future::Future<Output = ()> + Send>,
2233    ///         >,
2234    ///     ) -> Box<dyn core::any::Any + Send> {
2235    ///         // ... drive the future ...
2236    ///         # Box::new(())
2237    ///     }
2238    ///
2239    ///     fn block_on_for(
2240    ///         &self,
2241    ///     ) -> Option<&'static dyn AsyncExecutor> {
2242    ///         // async-std sets a thread-local handle; return `Some(self)`
2243    ///         // when one is reachable on this thread.
2244    ///         None
2245    ///     }
2246    /// }
2247    /// ```
2248    fn block_on_for(&self) -> Option<&'static dyn AsyncExecutor> {
2249        None
2250    }
2251}
2252
2253/// Typed convenience wrapper around [`AsyncExecutor::block_on_dyn`].
2254/// Implemented for every `T: AsyncExecutor`; re-introduces the natural
2255/// `block_on<F: Future>(&self, F) -> F::Output` signature on top of the
2256/// type-erased entry point.
2257///
2258/// # Example
2259///
2260/// ```rust,ignore
2261/// use tokitai_core::{AsyncExecutor, AsyncExecutorExt};
2262///
2263/// fn drive<E: AsyncExecutor>(exec: &E) -> String {
2264///     exec.block_on(async { String::from("typed output") })
2265/// }
2266/// ```
2267pub trait AsyncExecutorExt: AsyncExecutor {
2268    /// Drive a future to completion and return its output.
2269    ///
2270    /// The future must be `Send + 'static` (so it can cross the type-erasure
2271    /// boundary) and its output must be `Send + 'static` (so it can be
2272    /// downcast on the call site).
2273    ///
2274    /// # Example
2275    ///
2276    /// ```rust,ignore
2277    /// use tokitai_core::{AsyncExecutor, AsyncExecutorExt};
2278    ///
2279    /// fn run<E: AsyncExecutor>(exec: &E) -> i32 {
2280    ///     exec.block_on(async { 21 + 21 })
2281    /// }
2282    /// ```
2283    ///
2284    /// # Panics
2285    ///
2286    /// Panics if the internal output slot mutex is poisoned or if the
2287    /// underlying [`AsyncExecutor::block_on_dyn`] returns without driving the
2288    /// future to completion.
2289    fn block_on<F>(&self, future: F) -> F::Output
2290    where
2291        F: core::future::Future + Send + 'static,
2292        F::Output: Send + 'static,
2293    {
2294        use core::any::Any;
2295        use core::sync::atomic::{AtomicBool, Ordering};
2296        use std::sync::{Arc, Mutex};
2297
2298        let slot: Arc<Mutex<Option<F::Output>>> = Arc::new(Mutex::new(None));
2299        let ran: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
2300        let slot_inner = slot.clone();
2301        let ran_inner = ran.clone();
2302
2303        let wrapped = async move {
2304            let output = future.await;
2305            *slot_inner
2306                .lock()
2307                .expect("AsyncExecutor output slot poisoned") = Some(output);
2308            ran_inner.store(true, Ordering::Release);
2309        };
2310
2311        let _: Box<dyn Any + Send> = self.block_on_dyn(Box::pin(wrapped));
2312
2313        assert!(
2314            ran.load(Ordering::Acquire),
2315            "AsyncExecutor did not drive the future to completion"
2316        );
2317
2318        let mut guard = slot.lock().expect("AsyncExecutor output slot poisoned");
2319        guard
2320            .take()
2321            .expect("future reported complete but produced no output")
2322    }
2323}
2324
2325impl<T: AsyncExecutor + ?Sized> AsyncExecutorExt for T {}
2326
2327/// Process-wide slot that holds the user-registered [`AsyncExecutor`].
2328/// A `OnceLock<Box<dyn AsyncExecutor>>` is the portable, leak-free
2329/// storage: the box is leaked into the static slot, so the resulting
2330/// reference is `&'static`.
2331static ASYNC_EXECUTOR: std::sync::OnceLock<Box<dyn AsyncExecutor>> = std::sync::OnceLock::new();
2332
2333/// Install a global [`AsyncExecutor`] used by every `#[tool]`-generated sync
2334/// wrapper. Call once at program startup, before any sync `call_tool` is
2335/// invoked on an `async` tool. The box is leaked: the executor lives for the
2336/// lifetime of the program. The first call wins; subsequent calls are
2337/// silently ignored (best-effort registration).
2338///
2339/// # Global fallback role (T-003)
2340///
2341/// `set_async_executor` is the **process-wide fallback** used by the
2342/// `#[tool]` macro. The sync-from-async wrapper resolves an executor in
2343/// this order:
2344///
2345/// 1. [`block_on_for_executor`] — per-call / per-thread override probe
2346///    (T-003). Lets users wire async-std / smol / embassy without mutating
2347///    global state.
2348/// 2. The executor registered via [`set_async_executor`] (this function).
2349///    This is the slot `async-std` / `smol` / `embassy` users populate
2350///    when they want a single, program-wide bridge.
2351/// 3. The active Tokio runtime, when one is reachable on the current
2352///    thread.
2353/// 4. Otherwise, [`block_on_async`] returns a [`ToolError`] with the
2354///    canonical English message.
2355///
2356/// Use case: an `async-std` user can call this once at startup with an
2357/// `async_std::task::Executor` wrapper. The macro then resolves the
2358/// wrapper on every sync `call_tool` without the user touching the
2359/// macro.
2360///
2361/// # Example
2362///
2363/// ```rust,ignore
2364/// use tokitai_core::{set_async_executor, AsyncExecutor, AsyncExecutorExt};
2365/// use core::future::Future;
2366/// use core::pin::Pin;
2367///
2368/// struct BlockingExecutor;
2369/// impl AsyncExecutor for BlockingExecutor {
2370///     fn block_on_dyn(
2371///         &self,
2372///         future: Pin<Box<dyn Future<Output = ()> + Send>>,
2373///     ) -> Box<dyn core::any::Any + Send> {
2374///         Box::new(futures::executor::block_on(future))
2375///     }
2376/// }
2377///
2378/// set_async_executor(Box::new(BlockingExecutor));
2379/// ```
2380pub fn set_async_executor<E: AsyncExecutor + 'static>(executor: Box<E>) {
2381    let trait_obj: Box<dyn AsyncExecutor> = executor;
2382    // `_ =` to ignore the `AlreadyInitialized` error; the registration
2383    // is best-effort. The first executor wins.
2384    let _ = ASYNC_EXECUTOR.set(trait_obj);
2385}
2386
2387/// Return the currently registered [`AsyncExecutor`], or `None` if no
2388/// executor has been installed via [`set_async_executor`]. The returned
2389/// reference is `&'static` because the registered `Box<dyn AsyncExecutor>`
2390/// is held inside a `OnceLock` for the program's lifetime.
2391///
2392/// # Example
2393///
2394/// ```rust,ignore
2395/// use tokitai_core::current_async_executor;
2396///
2397/// // Initially no executor has been installed.
2398/// assert!(current_async_executor().is_none());
2399/// ```
2400pub fn current_async_executor() -> Option<&'static dyn AsyncExecutor> {
2401    ASYNC_EXECUTOR
2402        .get()
2403        .map(|b| b.as_ref() as &'static dyn AsyncExecutor)
2404}
2405
2406/// T-003 per-call override probe. Returns an executor that should be
2407/// preferred over the global slot. The macro's sync-from-async wrapper
2408/// calls this first; only on `None` does it fall back to
2409/// [`current_async_executor`] and the active Tokio runtime.
2410///
2411/// This hook lets users wire a runtime-aware executor (async-std / smol
2412/// / embassy) without leaking a global side effect from
2413/// [`set_async_executor`]. The default implementation returns `None` so
2414/// existing executors keep their old behaviour.
2415///
2416/// # Example
2417///
2418/// ```rust,ignore
2419/// use tokitai_core::block_on_for_executor;
2420///
2421/// // Without a registered executor this returns `None`.
2422/// assert!(block_on_for_executor().is_none());
2423/// ```
2424pub fn block_on_for_executor() -> Option<&'static dyn AsyncExecutor> {
2425    ASYNC_EXECUTOR.get().and_then(|b| b.block_on_for())
2426}
2427
2428/// Try to drive `future` to completion using the registered executor, the
2429/// current Tokio runtime (when available), or a clear English error.
2430///
2431/// This is the entry point used by the `#[tool]` macro. It is intentionally
2432/// panic-free and always returns a [`ToolError`] on failure so the macro
2433/// can propagate the error through `call_tool_sync` without `unwrap`.
2434///
2435/// # Example
2436///
2437/// ```rust,ignore
2438/// use tokitai_core::{block_on_async, set_async_executor, AsyncExecutor, AsyncExecutorExt};
2439/// use core::future::Future;
2440/// use core::pin::Pin;
2441/// use core::any::Any;
2442///
2443/// struct InlineExecutor;
2444/// impl AsyncExecutor for InlineExecutor {
2445///     fn block_on_dyn(
2446///         &self,
2447///         future: Pin<Box<dyn Future<Output = ()> + Send>>,
2448///     ) -> Box<dyn Any + Send> {
2449///         futures::executor::block_on(future);
2450///         Box::new(())
2451///     }
2452/// }
2453///
2454/// set_async_executor(Box::new(InlineExecutor));
2455/// let value: i32 = block_on_async(async { 21 + 21 }).unwrap();
2456/// assert_eq!(value, 42);
2457/// ```
2458///
2459/// # Errors
2460///
2461/// Returns a [`ToolError`] of kind [`ToolErrorKind::InternalError`] when no
2462/// executor is registered and no Tokio runtime is in scope. The `#[tool]`
2463/// macro also probes for a Tokio runtime and surfaces a richer message via
2464/// [`block_on_async_error_message`].
2465#[cfg(feature = "serde")]
2466pub fn block_on_async<F>(future: F) -> Result<F::Output, ToolError>
2467where
2468    F: core::future::Future + Send + 'static,
2469    F::Output: Send + 'static,
2470{
2471    // T-003: probe the per-call override seam *before* the global slot
2472    // so async-std / smol / embassy users who override `block_on_for`
2473    // on their registered executor get the per-thread handle when it
2474    // is reachable.
2475    if let Some(exec) = block_on_for_executor() {
2476        return Ok(exec.block_on(future));
2477    }
2478    if let Some(exec) = current_async_executor() {
2479        return Ok(exec.block_on(future));
2480    }
2481
2482    // No executor was registered. The `#[tool]` macro's sync wrappers
2483    // additionally probe the current Tokio runtime as a final fallback;
2484    // here we only handle the user-registered executor path and return
2485    // a clear error so the macro can surface a helpful message.
2486    drop(future);
2487    Err(ToolError::internal_error(block_on_async_error_message()))
2488}
2489
2490/// `no_std`/`no-serde` companion to [`block_on_async`]. Always returns an
2491/// error because no executor is reachable in a `no_std` build.
2492///
2493/// # Errors
2494///
2495/// Always returns `Err` with a static explanation string instructing the
2496/// caller to enable the `serde` feature and register an executor.
2497#[cfg(not(feature = "serde"))]
2498pub fn block_on_async<F>(_future: F) -> Result<F, &'static str> {
2499    Err(
2500        "no async runtime available in no_std build; enable the `serde` feature \
2501         and call `tokitai_core::set_async_executor(...)` to enable sync-from-async",
2502    )
2503}
2504
2505/// Canonical English error message returned by the `#[tool]` macro when
2506/// it cannot find any executor to drive a sync-from-async call. The macro
2507/// embeds the return value of this function as the error string, so the
2508/// message is centralised here for consistency and future i18n.
2509///
2510/// # Example
2511///
2512/// ```rust
2513/// use tokitai_core::block_on_async_error_message;
2514///
2515/// let msg = block_on_async_error_message();
2516/// assert!(msg.contains("no async runtime"));
2517/// ```
2518pub const fn block_on_async_error_message() -> &'static str {
2519    "no async runtime registered; either call from within an async context, \
2520     run inside a tokio runtime, or call `tokitai_core::set_async_executor(...)` \
2521     before invoking"
2522}
2523
2524// -------------------------------------------------------------------------
2525// T-004: runtime-agnostic async sleep.
2526//
2527// Resilience decorators (`#[retry]`, `#[rate_limit]`,
2528// `#[circuit_breaker]`) on `async fn` need to wait between attempts
2529// without blocking the runtime worker thread. With Tokio that is
2530// `tokio::time::sleep`; with async-std, `async_std::task::sleep`; with
2531// smol, `smol::Timer::after`. None of those crates are in scope for
2532// `tokitai-core` (it is zero-dep / no-`tokio`).
2533//
2534// The helper below provides a runtime-agnostic equivalent: spawn a
2535// thread that parks for the requested duration and wakes the future's
2536// `Waker` when the deadline elapses. The user-registered executor
2537// drives the future through `block_on`; the spawned thread is the
2538// "timer" replacement. This costs one short-lived thread per sleep,
2539// which is the same trade-off `async-std` / `smol` make internally.
2540//
2541// The sleep does NOT pull in any runtime crate — it only uses
2542// `std::thread` and `std::time`. The future is `Send + 'static` so it
2543// can cross the `AsyncExecutor` type-erasure boundary, and it is
2544// `Unpin` so it composes inside `select!`-style macros without an
2545// unsafe `Pin::get_unsafe_mut`.
2546// -------------------------------------------------------------------------
2547
2548/// Runtime-agnostic async sleep future returned by [`async_sleep`].
2549///
2550/// Polling the future for the first time spawns a single thread that
2551/// parks for the requested duration and wakes the registered
2552/// `Waker` when the deadline elapses. Subsequent polls return
2553/// `Poll::Pending` until the waker is invoked, at which point the
2554/// next poll returns `Poll::Ready(())`.
2555///
2556/// Used by the resilience decorator macros (`#[retry]`,
2557/// `#[rate_limit]`, `#[circuit_breaker]`) when wrapped around an
2558/// `async fn`. The sleep yields to the runtime for the full
2559/// duration, never blocks the executor thread.
2560#[cfg(feature = "serde")]
2561pub struct AsyncSleep {
2562    /// Deadline measured against `Instant::now()` at construction time.
2563    deadline: std::time::Instant,
2564    /// `true` once the timer thread has been spawned.
2565    armed: bool,
2566}
2567
2568#[cfg(feature = "serde")]
2569impl AsyncSleep {
2570    /// Construct a sleep that completes after `dur` from "now". The
2571    /// caller drives the future to completion via whatever executor
2572    /// it has in scope (Tokio / async-std / smol / custom).
2573    #[must_use]
2574    pub fn new(dur: std::time::Duration) -> Self {
2575        Self {
2576            deadline: std::time::Instant::now() + dur,
2577            armed: false,
2578        }
2579    }
2580
2581    /// Time remaining until the deadline elapses. Returns `Duration::ZERO`
2582    /// once the deadline has passed.
2583    pub fn remaining(&self) -> std::time::Duration {
2584        self.deadline
2585            .saturating_duration_since(std::time::Instant::now())
2586    }
2587}
2588
2589#[cfg(feature = "serde")]
2590impl core::future::Future for AsyncSleep {
2591    type Output = ();
2592    fn poll(
2593        mut self: core::pin::Pin<&mut Self>,
2594        cx: &mut core::task::Context<'_>,
2595    ) -> core::task::Poll<()> {
2596        // If the deadline has already passed, complete immediately.
2597        if std::time::Instant::now() >= self.deadline {
2598            return core::task::Poll::Ready(());
2599        }
2600        let remaining = self.remaining();
2601        if !self.armed {
2602            // Arm the timer exactly once. The spawned thread parks
2603            // for `remaining` and then wakes the future's waker.
2604            // We `clone` the waker so the timer thread can outlive
2605            // the current `Context` and the future still observes
2606            // the wake even if it was re-polled in between.
2607            let waker = cx.waker().clone();
2608            std::thread::spawn(move || {
2609                std::thread::park_timeout(remaining);
2610                waker.wake();
2611            });
2612            self.armed = true;
2613        }
2614        core::task::Poll::Pending
2615    }
2616}
2617
2618/// Sleep for `dur` without blocking the current thread.
2619///
2620/// Returns a future that completes after the given duration. The
2621/// future is `Send + 'static` and `Unpin`, so it composes with
2622/// any executor and with `select!`-style combinators.
2623///
2624/// This is the **async** counterpart to `std::thread::sleep`:
2625/// polling the future yields control to the runtime, which is
2626/// free to drive other tasks while the deadline elapses. The
2627/// `#[retry]`, `#[rate_limit]`, and `#[circuit_breaker]` macros
2628/// emit `await tokitai_core::async_sleep(...)` on `async fn`
2629/// bodies so that backoff between attempts does not stall a
2630/// runtime worker thread.
2631///
2632/// # Example
2633///
2634/// ```rust,ignore
2635/// use tokitai_core::async_sleep;
2636/// use std::time::Duration;
2637///
2638/// async fn retryable() -> Result<(), &'static str> {
2639///     for attempt in 1..3 {
2640///         match try_once().await {
2641///             Ok(()) => return Ok(()),
2642///             Err(_) if attempt < 3 => {
2643///                 async_sleep(Duration::from_millis(100 * attempt)).await;
2644///             }
2645///             Err(e) => return Err(e),
2646///         }
2647///     }
2648///     Ok(())
2649/// }
2650///
2651/// async fn try_once() -> Result<(), &'static str> { Ok(()) }
2652/// ```
2653#[cfg(feature = "serde")]
2654#[must_use]
2655pub fn async_sleep(dur: std::time::Duration) -> AsyncSleep {
2656    AsyncSleep::new(dur)
2657}
2658
2659#[cfg(feature = "serde")]
2660mod executor_internal {
2661    use super::*;
2662
2663    /// A no-op executor used as a placeholder. Registered executors must
2664    /// implement [`AsyncExecutor`]; this stub panics on use so an accidental
2665    /// `set_async_executor(Box::new(NullExecutor))` is loud rather than
2666    /// silently broken.
2667    pub struct NullExecutor;
2668
2669    impl AsyncExecutor for NullExecutor {
2670        /// # Panics
2671        ///
2672        /// Always panics with a message instructing the caller to install
2673        /// a real executor via [`crate::set_async_executor`].
2674        fn block_on_dyn(
2675            &self,
2676            _future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
2677        ) -> Box<dyn core::any::Any + Send> {
2678            panic!(
2679                "NullExecutor::block_on_dyn invoked; \
2680                 install a real AsyncExecutor via set_async_executor(...)"
2681            )
2682        }
2683    }
2684}
2685
2686#[cfg(feature = "serde")]
2687pub mod serde_types {
2688    //! Re-exports of `serde_json` and `alloc::string` types under a stable
2689    //! path. Available when the `serde` feature is enabled.
2690
2691    pub use alloc::string::String;
2692    pub use serde_json::Value;
2693}
2694
2695/// Compile-time helper for emitting JSON Schema strings without runtime
2696/// overhead.
2697///
2698/// # Example
2699///
2700/// ```rust,ignore
2701/// use tokitai_core::json_schema;
2702///
2703/// const SCHEMA: &str = json_schema!({
2704///     "city": {
2705///         type: String,
2706///         description: "Name of the city",
2707///         required: true,
2708///     }
2709/// });
2710/// ```
2711#[macro_export]
2712macro_rules! json_schema {
2713    (
2714        {
2715            $($param_name:literal: {
2716                type: $param_type:ident,
2717                description: $description:literal,
2718                required: $required:literal $(,)?
2719            }),*
2720            $(,)?
2721        }
2722    ) => {{
2723        const SCHEMA: &str = concat!(
2724            "{\"type\":\"object\",\"properties\":{",
2725            $({
2726                concat!(
2727                    "\"", $param_name, "\":",
2728                    "{\"type\":\"", $crate::ParamType::$param_type.as_str(), "\",\"description\":\"", $description, "\"}"
2729                )
2730            },)*
2731            "},\"required\":[",
2732            $({
2733                if $required { concat!("\"", $param_name, "\"") } else { "" }
2734            },)*
2735            "]}"
2736        );
2737        SCHEMA
2738    }};
2739}
2740
2741// ===========================================================================
2742// T-023: per-tool capability manifest
2743//
2744// Every `#[tool]` method declares the capabilities it requires
2745// (`db:read:sales`, `net:egress:smtp`, etc.). The macro emits a
2746// per-method `CAPABILITIES_<NAME>` const plus a per-impl aggregated
2747// `CAPABILITIES` slice; the MCP server walks that slice at startup
2748// to enforce the operator-supplied allowlist.
2749//
2750// The manifest type below is the on-the-wire shape: a `Vec<(String,
2751// Vec<String>)>` mapping `tool_name -> required_capabilities`. The
2752// `Vec<String>` and `Vec<(String, Vec<String>)>` types are the
2753// minimum surface the server needs to read the manifest without
2754// pulling in any new external types. The shape is owned (not
2755// `&'static`) because the manifest is built at server start from
2756// the macro-emitted const slices; the const slice itself is
2757// `&'static`, but the on-the-wire `Vec` is filled with cloned
2758// strings for the allowlist walk.
2759// ===========================================================================
2760
2761/// T-023: per-tool capability manifest. Each tuple is
2762/// `(tool_name, required_capabilities)`. The macro emits an
2763/// aggregated `CAPABILITIES` slice per impl block; the server
2764/// folds those slices into a single `CapabilityManifest` at
2765/// startup before walking the allowlist.
2766///
2767/// The shape is `Vec<(String, Vec<String>)>` (no new external
2768/// types) per the T-023 acceptance criteria. A `Vec<(String,
2769/// Vec<String>)>` is the minimum surface that lets the
2770/// `tokitai-mcp-server` enforce the allowlist without depending
2771/// on a third-party manifest crate (and without violating the
2772/// "no new top-level deps" rule).
2773///
2774/// # Example
2775///
2776/// ```rust
2777/// use tokitai_core::CapabilityManifest;
2778///
2779/// let manifest: CapabilityManifest = vec![
2780///     ("send_email".to_string(), vec!["db:read:sales".to_string(), "net:egress:smtp".to_string()]),
2781///     ("summarize".to_string(), vec!["db:read:sales".to_string()]),
2782/// ];
2783/// assert_eq!(manifest.len(), 2);
2784/// ```
2785pub type CapabilityManifest = alloc::vec::Vec<(
2786    alloc::string::String,
2787    alloc::vec::Vec<alloc::string::String>,
2788)>;
2789
2790/// T-023: trait the `#[tool]` macro auto-implements for any
2791/// `impl` block it processes. Exposes the aggregated
2792/// `CAPABILITIES` slice (per-method name + per-method required
2793/// capability tokens) so the `tokitai-mcp-server` builder can
2794/// walk it at start time without depending on a per-impl
2795/// generated type.
2796///
2797/// The default implementation returns an empty slice (no
2798/// capabilities declared) so providers that did not opt in to
2799/// the manifest path keep working unchanged. The `#[tool]`
2800/// macro overrides the default to return the per-impl
2801/// `CAPABILITIES` slice it baked at compile time.
2802///
2803/// # Example
2804///
2805/// ```rust
2806/// use tokitai_core::CapabilityManifestProvider;
2807///
2808/// fn check<T: CapabilityManifestProvider>() {
2809///     let manifest = T::capability_manifest();
2810///     // walk every (method, requires) entry
2811///     for (tool, caps) in manifest {
2812///         for c in *caps {
2813///             // ...
2814///         }
2815///         let _ = (tool, caps);
2816///     }
2817/// }
2818/// ```
2819pub trait CapabilityManifestProvider {
2820    /// Return the aggregated `(tool_name, requires)` slice for
2821    /// this provider. The slice is `&'static` so no allocation
2822    /// happens on the hot path.
2823    fn capability_manifest() -> &'static [(&'static str, &'static [&'static str])] {
2824        // Default: no capabilities declared. The `#[tool]`
2825        // macro overrides this with the aggregated slice it
2826        // baked at compile time. Providers that have not been
2827        // processed by `#[tool]` (e.g. an empty unit struct
2828        // used as a placeholder) keep the empty default.
2829        &[]
2830    }
2831}
2832
2833/// T-023: returns `true` when `declared` is covered by some entry
2834/// in `allowlist`. The allowlist supports one wildcard form: a
2835/// trailing `*` (e.g. `db:read:*`) is treated as a prefix match
2836/// (so `db:read:*` covers `db:read:sales`, `db:read:any_resource`,
2837/// and any other `db:read:<X>`). Exact entries (no `*`) must
2838/// match the declared capability verbatim. The matcher is
2839/// case-sensitive: capability tokens are typically
2840/// lowercase-with-colons in the documented category set, and
2841/// allowing case folding would weaken the allowlist contract
2842/// without a clear use case.
2843///
2844/// The match runs in `O(N * M)` where `N` is the number of
2845/// declared capabilities and `M` is the allowlist length. Both
2846/// are expected to be small (a few dozen entries at most), so a
2847/// linear scan is fine.
2848///
2849/// # Example
2850///
2851/// ```rust
2852/// use tokitai_core::capability_in_allowlist;
2853///
2854/// let allowlist = vec!["db:read:*".to_string(), "net:egress:smtp".to_string()];
2855/// assert!(capability_in_allowlist("db:read:sales", &allowlist));
2856/// assert!(capability_in_allowlist("net:egress:smtp", &allowlist));
2857/// assert!(!capability_in_allowlist("db:delete:users", &allowlist));
2858/// ```
2859pub fn capability_in_allowlist(declared: &str, allowlist: &[alloc::string::String]) -> bool {
2860    for entry in allowlist {
2861        if let Some(prefix) = entry.strip_suffix('*') {
2862            // Wildcard: `db:read:*` matches anything that starts
2863            // with `db:read:`. We require the prefix to end with
2864            // a `:` separator (or be empty) so a bare `*` does
2865            // not silently match every capability.
2866            if prefix.is_empty() {
2867                // `*` alone — match everything. Documented as a
2868                // valid (if risky) operator escape hatch.
2869                return true;
2870            }
2871            if declared.starts_with(prefix) {
2872                return true;
2873            }
2874        } else if entry.as_str() == declared {
2875            return true;
2876        }
2877    }
2878    false
2879}
2880
2881#[cfg(test)]
2882mod capability_manifest_tests {
2883    use super::*;
2884
2885    #[test]
2886    fn exact_match_succeeds() {
2887        let allowlist = alloc::vec!["net:egress:smtp".to_string()];
2888        assert!(capability_in_allowlist("net:egress:smtp", &allowlist));
2889    }
2890
2891    #[test]
2892    fn exact_match_misses() {
2893        let allowlist = alloc::vec!["net:egress:smtp".to_string()];
2894        assert!(!capability_in_allowlist("net:egress:http", &allowlist));
2895    }
2896
2897    #[test]
2898    fn wildcard_prefix_matches() {
2899        let allowlist = alloc::vec!["db:read:*".to_string()];
2900        assert!(capability_in_allowlist("db:read:sales", &allowlist));
2901        assert!(capability_in_allowlist("db:read:any_resource", &allowlist));
2902    }
2903
2904    #[test]
2905    fn wildcard_prefix_does_not_match_unrelated() {
2906        let allowlist = alloc::vec!["db:read:*".to_string()];
2907        assert!(!capability_in_allowlist("db:write:sales", &allowlist));
2908        assert!(!capability_in_allowlist("net:egress:smtp", &allowlist));
2909    }
2910
2911    #[test]
2912    fn empty_allowlist_fails_closed() {
2913        let allowlist: alloc::vec::Vec<alloc::string::String> = alloc::vec::Vec::new();
2914        assert!(!capability_in_allowlist("db:read:sales", &allowlist));
2915    }
2916
2917    #[test]
2918    fn bare_star_matches_anything() {
2919        let allowlist = alloc::vec!["*".to_string()];
2920        assert!(capability_in_allowlist("db:read:sales", &allowlist));
2921        assert!(capability_in_allowlist("process:exec", &allowlist));
2922    }
2923}
2924
2925// ===========================================================================
2926// T-024: cross-crate version assertion
2927//
2928// A `tokitai-core` consumer pinned to one minor line (say `0.7`) can
2929// silently drift against another consumer (a third-party crate) that
2930// pinned a different line (say `0.8`): both compile, but the agent sees
2931// a tool shape from one version and a call site from the other. The
2932// fix is structural:
2933//   1. A compile-time `pub const CORE_VERSION` carrying the exact
2934//      version of `tokitai-core` that was compiled in (via
2935//      `env!("CARGO_PKG_VERSION")`). Consumers re-export this from
2936//      their own build script to compare against their pinned version.
2937//   2. A `pub const fn assert_compatible_with(expected: &str)` whose
2938//      body is a `const {}` block. When the function is called from a
2939//      downstream crate inside a `const` context, a mismatch raises a
2940//      `compile_error!` naming both versions and the docs.rs migration
2941//      link. Match rule: exact match for `x.y.z`, prefix match for
2942//      `x.y` or `x` (e.g. `0.8` matches `0.8.1`).
2943// ===========================================================================
2944
2945/// T-024: exact version string of `tokitai-core` that was compiled in
2946/// (sourced from `CARGO_PKG_VERSION`). The mcp-server build script
2947/// writes a generated source file with this constant so the running
2948/// binary can compare against the version that was baked at compile
2949/// time.
2950///
2951/// # Example
2952///
2953/// ```rust
2954/// use tokitai_core::CORE_VERSION;
2955///
2956/// let _v: &str = CORE_VERSION;
2957/// assert!(!CORE_VERSION.is_empty());
2958/// ```
2959pub const CORE_VERSION: &str = env!("CARGO_PKG_VERSION");
2960
2961/// T-024: assert at compile time that the compiled-against
2962/// `tokitai-core` matches the expected SemVer string under the
2963/// standard prefix-match rule (`x.y` matches `x.y.z`; `x.y.z` matches
2964/// exactly). A mismatch raises a `compile_error!` naming both
2965/// versions and the docs.rs migration link.
2966///
2967/// The function is `pub const fn` so the check runs in the caller's
2968/// `const` context. When called outside a `const` context the check
2969/// still runs but produces a runtime panic instead of a compile error.
2970///
2971/// # Match rule (canonical SemVer)
2972///
2973/// | `expected` | `CORE_VERSION` | result |
2974/// |------------|------------------|--------|
2975/// | `"0.8.1"`  | `"0.8.1"`        | passes (exact) |
2976/// | `"0.8"`    | `"0.8.1"`        | passes (prefix) |
2977/// | `"0.8.1"`  | `"0.8.2"`        | fails |
2978/// | `"0.9"`    | `"0.8.1"`        | fails |
2979///
2980/// # Example (positive)
2981///
2982/// ```rust
2983/// use tokitai_core::assert_compatible_with;
2984///
2985/// // Inside your own crate, gated by a `const _: () = ...;` block
2986/// // so the check fires at compile time:
2987/// const _: () = {
2988///     tokitai_core::assert_compatible_with("0.6");
2989/// };
2990/// ```
2991///
2992/// # Negative example (cross-crate drift)
2993///
2994/// A consumer crate that pins `tokitai = "0.7"` and calls
2995/// `assert_compatible_with("0.8")` would fail to compile with a
2996/// diagnostic naming both versions and pointing at the docs.rs
2997/// migration guide. The compile-time guarantee is the whole point
2998/// of T-024 — drift surfaces before the binary ever runs.
2999pub const fn assert_compatible_with(expected: &str) {
3000    // T-024: keep the panic message static so it can be reached
3001    // inside a `const fn`. Dynamic formatters are not const-stable
3002    // for non-`'static` references, so we parse the version
3003    // components inline (no `semver::Version::parse`, which is not
3004    // a `const fn` in the `semver` crate today) and produce a
3005    // static drift-category message on mismatch.
3006    //
3007    // Match rule (canonical SemVer): 1 component matches any on
3008    // the same major; 2 components match any on the same
3009    // major.minor; 3 components must match exactly. The
3010    // pre-release / build suffix is ignored for ordering.
3011    //
3012    // When called from a `const _: () = assert_compatible_with("0.8")`
3013    // context inside a downstream crate, the compiler evaluates
3014    // this function at compile time. A mismatch surfaces as a
3015    // `compile_error!` naming the drift category and the docs.rs
3016    // migration guide link. When called from a runtime context the
3017    // same panic fires at run time, which is the documented
3018    // fallback.
3019
3020    // Strip the optional `v`/`V` prefix from `expected`.
3021    let expected_bytes = expected.as_bytes();
3022    let mut exp_offset: usize = 0;
3023    let mut exp_new_len = expected_bytes.len();
3024    if !expected_bytes.is_empty() {
3025        let first = expected_bytes[0];
3026        if first == b'v' || first == b'V' {
3027            exp_offset = 1;
3028            exp_new_len = expected_bytes.len() - 1;
3029        }
3030    }
3031    let expected_tail = unsafe {
3032        core::slice::from_raw_parts(expected_bytes.as_ptr().add(exp_offset), exp_new_len)
3033    };
3034    // SAFETY: `expected_tail` is a sub-slice of a valid `&str`;
3035    // the `v`/`V` prefix is single-byte ASCII so removing it
3036    // cannot break UTF-8 invariants.
3037    let expected_str = unsafe { core::str::from_utf8_unchecked(expected_tail) };
3038
3039    // Strip the optional `v`/`V` prefix from `CORE_VERSION`.
3040    let core_bytes = CORE_VERSION.as_bytes();
3041    let mut core_offset: usize = 0;
3042    let mut core_new_len = core_bytes.len();
3043    if !core_bytes.is_empty() {
3044        let first = core_bytes[0];
3045        if first == b'v' || first == b'V' {
3046            core_offset = 1;
3047            core_new_len = core_bytes.len() - 1;
3048        }
3049    }
3050    let core_tail =
3051        unsafe { core::slice::from_raw_parts(core_bytes.as_ptr().add(core_offset), core_new_len) };
3052    let core_str_stripped = unsafe { core::str::from_utf8_unchecked(core_tail) };
3053
3054    // Inline SemVer parser (const-stable). Returns the three
3055    // numeric components and the textual arity. A non-numeric
3056    // byte inside a component is a parse failure (returns the
3057    // `Err` arm). The pre-release / build suffix is stripped
3058    // before parsing so the ordering matches the canonical
3059    // SemVer 2.0 rule (build metadata ignored for comparison).
3060    let expected_parts = parse_semver_const(expected_str);
3061    let core_parts = parse_semver_const(core_str_stripped);
3062
3063    match (expected_parts, core_parts) {
3064        (Some(ep), Some(cp)) => {
3065            let (emaj, emin, epat, eary) = ep;
3066            let (cmaj, cmin, cpat, _) = cp;
3067            let _ = cpat; // silence unused-when-not-3 warning
3068                          // 3-arity requires exact patch match; 2-arity skips
3069                          // the patch check; 1-arity skips minor and patch.
3070                          // We always check the major. The arity was computed
3071                          // from the textual form of `expected` (1, 2, or 3
3072                          // numeric components).
3073            if eary >= 1 && cmaj != emaj {
3074                panic!(concat!(
3075                    "tokitai-core version mismatch: major drift. ",
3076                    "compiled CORE_VERSION=",
3077                    env!("CARGO_PKG_VERSION"),
3078                    " differs from the caller's `expected` argument on the major component. ",
3079                    "See https://docs.rs/tokitai-core for the migration guide."
3080                ));
3081            }
3082            if eary >= 2 && cmin != emin {
3083                panic!(concat!(
3084                    "tokitai-core version mismatch: minor drift. ",
3085                    "compiled CORE_VERSION=",
3086                    env!("CARGO_PKG_VERSION"),
3087                    " differs from the caller's `expected` argument on the minor component. ",
3088                    "See https://docs.rs/tokitai-core for the migration guide."
3089                ));
3090            }
3091            if eary >= 3 && cpat != epat {
3092                panic!(concat!(
3093                    "tokitai-core version mismatch: patch drift. ",
3094                    "compiled CORE_VERSION=",
3095                    env!("CARGO_PKG_VERSION"),
3096                    " differs from the caller's `expected` argument on the patch component. ",
3097                    "See https://docs.rs/tokitai-core for the migration guide."
3098                ));
3099            }
3100        }
3101        _ => {
3102            // Either side failed to parse. A malformed expected is
3103            // a typo; a malformed core is an internal
3104            // inconsistency. Either way, fail loud rather than
3105            // silently passing.
3106            panic!(concat!(
3107                "tokitai-core version mismatch: invalid SemVer literal. ",
3108                "compiled CORE_VERSION=",
3109                env!("CARGO_PKG_VERSION"),
3110                " (one or both sides failed to parse). ",
3111                "See https://docs.rs/tokitai-core for the migration guide."
3112            ));
3113        }
3114    }
3115}
3116
3117/// T-028: non-const runtime companion to [`assert_compatible_with`].
3118///
3119/// Same check as the `const fn`, but the panic message uses
3120/// `format!()` so it can include *both* the `expected` argument and
3121/// the actual `CORE_VERSION` verbatim. Use this from runtime call
3122/// sites (typically `fn main` of a downstream binary) when you want
3123/// the on-panic message to read like
3124/// `tokitai-core version mismatch: expected="0.7", actual="0.8.1" (major drift)`
3125/// rather than the static-category message the `const fn` panic
3126/// emits.
3127///
3128/// For the compile-time const-eval use case (`const _: () =
3129/// assert_compatible_with("...");`) the `const fn` is still the
3130/// right entry point - `const_format_args!` is not stable, so the
3131/// `const fn` panic message cannot interpolate runtime strings.
3132pub fn assert_compatible_with_runtime(expected: &str) {
3133    // Re-parse both sides so the drift classification matches.
3134    let expected_str = expected
3135        .strip_prefix('v')
3136        .or_else(|| expected.strip_prefix('V'))
3137        .unwrap_or(expected);
3138    let core_str_stripped = CORE_VERSION
3139        .strip_prefix('v')
3140        .or_else(|| CORE_VERSION.strip_prefix('V'))
3141        .unwrap_or(CORE_VERSION);
3142    let expected_parts = parse_semver_const(expected_str);
3143    let core_parts = parse_semver_const(core_str_stripped);
3144    match (expected_parts, core_parts) {
3145        (Some(ep), Some(cp)) => {
3146            let (emaj, emin, epat, eary) = ep;
3147            let (cmaj, cmin, cpat, _) = cp;
3148            if eary >= 1 && cmaj != emaj {
3149                panic!(
3150                    "tokitai-core version mismatch: expected={}, actual={} (major drift). See https://docs.rs/tokitai-core for the migration guide.",
3151                    expected, CORE_VERSION
3152                );
3153            }
3154            if eary >= 2 && cmin != emin {
3155                panic!(
3156                    "tokitai-core version mismatch: expected={}, actual={} (minor drift). See https://docs.rs/tokitai-core for the migration guide.",
3157                    expected, CORE_VERSION
3158                );
3159            }
3160            if eary >= 3 && cpat != epat {
3161                panic!(
3162                    "tokitai-core version mismatch: expected={}, actual={} (patch drift). See https://docs.rs/tokitai-core for the migration guide.",
3163                    expected, CORE_VERSION
3164                );
3165            }
3166        }
3167        _ => panic!(
3168            "tokitai-core version mismatch: expected={}, actual={} (invalid SemVer literal on one or both sides). See https://docs.rs/tokitai-core for the migration guide.",
3169            expected, CORE_VERSION
3170        ),
3171    }
3172}
3173
3174/// T-024: const-stable SemVer prefix parser. Returns
3175/// `(major, minor, patch, arity)` when the input parses as
3176/// `x[.y[.z]]` (each component is a sequence of ASCII digits;
3177/// pre-release `-...` and build `+...` suffixes are accepted and
3178/// ignored). `arity` is the number of numeric components found
3179/// in the textual form (1, 2, or 3).
3180///
3181/// Returns `None` when:
3182/// * the input is empty,
3183/// * any component is non-numeric,
3184/// * there are more than three components,
3185/// * or a component has leading `0` followed by another digit
3186///   (canonical SemVer disallows this; we enforce it so the
3187///   comparison is well-defined).
3188const fn parse_semver_const(s: &str) -> Option<(u64, u64, u64, usize)> {
3189    let bytes = s.as_bytes();
3190    let mut i: usize = 0;
3191    let mut parts: [u64; 3] = [0, 0, 0];
3192    let mut arity: usize = 0;
3193    let mut end = bytes.len();
3194    if end == 0 {
3195        return None;
3196    }
3197    // Strip pre-release / build metadata.
3198    while i < end {
3199        let b = bytes[i];
3200        if b == b'-' || b == b'+' {
3201            end = i;
3202            break;
3203        }
3204        i += 1;
3205    }
3206    i = 0;
3207    while i <= end {
3208        let at_end = i == end;
3209        let is_dot = !at_end && bytes[i] == b'.';
3210        if at_end || is_dot {
3211            // Empty component (e.g. ".0" or trailing ".") -> parse
3212            // failure.
3213            if i == 0 {
3214                return None;
3215            }
3216            if bytes[i - 1] == b'.' {
3217                return None;
3218            }
3219            if arity >= 3 {
3220                return None;
3221            }
3222            arity += 1;
3223            if at_end {
3224                break;
3225            }
3226        } else {
3227            let b = bytes[i];
3228            if b < b'0' || b > b'9' {
3229                return None;
3230            }
3231            // Reject leading-zero components (`01`) so the
3232            // ordering is unambiguous. A "leading zero" is a `0`
3233            // at the very start of a component that is followed
3234            // by another digit before the next `.` or end.
3235            if b == b'0' {
3236                let prev_is_dot = i == 0 || bytes[i - 1] == b'.';
3237                if prev_is_dot {
3238                    let next_is_dot_or_end = i + 1 == end || bytes[i + 1] == b'.';
3239                    if !next_is_dot_or_end {
3240                        return None;
3241                    }
3242                }
3243            }
3244            // Refuse to accumulate a fourth component. We must
3245            // check this BEFORE writing `parts[arity]`, otherwise
3246            // an input like "0.0.0.0" would silently index past
3247            // the end of the `parts` array and panic in const
3248            // context. The `.`-terminated case is caught by the
3249            // `arity >= 3` guard above; the digit case (no dot
3250            // before the 4th component) needs its own guard.
3251            if arity >= 3 {
3252                return None;
3253            }
3254            // Accumulate into the current component. We are
3255            // guaranteed `arity < 3` because we early-return above
3256            // when arity would reach 3, and we increment AFTER the
3257            // component ends (`.` or EOF).
3258            let slot = &mut parts[arity];
3259            let mul = match slot.checked_mul(10) {
3260                Some(v) => v,
3261                None => return None,
3262            };
3263            let add = match mul.checked_add((b - b'0') as u64) {
3264                Some(v) => v,
3265                None => return None,
3266            };
3267            *slot = add;
3268        }
3269        i += 1;
3270    }
3271    Some((parts[0], parts[1], parts[2], arity))
3272}
3273
3274#[cfg(test)]
3275mod tests {
3276    use super::*;
3277
3278    #[test]
3279    fn test_param_type_from_rust_type() {
3280        assert_eq!(ParamType::from_rust_type("String"), Some(ParamType::String));
3281        assert_eq!(ParamType::from_rust_type("i32"), Some(ParamType::Integer));
3282        assert_eq!(ParamType::from_rust_type("f64"), Some(ParamType::Number));
3283        assert_eq!(ParamType::from_rust_type("bool"), Some(ParamType::Boolean));
3284        assert_eq!(
3285            ParamType::from_rust_type("Vec<i32>"),
3286            Some(ParamType::Array)
3287        );
3288    }
3289
3290    #[test]
3291    fn test_tool_definition_const() {
3292        let tool = ToolDefinition::new("test", "A test tool", "{}");
3293        assert_eq!(tool.name, "test");
3294        assert_eq!(tool.description, "A test tool");
3295    }
3296
3297    /// C-1: `parse_semver_const` must reject anything with more
3298    /// than three numeric components. The pre-fix code only
3299    /// guarded the `.`-terminated branch (`if arity >= 3`),
3300    /// leaving a hole: an input like `"0.0.0.0"` reaches the
3301    /// fourth digit, falls into the digit-accumulation branch,
3302    /// and writes `parts[arity]` with `arity == 3`, panicking
3303    /// in const context.
3304    #[test]
3305    fn parse_semver_const_rejects_extra_component() {
3306        assert_eq!(parse_semver_const("0.0.0.0"), None);
3307        assert_eq!(parse_semver_const("1.2.3.4"), None);
3308        assert_eq!(parse_semver_const("0.5.1.2.3"), None);
3309        // Sanity: 3-component inputs still parse.
3310        assert_eq!(parse_semver_const("0.5.1"), Some((0, 5, 1, 3)));
3311    }
3312
3313    #[cfg(feature = "serde")]
3314    #[test]
3315    fn test_tool_definition_to_json() {
3316        let tool = ToolDefinition::new("test", "A test tool", r#"{"type":"object"}"#);
3317        let json = tool.to_json().unwrap();
3318        assert!(json.contains(r#""name":"test""#));
3319    }
3320
3321    // -----------------------------------------------------------------
3322    // AsyncExecutor unit tests
3323    //
3324    // The three scenarios the executor must cover are exercised in a
3325    // single `#[test]` so the order is deterministic. Splitting them
3326    // into three `#[test]` functions would not be reliable here:
3327    // libtest runs tests in alphabetical order, and the global
3328    // `ASYNC_EXECUTOR` slot is a `OnceLock` that can only be populated
3329    // once per process. A fresh-process test for the "no executor"
3330    // case is provided separately in
3331    // `tests/async_executor_no_executor_test.rs`.
3332    // -----------------------------------------------------------------
3333
3334    /// Minimal test executor that drives a boxed future to completion via
3335    /// `futures::executor::block_on`. The output of the wrapped future is
3336    /// not used here because the typed `AsyncExecutorExt::block_on` reads
3337    /// the output from a shared slot.
3338    #[cfg(feature = "serde")]
3339    struct TestExecutor;
3340
3341    #[cfg(feature = "serde")]
3342    impl AsyncExecutor for TestExecutor {
3343        fn block_on_dyn(
3344            &self,
3345            future: core::pin::Pin<Box<dyn core::future::Future<Output = ()> + Send>>,
3346        ) -> Box<dyn core::any::Any + Send> {
3347            futures::executor::block_on(future);
3348            Box::new(())
3349        }
3350    }
3351
3352    /// Single-threaded executor used by the async-executor tests. We use
3353    /// `futures::executor::block_on` (no Tokio) so the suite can run
3354    /// without a Tokio runtime.
3355    #[cfg(feature = "serde")]
3356    #[test]
3357    #[serial_test::serial]
3358    fn test_async_executor_lifecycle() {
3359        // Step 1: in a fresh process the `OnceLock` is empty, so a
3360        // call to `block_on_async` returns the standard English
3361        // error. (We use a best-effort assertion: if a prior test in
3362        // the same binary has already set the executor, skip this
3363        // sub-assertion; the dedicated
3364        // `async_executor_no_executor_test.rs` integration test
3365        // exercises this path in a fresh process.)
3366        if current_async_executor().is_none() {
3367            let result: Result<(), ToolError> = block_on_async(async {});
3368            let err = result
3369                .expect_err("block_on_async should error when no AsyncExecutor is registered");
3370            assert_eq!(err.kind, ToolErrorKind::InternalError);
3371            let expected = block_on_async_error_message();
3372            assert!(
3373                err.message.contains(expected) || err.message.contains("no async runtime"),
3374                "expected English error message, got: {:?}",
3375                err.message
3376            );
3377        }
3378
3379        // Step 2: register a custom executor and verify
3380        // `current_async_executor` returns it.
3381        set_async_executor(Box::new(TestExecutor));
3382        let exec = current_async_executor().expect("executor should be registered");
3383        // Sanity check: it is a `&'static dyn AsyncExecutor`.
3384        let _static_ref: &'static dyn AsyncExecutor = exec;
3385
3386        // Step 3: drive a future with a `String` output through the
3387        // typed `AsyncExecutorExt::block_on` wrapper.
3388        let string_result: String = exec.block_on(async { String::from("hello, executor") });
3389        assert_eq!(string_result, "hello, executor");
3390
3391        // Step 4: drive a future with a non-`()` typed output
3392        // (`(i32, String)`) through the type-erased `block_on_dyn`
3393        // boundary and back. This is the
3394        // `test_async_executor_typed_output` scenario.
3395        let tuple_result: (i32, String) = exec.block_on(async { (42, String::from("typed")) });
3396        assert_eq!(tuple_result, (42, String::from("typed")));
3397    }
3398}