Skip to main content

cortex_sdk/
tool.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{Attachment, ToolCapabilities, ToolRuntime};
4
5/// A tool that the LLM can invoke during conversation.
6///
7/// Tools are the primary extension point for Cortex plugins.  Each tool
8/// has a name, description, JSON Schema for input parameters, and an
9/// execute function.  The runtime presents the tool definition to the LLM
10/// and routes invocations to [`Tool::execute`].
11///
12/// # Thread Safety
13///
14/// Tools must be `Send + Sync` because a single tool instance is shared
15/// across all turns in the daemon process.  Use interior mutability
16/// (`Mutex`, `RwLock`, `AtomicXxx`) if you need mutable state.
17pub trait Tool: Send + Sync {
18    /// Unique tool name (lowercase, underscores, e.g. `"web_search"`).
19    ///
20    /// Must be unique across all registered tools.  If two tools share a
21    /// name, the later registration wins.
22    fn name(&self) -> &'static str;
23
24    /// Human-readable description shown to the LLM.
25    ///
26    /// Write this for the LLM, not for humans.  Include:
27    /// - What the tool does
28    /// - When to use it
29    /// - When *not* to use it
30    /// - Any constraints or limitations
31    fn description(&self) -> &'static str;
32
33    /// JSON Schema describing the tool's input parameters.
34    ///
35    /// The LLM generates a JSON object matching this schema.  Example:
36    ///
37    /// ```json
38    /// {
39    ///   "type": "object",
40    ///   "properties": {
41    ///     "query": { "type": "string", "description": "Search query" }
42    ///   },
43    ///   "required": ["query"]
44    /// }
45    /// ```
46    fn input_schema(&self) -> serde_json::Value;
47
48    /// Execute the tool with the given input.
49    ///
50    /// `input` is a JSON object matching [`Self::input_schema`].  The
51    /// runtime validates the schema before calling this method, but
52    /// individual field types should still be checked defensively.
53    ///
54    /// # Return Values
55    ///
56    /// - [`ToolResult::success`] — normal output returned to the LLM
57    /// - [`ToolResult::error`] — the tool ran but produced an error the
58    ///   LLM should see and potentially recover from
59    ///
60    /// # Errors
61    ///
62    /// Return [`ToolError::InvalidInput`] for malformed parameters or
63    /// [`ToolError::ExecutionFailed`] for unrecoverable failures.  These
64    /// are surfaced as error events in the turn journal.
65    fn execute(&self, input: serde_json::Value) -> Result<ToolResult, ToolError>;
66
67    /// Execute the tool with runtime context and host callbacks.
68    ///
69    /// Plugins can override this to read session/actor/source metadata and
70    /// emit progress or observer updates through the provided runtime bridge.
71    ///
72    /// The default implementation preserves the classic SDK contract and calls
73    /// [`Self::execute`].
74    ///
75    /// # Errors
76    ///
77    /// Returns the same `ToolError` variants that [`Self::execute`] would
78    /// return for invalid input or unrecoverable execution failure.
79    fn execute_with_runtime(
80        &self,
81        input: serde_json::Value,
82        runtime: &dyn ToolRuntime,
83    ) -> Result<ToolResult, ToolError> {
84        let _ = runtime;
85        self.execute(input)
86    }
87
88    /// Optional per-tool execution timeout in seconds.
89    ///
90    /// If `None` (the default), the global `[turn].tool_timeout_secs`
91    /// from the instance configuration applies.
92    fn timeout_secs(&self) -> Option<u64> {
93        None
94    }
95
96    /// Stable capability hints consumed by the runtime and observability
97    /// layers.
98    fn capabilities(&self) -> ToolCapabilities {
99        ToolCapabilities::default()
100    }
101}
102
103/// Result of a tool execution returned to the LLM.
104///
105/// Use [`ToolResult::success`] for normal output and [`ToolResult::error`]
106/// for recoverable errors the LLM should see.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ToolResult {
109    /// Output text returned to the LLM.
110    pub output: String,
111    /// Structured media attachments produced by this tool.
112    ///
113    /// Attachments are delivered by Cortex transports independently from the
114    /// text the model sees, so tools do not need transport-specific protocols.
115    pub media: Vec<Attachment>,
116    /// Whether this result represents an error condition.
117    ///
118    /// When `true`, the LLM sees this as a failed tool call and may retry
119    /// with different parameters or switch strategy.
120    pub is_error: bool,
121}
122
123impl ToolResult {
124    /// Create a successful result.
125    #[must_use]
126    pub fn success(output: impl Into<String>) -> Self {
127        Self {
128            output: output.into(),
129            media: Vec::new(),
130            is_error: false,
131        }
132    }
133
134    /// Create an error result (tool ran but failed).
135    ///
136    /// Use this for recoverable errors; the LLM sees the output and can
137    /// decide how to proceed. For example: "file not found", "permission
138    /// denied", "rate limit exceeded".
139    #[must_use]
140    pub fn error(output: impl Into<String>) -> Self {
141        Self {
142            output: output.into(),
143            media: Vec::new(),
144            is_error: true,
145        }
146    }
147
148    /// Attach one media item to the result.
149    #[must_use]
150    pub fn with_media(mut self, attachment: Attachment) -> Self {
151        self.media.push(attachment);
152        self
153    }
154
155    /// Attach multiple media items to the result.
156    #[must_use]
157    pub fn with_media_many(mut self, media: impl IntoIterator<Item = Attachment>) -> Self {
158        self.media.extend(media);
159        self
160    }
161}
162
163/// Error from tool execution.
164///
165/// Unlike [`ToolResult::error`] (which is a "soft" error the LLM sees),
166/// `ToolError` represents a hard failure that is logged in the turn
167/// journal as a tool invocation error.
168#[derive(Debug)]
169pub enum ToolError {
170    /// Input parameters are invalid or missing required fields.
171    InvalidInput(String),
172    /// Execution failed due to an external or internal error.
173    ExecutionFailed(String),
174}
175
176impl std::fmt::Display for ToolError {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        match self {
179            Self::InvalidInput(e) => write!(f, "invalid input: {e}"),
180            Self::ExecutionFailed(e) => write!(f, "execution failed: {e}"),
181        }
182    }
183}
184
185impl std::error::Error for ToolError {}
186
187/// Plugin metadata returned to the runtime at load time.
188///
189/// The `name` field must match the plugin's directory name and the
190/// `name` field in `manifest.toml`.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct PluginInfo {
193    /// Unique plugin identifier (e.g. `"my-plugin"`).
194    pub name: String,
195    /// Plugin semantic version string (e.g. `"0.1.0"`).
196    pub version: String,
197    /// Human-readable one-line description.
198    pub description: String,
199}
200
201/// A plugin that provides multiple tools from a single shared library.
202///
203/// This is the primary interface between a plugin and the Cortex runtime.
204/// Implement this trait and use `export_plugin!` to generate the FFI
205/// entry point.
206///
207/// # Requirements
208///
209/// - The implementing type must also implement `Default` (required by
210///   `export_plugin!` for construction via FFI).
211/// - The type must be `Send + Sync` because the runtime may access it
212///   from multiple threads.
213///
214/// # Example
215///
216/// ```text
217/// use cortex_sdk::prelude::*;
218///
219/// #[derive(Default)]
220/// struct MyPlugin;
221///
222/// impl MultiToolPlugin for MyPlugin {
223///     fn plugin_info(&self) -> PluginInfo {
224///         PluginInfo {
225///             name: "my-plugin".into(),
226///             version: "0.1.0".into(),
227///             description: "Example plugin".into(),
228///         }
229///     }
230///
231///     fn create_tools(&self) -> Vec<Box<dyn Tool>> {
232///         vec![]
233///     }
234/// }
235///
236/// cortex_sdk::export_plugin!(MyPlugin);
237/// ```
238pub trait MultiToolPlugin: Send + Sync {
239    /// Return plugin metadata.
240    fn plugin_info(&self) -> PluginInfo;
241
242    /// Create all tools this plugin provides.
243    ///
244    /// Called once at daemon startup.  Returned tools live for the
245    /// daemon's lifetime.  Each tool is registered by name into the
246    /// global tool registry.
247    fn create_tools(&self) -> Vec<Box<dyn Tool>>;
248}