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}