Skip to main content

molo_core/tool/
mod.rs

1//! Tool: external capabilities an agent can invoke.
2//!
3//! This file is where Tool is defined: the [`Tool`] trait defines the
4//! interface agents use to execute tools, [`ToolSchema`] describes the
5//! definition exposed to the model, [`ToolError`] describes why a call
6//! fails; it contains no concrete tools — those are implemented by agent
7//! applications (see `examples/tool_agent.rs`).
8//!
9//! Companion component:
10//! - [`SharedState`] — a container for cross-tool shared state, injected
11//!   via [`ToolContext`] on [`Tool::call`].
12
13pub use shared_state::SharedState;
14
15use crate::effect::{DisplayOutput, EffectRequest, RiskLevel};
16use crate::run::{Artifact, RunContext, RunMetadata};
17use serde::{Deserialize, Serialize};
18use std::fmt;
19use std::time::Duration;
20
21/// Namespace assigned to a tool by the host application or extension layer.
22///
23/// The provider-facing tool name is still a single unique string. The
24/// namespace is host-facing metadata used for extension unload, policy,
25/// audit, and debugging.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct ToolNamespace {
28    /// Namespace kind.
29    pub kind: ToolNamespaceKind,
30    /// Stable host-assigned namespace id.
31    pub id: String,
32}
33
34impl ToolNamespace {
35    /// Constructs a namespace from a kind and stable id.
36    pub fn new(kind: ToolNamespaceKind, id: impl Into<String>) -> Self {
37        Self {
38            kind,
39            id: id.into(),
40        }
41    }
42
43    /// Namespace for local application tools registered without extension
44    /// source metadata.
45    pub fn local() -> Self {
46        Self::new(ToolNamespaceKind::Local, "local")
47    }
48
49    /// Namespace for tools discovered from one MCP server.
50    pub fn mcp_server(id: impl Into<String>) -> Self {
51        Self::new(ToolNamespaceKind::McpServer, id)
52    }
53
54    /// Namespace for tools exposed by one skill layer.
55    pub fn skill_layer(id: impl Into<String>) -> Self {
56        Self::new(ToolNamespaceKind::SkillLayer, id)
57    }
58}
59
60impl fmt::Display for ToolNamespace {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{:?}:{}", self.kind, self.id)
63    }
64}
65
66/// Kind of tool namespace.
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[non_exhaustive]
69pub enum ToolNamespaceKind {
70    /// Local application-owned tools.
71    Local,
72    /// Tools discovered from an MCP server.
73    McpServer,
74    /// Tools exposed by an Agent Skills layer.
75    SkillLayer,
76    /// Tools exposed by a sub-agent.
77    SubAgent,
78    /// Application-specific namespace kind.
79    Custom(String),
80}
81
82/// Trust level assigned to a tool source.
83///
84/// This value is a policy input only. It does not grant permission and must
85/// not be used to bypass harness governance.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[non_exhaustive]
88pub enum ToolTrustLevel {
89    /// Host-owned trusted code.
90    Trusted,
91    /// Project-local source selected by the host.
92    Project,
93    /// User-installed extension source.
94    UserInstalled,
95    /// External process or service.
96    External,
97    /// Untrusted source.
98    Untrusted,
99}
100
101/// Host-facing metadata describing where a provider-visible tool came from.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct ToolSource {
104    /// Source namespace.
105    pub namespace: ToolNamespace,
106    /// Raw source name before provider-facing disambiguation.
107    pub raw_name: String,
108    /// Provider-facing display name registered in `ToolRegistry`.
109    pub display_name: String,
110    /// Source trust level.
111    pub trust: ToolTrustLevel,
112    /// Host/application metadata.
113    pub metadata: RunMetadata,
114}
115
116impl ToolSource {
117    /// Constructs source metadata with external trust by default.
118    pub fn new(
119        namespace: ToolNamespace,
120        raw_name: impl Into<String>,
121        display_name: impl Into<String>,
122    ) -> Self {
123        Self {
124            namespace,
125            raw_name: raw_name.into(),
126            display_name: display_name.into(),
127            trust: ToolTrustLevel::External,
128            metadata: RunMetadata::new(),
129        }
130    }
131
132    /// Constructs source metadata for a local application tool.
133    pub fn local(name: impl Into<String>) -> Self {
134        let name = name.into();
135        Self {
136            namespace: ToolNamespace::local(),
137            raw_name: name.clone(),
138            display_name: name,
139            trust: ToolTrustLevel::Trusted,
140            metadata: RunMetadata::new(),
141        }
142    }
143
144    /// Sets the trust level.
145    pub fn with_trust(mut self, trust: ToolTrustLevel) -> Self {
146        self.trust = trust;
147        self
148    }
149
150    /// Sets source metadata.
151    pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
152        self.metadata = metadata;
153        self
154    }
155}
156
157/// The definition of a tool.
158///
159/// The model decides whether to call the tool and how to generate arguments
160/// from `name` / `description` / `parameters`; Provider implementations map
161/// those three fields to the vendor's wire format. [`ToolPolicy`] and
162/// [`ToolSchema::metadata`] are framework-facing metadata and are not sent to
163/// providers unless a provider adapter explicitly supports such annotations.
164///
165/// # Example
166///
167/// ```
168/// # extern crate molo_core as molo;
169/// use molo::tool::ToolSchema;
170/// use serde_json::json;
171///
172/// let schema = ToolSchema::new(
173///     "get_weather",
174///     "Get the weather for a given city",
175///     json!({
176///         "type": "object",
177///         "properties": {
178///             "city": { "type": "string", "description": "City name" }
179///         },
180///         "required": ["city"]
181///     }),
182/// );
183///
184/// assert_eq!(schema.name, "get_weather");
185/// ```
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct ToolSchema {
188    /// Tool name (the basis on which the model selects a tool).
189    pub name: String,
190    /// Tool description (the basis on which the model understands the
191    /// tool's purpose).
192    pub description: String,
193    /// JSON Schema for the arguments, preferably generated from a serde
194    /// struct with `schemars::schema_for!`.
195    pub parameters: serde_json::Value,
196    /// Framework-facing policy declaration.
197    pub policy: ToolPolicy,
198    /// Framework/application metadata.
199    pub metadata: RunMetadata,
200}
201
202impl ToolSchema {
203    /// Constructs a tool schema with default policy and no metadata.
204    pub fn new(
205        name: impl Into<String>,
206        description: impl Into<String>,
207        parameters: serde_json::Value,
208    ) -> Self {
209        Self {
210            name: name.into(),
211            description: description.into(),
212            parameters,
213            policy: ToolPolicy::default(),
214            metadata: RunMetadata::new(),
215        }
216    }
217
218    /// Sets framework-facing policy metadata.
219    pub fn with_policy(mut self, policy: ToolPolicy) -> Self {
220        self.policy = policy;
221        self
222    }
223
224    /// Sets framework/application metadata.
225    pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
226        self.metadata = metadata;
227        self
228    }
229}
230
231/// Tool policy metadata declared by the tool author.
232///
233/// This is an input to registry events and harness policy; it is not an
234/// authorization decision.
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct ToolPolicy {
237    /// Declared side-effect level.
238    pub side_effects: SideEffectLevel,
239    /// Default risk declaration.
240    pub risk: RiskLevel,
241    /// Whether the tool author recommends confirmation before execution.
242    pub requires_confirmation: bool,
243    /// Suggested timeout for tool/effect execution.
244    pub timeout: Option<Duration>,
245    /// Default memory policy for this tool's model-visible output.
246    pub memory_policy: ToolMemoryPolicy,
247}
248
249impl Default for ToolPolicy {
250    fn default() -> Self {
251        Self {
252            side_effects: SideEffectLevel::Pure,
253            risk: RiskLevel::Low,
254            requires_confirmation: false,
255            timeout: None,
256            memory_policy: ToolMemoryPolicy::Normal,
257        }
258    }
259}
260
261/// Declared side-effect level for a tool.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[non_exhaustive]
264pub enum SideEffectLevel {
265    /// Pure computation.
266    Pure,
267    /// Reads host/application state but does not write it.
268    ReadOnly,
269    /// Writes host/application state.
270    Write,
271    /// Interacts with an external system.
272    External,
273}
274
275/// Memory handling policy for model-visible tool/effect output.
276#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
277#[non_exhaustive]
278pub enum ToolMemoryPolicy {
279    /// Record normally.
280    #[default]
281    Normal,
282    /// Record as protected memory when supported by the memory implementation.
283    Protected,
284}
285
286impl ToolMemoryPolicy {
287    /// Whether the output should be recorded as protected memory.
288    pub fn is_protected(self) -> bool {
289        matches!(self, Self::Protected)
290    }
291}
292
293/// Model-visible output produced by a tool.
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct ToolOutput {
296    /// Text visible to the model through [`Message::ToolResult`](crate::Message::ToolResult).
297    pub content: String,
298    /// Optional host/UI display output.
299    pub display: Option<DisplayOutput>,
300    /// Artifact handles produced by the tool.
301    pub artifacts: Vec<Artifact>,
302    /// Memory policy for the model-visible content.
303    pub memory_policy: ToolMemoryPolicy,
304    /// Framework/application metadata.
305    pub metadata: RunMetadata,
306}
307
308impl ToolOutput {
309    /// Constructs plain text model-visible output.
310    pub fn text(content: impl Into<String>) -> Self {
311        Self {
312            content: content.into(),
313            display: None,
314            artifacts: Vec::new(),
315            memory_policy: ToolMemoryPolicy::Normal,
316            metadata: RunMetadata::new(),
317        }
318    }
319
320    /// Sets host/UI display output.
321    pub fn with_display(mut self, display: DisplayOutput) -> Self {
322        self.display = Some(display);
323        self
324    }
325
326    /// Sets artifact handles.
327    pub fn with_artifacts(mut self, artifacts: Vec<Artifact>) -> Self {
328        self.artifacts = artifacts;
329        self
330    }
331
332    /// Sets memory policy.
333    pub fn with_memory_policy(mut self, policy: ToolMemoryPolicy) -> Self {
334        self.memory_policy = policy;
335        self
336    }
337
338    /// Sets framework/application metadata.
339    pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
340        self.metadata = metadata;
341        self
342    }
343}
344
345impl From<String> for ToolOutput {
346    fn from(content: String) -> Self {
347        Self::text(content)
348    }
349}
350
351impl From<&str> for ToolOutput {
352    fn from(content: &str) -> Self {
353        Self::text(content)
354    }
355}
356
357/// Result of a tool call.
358///
359/// Pure or low-risk work can return [`ToolResult::Output`]. Side-effecting
360/// tools should return [`ToolResult::Effect`], allowing an outer harness to
361/// govern and execute the requested side effect.
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363#[non_exhaustive]
364pub enum ToolResult {
365    /// Immediate model-visible output.
366    Output(ToolOutput),
367    /// Side-effect request for an outer harness.
368    Effect(EffectRequest),
369}
370
371impl ToolResult {
372    /// Returns model-visible text for immediate output results.
373    ///
374    /// Effect results return `None` because the side effect has not executed.
375    pub fn output_content(&self) -> Option<&str> {
376        match self {
377            Self::Output(output) => Some(&output.content),
378            Self::Effect(_) => None,
379        }
380    }
381
382    /// Returns model-visible text, or an empty string for effect requests
383    /// that have not executed yet.
384    pub fn content_or_empty(&self) -> &str {
385        self.output_content().unwrap_or("")
386    }
387}
388
389impl std::fmt::Display for ToolResult {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        match self {
392            Self::Output(output) => f.write_str(&output.content),
393            Self::Effect(request) => {
394                write!(
395                    f,
396                    "effect request: {} ({})",
397                    request.description, request.id
398                )
399            }
400        }
401    }
402}
403
404impl From<ToolOutput> for ToolResult {
405    fn from(output: ToolOutput) -> Self {
406        Self::Output(output)
407    }
408}
409
410impl From<String> for ToolResult {
411    fn from(content: String) -> Self {
412        Self::Output(ToolOutput::text(content))
413    }
414}
415
416impl From<&str> for ToolResult {
417    fn from(content: &str) -> Self {
418        Self::Output(ToolOutput::text(content))
419    }
420}
421
422impl PartialEq<str> for ToolResult {
423    fn eq(&self, other: &str) -> bool {
424        self.output_content() == Some(other)
425    }
426}
427
428impl PartialEq<&str> for ToolResult {
429    fn eq(&self, other: &&str) -> bool {
430        self == *other
431    }
432}
433
434impl PartialEq<ToolResult> for str {
435    fn eq(&self, other: &ToolResult) -> bool {
436        other == self
437    }
438}
439
440impl PartialEq<ToolResult> for &str {
441    fn eq(&self, other: &ToolResult) -> bool {
442        other == *self
443    }
444}
445
446impl PartialEq<String> for ToolResult {
447    fn eq(&self, other: &String) -> bool {
448        self == other.as_str()
449    }
450}
451
452impl PartialEq<ToolResult> for String {
453    fn eq(&self, other: &ToolResult) -> bool {
454        other == self
455    }
456}
457
458/// Context passed to a tool call.
459#[derive(Debug, Clone, Copy)]
460pub struct ToolContext<'a> {
461    /// Run execution context.
462    pub run: &'a RunContext,
463    /// Shared cross-tool state.
464    pub state: &'a SharedState,
465    /// Source model tool-call id.
466    pub tool_call_id: &'a str,
467    /// Tool name used for this call.
468    pub tool_name: &'a str,
469}
470
471impl<'a> ToolContext<'a> {
472    /// Constructs tool-call context.
473    pub fn new(
474        run: &'a RunContext,
475        state: &'a SharedState,
476        tool_call_id: &'a str,
477        tool_name: &'a str,
478    ) -> Self {
479        Self {
480            run,
481            state,
482            tool_call_id,
483            tool_name,
484        }
485    }
486}
487
488/// A tool an agent can invoke.
489///
490/// A tool has two perspectives:
491/// - [`Tool::schema`] — the model perspective — tells the model what the
492///   tool is and what its arguments look like;
493/// - [`Tool::call`] — parses model-provided arguments into an immediate
494///   [`ToolOutput`] or an [`EffectRequest`] to be executed by an outer
495///   harness.
496///
497/// Implementations must be `Send + Sync`: the agent loop may execute tools
498/// concurrently on any thread. Tools that need to flow / share custom
499/// content across tools read and write [`ToolContext::state`];
500/// tools that do not can ignore it (`_state`).
501///
502/// # Example
503///
504/// ```
505/// # extern crate molo_core as molo;
506/// use molo::tool::{Tool, ToolContext, ToolError, ToolOutput, ToolResult, ToolSchema};
507/// use serde_json::json;
508///
509/// // A demo tool: returns a fixed time.
510/// struct TimeTool;
511///
512/// #[molo::async_trait]
513/// impl Tool for TimeTool {
514///     fn schema(&self) -> ToolSchema {
515///         ToolSchema::new(
516///             "time",
517///             "Return the current time",
518///             json!({ "type": "object", "properties": {} }),
519///         )
520///     }
521///
522///     async fn call(
523///         &self,
524///         _arguments: serde_json::Value,
525///         _context: ToolContext<'_>,
526///     ) -> Result<ToolResult, ToolError> {
527///         Ok(ToolOutput::text("12:00").into())
528///     }
529/// }
530///
531/// let tool = TimeTool;
532/// assert_eq!(tool.schema().name, "time");
533/// ```
534#[async_trait::async_trait]
535pub trait Tool: Send + Sync {
536    /// Model perspective: this tool's definition.
537    fn schema(&self) -> ToolSchema;
538
539    /// Execution perspective: run this tool.
540    ///
541    /// `arguments` is the model-generated arguments JSON parsed by the
542    /// registry. `context` carries the run context, source tool-call id/name,
543    /// and the agent's shared state.
544    async fn call(
545        &self,
546        arguments: serde_json::Value,
547        context: ToolContext<'_>,
548    ) -> Result<ToolResult, ToolError>;
549}
550
551/// Reasons a tool call fails.
552///
553/// `#[non_exhaustive]` ensures future error categories are not breaking
554/// changes; external crates should match with a wildcard arm to stay
555/// compatible with variants added in later versions.
556///
557/// # Example
558///
559/// ```
560/// # extern crate molo_core as molo;
561/// use molo::tool::ToolError;
562///
563/// let err = ToolError::InvalidArguments("missing field city".into());
564/// assert_eq!(err.to_string(), "invalid arguments: missing field city");
565/// ```
566#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
567#[non_exhaustive]
568pub enum ToolError {
569    /// The model-provided arguments do not match the tool's arguments schema.
570    #[error("invalid arguments: {0}")]
571    InvalidArguments(String),
572    /// The tool failed while executing.
573    ///
574    /// The Display text carries no "tool " prefix: the error type name
575    /// already conveys the domain, avoiding a doubled prefix like
576    /// "tool error: tool ..." after being wrapped by registry execution
577    /// errors.
578    #[error("execution failed: {0}")]
579    Execution(String),
580}
581
582impl From<serde_json::Error> for ToolError {
583    fn from(err: serde_json::Error) -> Self {
584        ToolError::InvalidArguments(err.to_string())
585    }
586}
587
588mod shared_state;