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}