Skip to main content

llm_tool/
types.rs

1//! Custom tool registration for the Antigravity SDK bridge.
2//!
3//! Defines Rust-side tool metadata and a registry that tracks tools by name.
4//! The actual Python wrapping (converting Rust async fns into Python callables)
5//! requires the Python runtime and is gated behind integration tests.
6
7use std::{
8    collections::HashMap,
9    sync::{Arc, Mutex},
10};
11
12use serde::{Deserialize, Serialize};
13
14/// Context passed to Rust tools during dispatch, mirroring the Python SDK's `ToolContext`.
15///
16/// Provides access to the current conversation ID, a shared key-value state
17/// store that persists across tool calls within the same agent turn, and an
18/// idle flag.
19///
20/// The state is backed by `Arc<Mutex<HashMap>>` so it can be cheaply cloned
21/// and shared across concurrent tool invocations.
22///
23/// # `get_state` vs `set_state` error handling
24///
25/// These two methods intentionally handle mutex poisoning differently:
26///
27/// - **[`get_state`](Self::get_state)** returns the caller-supplied `default`
28///   when the lock is poisoned. Reads are best-effort — a missing value is
29///   indistinguishable from a default, so returning `default` keeps the tool
30///   running without surfacing infrastructure errors to the model.
31///
32/// - **[`set_state`](Self::set_state)** returns `Err` when the lock is
33///   poisoned. Writes that silently vanish can cause subtle logic bugs, so
34///   callers must handle the failure explicitly.
35pub struct ToolContext {
36    conversation_id: Option<String>,
37    state: Arc<Mutex<HashMap<String, serde_json::Value>>>,
38    is_idle: bool,
39}
40
41impl ToolContext {
42    /// Create a new context with the given conversation ID.
43    #[must_use]
44    pub fn new(conversation_id: Option<String>) -> Self {
45        Self {
46            conversation_id,
47            state: Arc::new(Mutex::new(HashMap::new())),
48            is_idle: false,
49        }
50    }
51
52    /// Create a context that shares an externally-provided state map.
53    ///
54    /// Use this when multiple `ToolContext` instances (e.g. successive tool
55    /// calls within the same agent) must read/write the **same** state store.
56    #[must_use]
57    pub fn with_shared_state(
58        conversation_id: Option<String>,
59        state: Arc<Mutex<HashMap<String, serde_json::Value>>>,
60    ) -> Self {
61        Self {
62            conversation_id,
63            state,
64            is_idle: false,
65        }
66    }
67
68    /// Return the conversation ID, if one has been set.
69    #[must_use]
70    pub fn conversation_id(&self) -> Option<&str> {
71        self.conversation_id.as_deref()
72    }
73
74    /// Retrieve a value from the shared state, returning `default` if the key
75    /// is absent or the lock is poisoned.
76    ///
77    /// This method never fails — on a poisoned lock it logs a warning and
78    /// returns `default`. See the [struct-level docs](Self) for rationale.
79    #[must_use]
80    pub fn get_state(&self, key: &str, default: serde_json::Value) -> serde_json::Value {
81        match self.state.lock() {
82            Ok(guard) => guard.get(key).cloned().unwrap_or(default),
83            Err(e) => {
84                tracing::warn!(key, error = %e, "ToolContext::get_state: lock poisoned, returning default");
85                default
86            }
87        }
88    }
89
90    /// Insert or update a value in the shared state.
91    ///
92    /// Unlike [`get_state`](Self::get_state), this method returns `Err` on a
93    /// poisoned lock because silently dropping a write can cause subtle bugs.
94    /// See the [struct-level docs](Self) for rationale.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`ToolError`]
99    /// if the mutex is poisoned.
100    pub fn set_state(&self, key: &str, value: serde_json::Value) -> Result<(), ToolError> {
101        match self.state.lock() {
102            Ok(mut guard) => {
103                guard.insert(key.to_owned(), value);
104                Ok(())
105            }
106            Err(e) => {
107                let msg = format!("ToolContext::set_state: lock poisoned for key '{key}': {e}");
108                tracing::warn!("{msg}");
109                Err(ToolError::new(msg))
110            }
111        }
112    }
113
114    /// Whether the agent is currently idle.
115    #[must_use]
116    pub const fn is_idle(&self) -> bool {
117        self.is_idle
118    }
119}
120
121/// Re-export the `#[llm_tool]` proc macro for defining tools from plain functions.
122pub use llm_tool_macros::llm_tool;
123// Re-export `JsonSchema` derive so tool authors can write `use llm_tool::JsonSchema;`
124// without adding `schemars` to their own `Cargo.toml`.
125pub use schemars::JsonSchema;
126
127/// Human-readable JSON type name for error messages.
128fn other_type_name(value: &serde_json::Value) -> &'static str {
129    match value {
130        serde_json::Value::Null => "null",
131        serde_json::Value::Bool(_) => "bool",
132        serde_json::Value::Number(_) => "number",
133        serde_json::Value::String(_) => "string",
134        serde_json::Value::Array(_) => "array",
135        serde_json::Value::Object(_) => "object",
136    }
137}
138
139/// The return value of a Rust tool execution.
140///
141/// Every tool produces a `ToolOutput` containing:
142/// - **`content`**: the text sent back to the model.
143/// - **`metadata`**: an optional structured key-value map available to hooks,
144///   policies, and logging pipelines — but **never** sent to the model.
145///
146/// # Ergonomics
147///
148/// `ToolOutput` implements `From<String>`, `From<&str>`, and `Display`, so
149/// simple tools can return plain strings without ceremony:
150///
151/// ```rust
152/// use llm_tool::ToolOutput;
153/// use serde::Serialize;
154///
155/// // From a String
156/// let out: ToolOutput = "hello".to_string().into();
157/// assert_eq!(out.content(), "hello");
158/// assert!(out.metadata().is_empty());
159///
160/// // From &str
161/// let out: ToolOutput = "world".into();
162/// assert_eq!(out.content(), "world");
163///
164/// // Structured metadata from a typed struct (preferred)
165/// #[derive(Serialize)]
166/// struct ReadMeta { bytes_read: usize, cached: bool }
167///
168/// let out = ToolOutput::new("file contents…")
169///     .with_metadata(&ReadMeta { bytes_read: 1024, cached: true })
170///     .unwrap();
171/// assert_eq!(out.metadata()["bytes_read"], 1024);
172/// assert_eq!(out.metadata()["cached"], true);
173///
174/// // Single ad-hoc entry
175/// let out = ToolOutput::new("done")
176///     .with_meta("exit_code", serde_json::json!(0));
177/// assert_eq!(out.metadata()["exit_code"], 0);
178/// ```
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ToolOutput {
181    /// The text content returned to the model.
182    content: String,
183    /// Structured metadata for hooks / policies / logging.
184    /// NOT sent to the model.
185    metadata: std::collections::HashMap<String, serde_json::Value>,
186}
187
188impl ToolOutput {
189    /// Create a new `ToolOutput` with the given content and no metadata.
190    pub fn new(content: impl Into<String>) -> Self {
191        Self {
192            content: content.into(),
193            metadata: std::collections::HashMap::new(),
194        }
195    }
196
197    /// Serialize a value to JSON and wrap it as tool output.
198    ///
199    /// The JSON string becomes the content sent to the model, but no
200    /// metadata is attached. For the zero-redundancy path that populates
201    /// **both** content and metadata from the same struct, use
202    /// [`from_metadata`](Self::from_metadata).
203    ///
204    /// ```rust
205    /// use llm_tool::{ToolOutput, ToolError};
206    ///
207    /// let data = serde_json::json!({"temp": 72, "unit": "F"});
208    /// let output = ToolOutput::json(&data).unwrap();
209    /// assert!(output.content().contains("72"));
210    /// assert!(output.metadata().is_empty()); // no metadata attached
211    /// ```
212    ///
213    /// # Errors
214    ///
215    /// Returns `Err(ToolError)` if serialization fails.
216    pub fn json<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
217        serde_json::to_string(value)
218            .map(Self::new)
219            .map_err(|e| ToolError::new(format!("serialization failed: {e}")))
220    }
221
222    /// Create a `ToolOutput` where **both** the content and metadata come
223    /// from the same serializable value.
224    ///
225    /// - **Content** (sent to the model): the JSON representation of `value`.
226    /// - **Metadata** (hooks / policies / logging): the flattened object fields.
227    ///
228    /// This is the zero-redundancy path: define one struct, derive
229    /// `Serialize`, and everything is populated automatically.
230    ///
231    /// # Errors
232    ///
233    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object.
234    ///
235    /// # Example
236    ///
237    /// ```rust
238    /// use llm_tool::ToolOutput;
239    /// use serde::Serialize;
240    ///
241    /// #[derive(Serialize)]
242    /// struct Weather { location: String, temp_f: i32, condition: String }
243    ///
244    /// let out = ToolOutput::from_metadata(&Weather {
245    ///     location: "Seattle".into(),
246    ///     temp_f: 72,
247    ///     condition: "Sunny".into(),
248    /// }).unwrap();
249    ///
250    /// // Model sees the JSON string
251    /// assert!(out.content().contains("Seattle"));
252    /// assert!(out.content().contains("72"));
253    ///
254    /// // Hooks see typed fields
255    /// assert_eq!(out.metadata()["location"], "Seattle");
256    /// assert_eq!(out.metadata()["temp_f"], 72);
257    /// ```
258    pub fn from_metadata<T: serde::Serialize>(value: &T) -> Result<Self, ToolError> {
259        let json_value = serde_json::to_value(value)
260            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
261        match json_value {
262            serde_json::Value::Object(map) => {
263                // serde_json::Value::to_string() never fails.
264                let content = serde_json::Value::Object(map.clone()).to_string();
265                Ok(Self {
266                    content,
267                    metadata: map.into_iter().collect(),
268                })
269            }
270            other => Err(ToolError::new(format!(
271                "metadata must serialize to a JSON object, got {}",
272                other_type_name(&other),
273            ))),
274        }
275    }
276
277    /// Attach a single metadata key-value pair. Chainable.
278    ///
279    /// For attaching multiple fields at once, prefer
280    /// [`with_metadata`](Self::with_metadata) with a typed struct.
281    #[must_use]
282    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
283        self.metadata.insert(key.into(), value);
284        self
285    }
286
287    /// Attach structured metadata from a serializable value.
288    ///
289    /// The value is serialized to a JSON object and its fields are **merged**
290    /// into the metadata map. This is the preferred way to attach metadata
291    /// because it avoids stringly-typed keys and data duplication.
292    ///
293    /// # Errors
294    ///
295    /// Returns `Err(ToolError)` if `value` doesn't serialize to a JSON object
296    /// (e.g. it serializes to a scalar or array).
297    ///
298    /// # Example
299    ///
300    /// ```rust
301    /// use llm_tool::ToolOutput;
302    /// use serde::Serialize;
303    ///
304    /// #[derive(Serialize)]
305    /// struct FileMeta { bytes_read: usize, source: String }
306    ///
307    /// let out = ToolOutput::new("file contents")
308    ///     .with_metadata(&FileMeta { bytes_read: 1024, source: "/etc/hosts".into() })
309    ///     .unwrap();
310    /// assert_eq!(out.metadata()["bytes_read"], 1024);
311    /// assert_eq!(out.metadata()["source"], "/etc/hosts");
312    /// ```
313    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, ToolError> {
314        let json = serde_json::to_value(value)
315            .map_err(|e| ToolError::new(format!("metadata serialization failed: {e}")))?;
316        match json {
317            serde_json::Value::Object(map) => {
318                self.metadata.extend(map);
319                Ok(self)
320            }
321            other => Err(ToolError::new(format!(
322                "metadata must serialize to a JSON object, got {}",
323                other_type_name(&other),
324            ))),
325        }
326    }
327
328    /// The text content sent back to the model.
329    #[must_use]
330    pub fn content(&self) -> &str {
331        &self.content
332    }
333
334    /// Consume self and return the owned content string.
335    #[must_use]
336    pub fn into_content(self) -> String {
337        self.content
338    }
339
340    /// The structured metadata map.
341    #[must_use]
342    pub fn metadata(&self) -> &std::collections::HashMap<String, serde_json::Value> {
343        &self.metadata
344    }
345}
346
347impl std::fmt::Display for ToolOutput {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        f.write_str(&self.content)
350    }
351}
352
353impl From<String> for ToolOutput {
354    fn from(content: String) -> Self {
355        Self::new(content)
356    }
357}
358
359impl From<&str> for ToolOutput {
360    fn from(content: &str) -> Self {
361        Self::new(content)
362    }
363}
364
365impl From<i64> for ToolOutput {
366    fn from(value: i64) -> Self {
367        Self::new(value.to_string())
368    }
369}
370
371impl From<f64> for ToolOutput {
372    fn from(value: f64) -> Self {
373        Self::new(value.to_string())
374    }
375}
376
377impl From<bool> for ToolOutput {
378    fn from(value: bool) -> Self {
379        Self::new(value.to_string())
380    }
381}
382
383impl From<serde_json::Value> for ToolOutput {
384    fn from(value: serde_json::Value) -> Self {
385        // serde_json::Value::to_string() never fails.
386        Self::new(value.to_string())
387    }
388}
389
390/// Wrapper for returning serializable values as JSON tool output.
391///
392/// Implements `From<Json<T>> for ToolOutput` so it works with the
393/// `#[llm_tool]` macro's `.into()` conversion — no `Result` wrapper needed
394/// for infallible serialization.
395///
396/// # Panics
397///
398/// The `From` conversion panics if `serde_json::to_string` fails.
399/// This only happens with broken `Serialize` implementations (e.g.,
400/// maps with non-string keys). For explicit error handling, use
401/// [`ToolOutput::json()`] instead.
402///
403/// # Example
404///
405/// ```rust
406/// use llm_tool::{Json, ToolOutput};
407/// use serde::Serialize;
408///
409/// #[derive(Serialize)]
410/// struct Weather { temp: f64, city: String }
411///
412/// let output: ToolOutput = Json(Weather { temp: 72.0, city: "NYC".into() }).into();
413/// assert!(output.content().contains("72"));
414/// ```
415pub struct Json<T>(pub T);
416
417impl<T: serde::Serialize> From<Json<T>> for ToolOutput {
418    fn from(json: Json<T>) -> Self {
419        Self::new(
420            serde_json::to_string(&json.0)
421                .expect("Json<T> serialization failed — this is a bug in the Serialize impl"),
422        )
423    }
424}
425
426/// An error returned from a tool execution.
427/// The error message is sent back to the model as the tool's error response.
428/// Structured metadata can be attached for hooks and logging — it is **not**
429/// sent to the model.
430///
431/// Implements `From<String>` and `From<&str>` for ergonomic construction.
432///
433/// # Example
434///
435/// ```
436/// use llm_tool::ToolError;
437/// use serde::Serialize;
438///
439/// let err: ToolError = "something went wrong".into();
440/// assert_eq!(err.to_string(), "something went wrong");
441///
442/// let err = ToolError::new(format!("failed to read {}", "file.txt"));
443/// assert!(err.to_string().contains("file.txt"));
444///
445/// // Structured metadata from a typed struct (preferred)
446/// #[derive(Serialize)]
447/// struct HttpErrorMeta { status_code: u16, url: String }
448///
449/// let err = ToolError::new("HTTP request failed")
450///     .with_metadata(&HttpErrorMeta { status_code: 503, url: "https://example.com".into() })
451///     .unwrap();
452/// assert_eq!(err.metadata()["status_code"], 503);
453///
454/// // Single ad-hoc entry
455/// let err = ToolError::new("timeout")
456///     .with_meta("retry_after_secs", serde_json::json!(30));
457/// assert_eq!(err.metadata()["retry_after_secs"], 30);
458/// ```
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct ToolError {
461    /// Human-readable error message sent to the model.
462    pub message: String,
463    /// Structured metadata for hooks / policies / logging.
464    /// NOT sent to the model.
465    metadata: std::collections::HashMap<String, serde_json::Value>,
466}
467
468impl ToolError {
469    /// Create a new tool error with no metadata.
470    pub fn new(message: impl Into<String>) -> Self {
471        Self {
472            message: message.into(),
473            metadata: std::collections::HashMap::new(),
474        }
475    }
476
477    /// Attach a single metadata key-value pair. Chainable.
478    ///
479    /// For attaching multiple fields at once, prefer
480    /// [`with_metadata`](Self::with_metadata) with a typed struct.
481    #[must_use]
482    pub fn with_meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
483        self.metadata.insert(key.into(), value);
484        self
485    }
486
487    /// Attach structured metadata from a serializable value.
488    ///
489    /// The value is serialized to a JSON object and its fields are **merged**
490    /// into the metadata map. See [`ToolOutput::with_metadata`] for details.
491    ///
492    /// # Errors
493    ///
494    /// Returns `Err(self)` if `value` doesn't serialize to a JSON object.
495    pub fn with_metadata<T: serde::Serialize>(mut self, value: &T) -> Result<Self, Self> {
496        let json = serde_json::to_value(value).map_err(|e| {
497            Self::new(format!(
498                "{} (metadata serialization also failed: {e})",
499                self.message
500            ))
501        })?;
502        match json {
503            serde_json::Value::Object(map) => {
504                self.metadata.extend(map);
505                Ok(self)
506            }
507            _ => {
508                // Don't lose the original error — return self unchanged.
509                Ok(self)
510            }
511        }
512    }
513
514    /// The structured metadata map.
515    #[must_use]
516    pub fn metadata(&self) -> &std::collections::HashMap<String, serde_json::Value> {
517        &self.metadata
518    }
519}
520
521impl std::fmt::Display for ToolError {
522    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523        write!(f, "{}", self.message)
524    }
525}
526
527impl std::error::Error for ToolError {}
528
529impl From<String> for ToolError {
530    fn from(message: String) -> Self {
531        Self::new(message)
532    }
533}
534
535impl From<&str> for ToolError {
536    fn from(message: &str) -> Self {
537        Self::new(message)
538    }
539}
540
541impl From<std::io::Error> for ToolError {
542    fn from(e: std::io::Error) -> Self {
543        Self::new(e.to_string())
544            .with_meta("error_kind", serde_json::json!(format!("{:?}", e.kind())))
545    }
546}
547
548impl From<serde_json::Error> for ToolError {
549    fn from(e: serde_json::Error) -> Self {
550        Self::new(e.to_string())
551            .with_meta("category", serde_json::json!(format!("{:?}", e.classify())))
552    }
553}
554
555impl From<Box<dyn std::error::Error + Send + Sync>> for ToolError {
556    fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
557        Self::new(e.to_string())
558    }
559}
560
561impl From<std::convert::Infallible> for ToolError {
562    fn from(never: std::convert::Infallible) -> Self {
563        match never {}
564    }
565}
566
567/// Serialize a tool's return value to a JSON string.
568///
569/// **Deprecated**: Use [`ToolOutput::json()`] instead.
570#[doc(hidden)]
571#[deprecated(since = "0.2.0", note = "Use ToolOutput::json() instead")]
572pub fn __serialize_tool_result<T: serde::Serialize>(value: &T) -> Result<ToolOutput, ToolError> {
573    ToolOutput::json(value)
574}
575
576/// Compile-time dispatch for converting tool return values into [`ToolOutput`].
577///
578/// Uses the "autoref specialization" pattern: the compiler checks inherent
579/// methods on `Wrap<T>` first (for `String`, `ToolOutput`, `Json<T>`),
580/// then falls back to the `SerializeFallback` trait blanket impl for
581/// `T: Serialize`. This eliminates all proc-macro type-name matching.
582///
583/// **Not public API** — used only by the `#[llm_tool]` proc macro.
584#[doc(hidden)]
585pub mod __private {
586    use super::{Json, ToolError, ToolOutput};
587
588    /// Wrapper enabling compile-time method dispatch for tool output conversion.
589    pub struct Wrap<T>(pub T);
590
591    // ── Inherent methods (highest priority in method resolution) ──
592
593    impl Wrap<ToolOutput> {
594        /// `ToolOutput` → identity pass-through.
595        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
596            Ok(self.0)
597        }
598    }
599
600    impl Wrap<String> {
601        /// `String` → wrap as plain text (no JSON encoding).
602        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
603            Ok(ToolOutput::new(self.0))
604        }
605    }
606
607    impl<T: serde::Serialize> Wrap<Json<T>> {
608        /// `Json<T>` → serialize to JSON string.
609        pub fn __convert(self) -> Result<ToolOutput, ToolError> {
610            Ok((self.0).into())
611        }
612    }
613
614    // ── Trait fallback (lower priority in method resolution) ──
615
616    /// Fallback conversion for any `T: Serialize` not covered by inherent methods.
617    ///
618    /// The compiler checks inherent methods first, so `String` and `ToolOutput`
619    /// use their inherent impls. Everything else falls through to this trait,
620    /// which serializes the value to JSON.
621    pub trait SerializeFallback {
622        /// Serialize `self` to JSON and wrap as [`ToolOutput`].
623        fn __convert(self) -> Result<ToolOutput, ToolError>;
624    }
625
626    impl<T: serde::Serialize> SerializeFallback for Wrap<T> {
627        fn __convert(self) -> Result<ToolOutput, ToolError> {
628            ToolOutput::json(&self.0)
629        }
630    }
631}
632
633/// Describes a custom tool that can be registered with an agent.
634///
635/// This struct holds the metadata the SDK needs to expose the tool to the
636/// model. The actual handler function is registered separately via
637/// [`ToolRegistry::register`](super::ToolRegistry::register).
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct ToolDefinition {
640    /// Unique tool name (e.g. `"flash_device"`).
641    pub name: String,
642    /// Human-readable description shown to the model.
643    pub description: String,
644    /// JSON Schema describing the tool's parameters.
645    pub parameter_schema: serde_json::Value,
646}