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}