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;