Skip to main content

fluers_core/
tool.rs

1//! Tool definitions, calls, and the `Tool` trait.
2//!
3//! Mirrors `AgentTool` / `AgentToolResult` from `pi-agent-core`.
4
5use std::collections::BTreeMap;
6
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use tokio_util::sync::CancellationToken;
11
12use crate::error::{CoreError, Result};
13
14/// A JSON value, aliased for ergonomic imports.
15pub type JsonValue = Value;
16
17/// JSON-Schema-ish parameter description for a tool.
18///
19/// Intentionally loose (a serde_json map) so adapters can carry any schema
20/// dialect a provider expects.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22pub struct ParameterSchema {
23    /// The schema fields.
24    #[serde(flatten)]
25    pub fields: BTreeMap<String, Value>,
26}
27
28/// Per-invocation context handed to a tool's [`Tool::execute`].
29pub struct InvokeContext {
30    /// The unique id of this invocation's tool call.
31    pub tool_call_id: String,
32    /// Cooperative + deadline cancellation. Tools should `select!` on
33    /// `cancel.cancelled()` for long-running work. Clonable and `'static`.
34    pub cancel: CancellationToken,
35}
36
37/// A tool call extracted from a model response.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ToolCall {
40    /// The tool's name.
41    pub name: String,
42    /// The arguments object the model produced.
43    pub input: Value,
44}
45
46/// A tool's result returned to the model.
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48pub struct ToolResult {
49    /// Human/model-readable content blocks describing the outcome.
50    pub content: Vec<Value>,
51    /// Optional structured details (not shown to the model verbatim).
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub details: Option<Value>,
54}
55
56/// A tool's static definition (name, schema, description).
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ToolDefinition {
59    /// Machine name, e.g. `write`.
60    pub name: String,
61    /// Human label, e.g. `Write File`.
62    pub label: String,
63    /// Description shown to the model.
64    pub description: String,
65    /// Input parameter schema.
66    pub parameters: ParameterSchema,
67}
68
69/// The trait every tool implements.
70///
71/// Flue's `AgentTool` is an object with a `parameters` schema and an async
72/// `execute`. The Rust equivalent is this trait, wrapped in `Arc<dyn Tool>`.
73///
74/// # Panic safety (precondition)
75///
76/// `run_agent` wraps `execute` in `catch_unwind` so a panicking tool is
77/// converted to a model-visible `Error:` result rather than aborting the run.
78/// For this to be sound, a tool's interior mutability must be **poison-free**
79/// (use `parking_lot` / atomics / `tokio::sync`, not `std::sync::Mutex`/
80/// `RwLock`, which poison on panic). A tool left mid-mutation by an unwinding
81/// panic must still be safely callable on subsequent turns. The built-in tools
82/// and `TaskTool` satisfy this.
83#[async_trait]
84pub trait Tool: Send + Sync {
85    /// The static definition (name/schema/description).
86    fn definition(&self) -> ToolDefinition;
87
88    /// Execute the tool with validated input.
89    async fn execute(&self, ctx: InvokeContext, input: Value) -> Result<ToolResult>;
90}
91
92/// Validate `input` against a tool's `parameters` schema.
93///
94/// MVP: only checks top-level required keys are present. Full JSON-Schema
95/// validation is layered in later (see `PORTING_PLAN.md`).
96pub fn validate_input(def: &ToolDefinition, input: &Value) -> Result<()> {
97    let Some(obj) = input.as_object() else {
98        return Err(CoreError::ToolInputValidation(format!(
99            "tool `{}` expects an object input",
100            def.name
101        )));
102    };
103    if let Some(required) = def
104        .parameters
105        .fields
106        .get("required")
107        .and_then(Value::as_array)
108    {
109        for req in required {
110            if let Some(key) = req.as_str() {
111                if !obj.contains_key(key) {
112                    return Err(CoreError::ToolInputValidation(format!(
113                        "tool `{}` missing required parameter `{key}`",
114                        def.name
115                    )));
116                }
117            }
118        }
119    }
120    Ok(())
121}