Skip to main content

everruns_core/
tools.rs

1// Tool Abstraction for Agent Loop
2//
3// This module provides a high-level abstraction for tools that can be executed
4// by the agent loop. Tools are defined using the `Tool` trait and can be
5// registered with a `ToolRegistry` for use in the loop.
6//
7// Design decisions:
8// - Tools are defined via a trait for flexibility (function-style tools)
9// - ToolRegistry implements ToolExecutor for integration with the agent loop
10// - Error handling distinguishes between user-visible errors and internal errors
11// - Internal errors are logged but not exposed to the LLM (security)
12
13use async_trait::async_trait;
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tracing::error;
18
19use crate::background::BackgroundExecutableTool;
20use crate::tool_types::{
21    BuiltinTool, DeferrablePolicy, ToolCall, ToolDefinition, ToolHints, ToolPolicy, ToolResult,
22    ToolResultImage,
23};
24use crate::{
25    tool_context::ToolContext, tool_context::ToolContextService, tool_context::ToolContextServices,
26};
27
28use crate::error::Result;
29use crate::tool_execution::ToolExecutor;
30// EVE-888: `spawn_background`, its session-task mirroring, the background
31// event sink and the reattach path moved to `everruns-platform`
32// (`background_run`). Creating session tasks and schedules is hosted behaviour;
33// the kernel keeps the neutral `BackgroundExecutableTool`/`BackgroundEventSink`
34// contracts in `crate::background` and runs whatever a host supplies.
35
36// ============================================================================
37// Tool Execution Result - Error Handling Contract
38// ============================================================================
39
40/// Result of a tool execution.
41///
42/// This enum distinguishes between different outcomes:
43/// - `Success`: Tool executed successfully, result is returned to LLM
44/// - `SuccessWithImages`: Successful execution with JSON result plus images
45/// - `ToolError`: Tool-level error that should be shown to the LLM
46///   (e.g., "City not found", "Invalid date format")
47/// - `InternalError`: System-level error that should NOT be exposed to the LLM
48///   (e.g., database connection failure, API key issues)
49///
50/// # Security
51///
52/// Internal errors are logged but replaced with a generic message when
53/// returned to the LLM. This prevents leaking sensitive information like
54/// database errors, API keys, or internal system details.
55#[derive(Debug)]
56pub enum ToolExecutionResult {
57    /// Successful execution with a JSON result
58    Success(Value),
59
60    /// Successful execution with a JSON result and images.
61    /// Images are sent to the LLM as native image content blocks
62    /// (not stringified JSON), enabling visual understanding.
63    SuccessWithImages {
64        result: Value,
65        images: Vec<ToolResultImage>,
66    },
67
68    /// Tool-level error that is safe to show to the LLM
69    ///
70    /// Use this for expected error conditions that the LLM should know about,
71    /// such as validation errors, resource not found, etc.
72    ToolError(String),
73
74    /// Internal/system error that should NOT be exposed to the LLM
75    ///
76    /// Use this for unexpected errors like network failures, database errors,
77    /// or other internal issues. The error details will be logged but replaced
78    /// with a generic message when returned to the LLM.
79    InternalError(ToolInternalError),
80
81    /// A user connection is required to execute this tool.
82    ///
83    /// Instead of returning an error, this signals that the workflow should
84    /// pause and ask the client to set up a connection for the given provider.
85    /// The UI renders an inline connection dialog; once the user saves (or
86    /// cancels), a tool result is submitted and execution resumes.
87    ConnectionRequired {
88        /// Connection provider id (e.g. "daytona", "brave_search")
89        provider: String,
90    },
91}
92
93impl ToolExecutionResult {
94    /// Create a successful result
95    pub fn success(value: impl Into<Value>) -> Self {
96        ToolExecutionResult::Success(value.into())
97    }
98
99    /// Create a successful result with pre-truncation raw output for VFS persistence.
100    /// The raw output is transferred to `ToolResult.raw_output` during `into_tool_result()`.
101    pub fn success_with_raw_output(value: impl Into<Value>, raw_output: String) -> Self {
102        let mut value = value.into();
103        // Embed raw output in a sidecar key — extracted in into_tool_result().
104        // Non-object values are wrapped in a scalar carrier so raw_output still
105        // flows through; the carrier is unwrapped on extraction.
106        match value.as_object_mut() {
107            Some(obj) => {
108                obj.insert("_raw_output".to_string(), Value::String(raw_output));
109            }
110            None => {
111                value = serde_json::json!({
112                    "_raw_output_scalar": value,
113                    "_raw_output": raw_output,
114                });
115            }
116        }
117        ToolExecutionResult::Success(value)
118    }
119
120    /// Create a successful result with images
121    pub fn success_with_images(value: impl Into<Value>, images: Vec<ToolResultImage>) -> Self {
122        ToolExecutionResult::SuccessWithImages {
123            result: value.into(),
124            images,
125        }
126    }
127
128    /// Create a tool-level error (safe to show to LLM)
129    pub fn tool_error(message: impl Into<String>) -> Self {
130        ToolExecutionResult::ToolError(message.into())
131    }
132
133    /// Create an internal error (will be hidden from LLM)
134    pub fn internal_error(error: impl std::error::Error + Send + Sync + 'static) -> Self {
135        ToolExecutionResult::InternalError(ToolInternalError::new(error))
136    }
137
138    /// Create an internal error from a string message
139    pub fn internal_error_msg(message: impl Into<String>) -> Self {
140        ToolExecutionResult::InternalError(ToolInternalError::from_message(message))
141    }
142
143    /// Signal that a user connection is required before this tool can execute.
144    pub fn connection_required(provider: impl Into<String>) -> Self {
145        ToolExecutionResult::ConnectionRequired {
146            provider: provider.into(),
147        }
148    }
149
150    /// Check if this is a successful result
151    pub fn is_success(&self) -> bool {
152        matches!(
153            self,
154            ToolExecutionResult::Success(_) | ToolExecutionResult::SuccessWithImages { .. }
155        )
156    }
157
158    /// Check if this is an error (either tool error or internal error)
159    pub fn is_error(&self) -> bool {
160        matches!(
161            self,
162            ToolExecutionResult::ToolError(_) | ToolExecutionResult::InternalError(_)
163        )
164    }
165
166    /// Check if this requires a user connection setup
167    pub fn is_connection_required(&self) -> bool {
168        matches!(self, ToolExecutionResult::ConnectionRequired { .. })
169    }
170
171    /// Convert to a ToolResult for the agent loop
172    ///
173    /// Both tool errors and internal errors are packaged as `{"error": "..."}` in the
174    /// result field. This provides a consistent contract where the result field always
175    /// contains the payload, and the agent loop continues the same way for all outcomes.
176    ///
177    /// Internal errors are logged but replaced with a generic message when returned.
178    pub fn into_tool_result(self, tool_call_id: &str, tool_name: &str) -> ToolResult {
179        match self {
180            ToolExecutionResult::Success(mut value) => {
181                // Extract sidecar raw output if present (from success_with_raw_output)
182                let raw_output = value
183                    .as_object_mut()
184                    .and_then(|obj| obj.remove("_raw_output"))
185                    .and_then(|v| v.as_str().map(|s| s.to_string()));
186                // Unwrap scalar carrier only when it matches the exact wrapper shape
187                // set by success_with_raw_output for non-object inputs.
188                let result_value = if let Some(obj) = value.as_object_mut() {
189                    let is_scalar_carrier = raw_output.is_some()
190                        && obj.len() == 1
191                        && obj.contains_key("_raw_output_scalar");
192                    if is_scalar_carrier {
193                        obj.remove("_raw_output_scalar").unwrap_or(Value::Null)
194                    } else {
195                        value
196                    }
197                } else {
198                    value
199                };
200                ToolResult {
201                    tool_call_id: tool_call_id.to_string(),
202                    result: Some(result_value),
203                    images: None,
204                    error: None,
205                    connection_required: None,
206                    raw_output,
207                }
208            }
209            ToolExecutionResult::SuccessWithImages { result, images } => ToolResult {
210                tool_call_id: tool_call_id.to_string(),
211                result: Some(result),
212                images: if images.is_empty() {
213                    None
214                } else {
215                    Some(images)
216                },
217                error: None,
218                connection_required: None,
219                raw_output: None,
220            },
221            ToolExecutionResult::ToolError(message) => ToolResult {
222                tool_call_id: tool_call_id.to_string(),
223                result: Some(serde_json::json!({ "error": &message })),
224                images: None,
225                error: Some(message),
226                connection_required: None,
227                raw_output: None,
228            },
229            ToolExecutionResult::InternalError(err) => {
230                // Log the full error details for debugging
231                error!(
232                    tool_name = %tool_name,
233                    tool_call_id = %tool_call_id,
234                    error = %err.message,
235                    error_chain = %err.chain_string(),
236                    "Tool internal error (details hidden from LLM)"
237                );
238
239                // Return generic error message to LLM, packaged as {"error": "..."}
240                let generic_msg = "An internal error occurred while executing the tool";
241                ToolResult {
242                    tool_call_id: tool_call_id.to_string(),
243                    result: Some(serde_json::json!({
244                        "error": generic_msg
245                    })),
246                    images: None,
247                    error: Some(generic_msg.to_string()),
248                    connection_required: None,
249                    raw_output: None,
250                }
251            }
252            ToolExecutionResult::ConnectionRequired { ref provider } => ToolResult {
253                tool_call_id: tool_call_id.to_string(),
254                result: Some(serde_json::json!({
255                    "connection_required": provider,
256                })),
257                images: None,
258                error: None,
259                connection_required: Some(provider.clone()),
260                raw_output: None,
261            },
262        }
263    }
264}
265
266/// Internal error details (logged but not exposed to LLM)
267#[derive(Debug)]
268pub struct ToolInternalError {
269    /// Error message for logging
270    pub message: String,
271    /// Optional source error
272    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
273}
274
275impl ToolInternalError {
276    /// Create from an error
277    pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
278        Self {
279            message: error.to_string(),
280            source: Some(Box::new(error)),
281        }
282    }
283
284    /// Create from a string message
285    pub fn from_message(message: impl Into<String>) -> Self {
286        Self {
287            message: message.into(),
288            source: None,
289        }
290    }
291
292    pub fn chain_string(&self) -> String {
293        let mut parts = vec![self.message.clone()];
294        let mut current = <Self as std::error::Error>::source(self);
295        while let Some(source) = current {
296            let message = source.to_string();
297            if parts.last() != Some(&message) {
298                parts.push(message);
299            }
300            current = source.source();
301        }
302        parts.join(": ")
303    }
304}
305
306impl std::fmt::Display for ToolInternalError {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        write!(f, "{}", self.message)
309    }
310}
311
312impl std::error::Error for ToolInternalError {
313    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
314        self.source
315            .as_ref()
316            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
317    }
318}
319
320// ============================================================================
321// Tool Trait - Core Tool Abstraction
322// ============================================================================
323
324/// Trait for implementing tools that can be executed by the agent loop.
325///
326/// # Example
327///
328/// ```ignore
329/// use async_trait::async_trait;
330/// use serde_json::{json, Value};
331///
332/// struct GetCurrentTime;
333///
334/// #[async_trait]
335/// impl Tool for GetCurrentTime {
336///     fn name(&self) -> &str {
337///         "get_current_time"
338///     }
339///
340///     fn description(&self) -> &str {
341///         "Get the current date and time"
342///     }
343///
344///     fn parameters_schema(&self) -> Value {
345///         json!({
346///             "type": "object",
347///             "properties": {
348///                 "timezone": {
349///                     "type": "string",
350///                     "description": "Timezone (e.g., 'UTC', 'America/New_York')"
351///                 }
352///             }
353///         })
354///     }
355///
356///     async fn execute(&self, arguments: Value) -> ToolExecutionResult {
357///         let timezone = arguments.get("timezone")
358///             .and_then(|v| v.as_str())
359///             .unwrap_or("UTC");
360///
361///         ToolExecutionResult::success(json!({
362///             "current_time": chrono::Utc::now().to_rfc3339(),
363///             "timezone": timezone
364///         }))
365///     }
366/// }
367/// ```
368#[async_trait]
369pub trait Tool: Send + Sync {
370    /// Returns the tool's unique name.
371    ///
372    /// This name is used by the LLM to invoke the tool and must be unique
373    /// within a ToolRegistry.
374    fn name(&self) -> &str;
375
376    /// Returns a human-readable display name for UI rendering.
377    ///
378    /// This name is shown to users in the UI instead of the technical tool name.
379    /// For example, "Get Current Time" instead of "get_current_time".
380    /// Returns None if no display name is set, in which case the UI may
381    /// fall back to the technical name.
382    fn display_name(&self) -> Option<&str> {
383        None
384    }
385
386    /// Returns a description of what the tool does.
387    ///
388    /// This description is provided to the LLM to help it understand
389    /// when and how to use the tool.
390    fn description(&self) -> &str;
391
392    /// Returns the JSON schema for the tool's parameters.
393    ///
394    /// This schema follows the JSON Schema specification and describes
395    /// the expected arguments for the tool. The LLM uses this to
396    /// generate valid tool calls.
397    fn parameters_schema(&self) -> Value;
398
399    /// Execute the tool with the given arguments.
400    ///
401    /// # Arguments
402    ///
403    /// * `arguments` - The arguments passed to the tool as a JSON value.
404    ///   These should conform to the schema returned by `parameters_schema()`.
405    ///
406    /// # Returns
407    ///
408    /// A `ToolExecutionResult` indicating success, tool error, or internal error.
409    async fn execute(&self, arguments: Value) -> ToolExecutionResult;
410
411    /// Execute the tool with context.
412    ///
413    /// This method provides access to runtime context like session ID and
414    /// optional stores (file store, etc.). Override this method for tools
415    /// that need access to session context or external resources.
416    ///
417    /// The default implementation simply calls `execute()`, ignoring the context.
418    ///
419    /// # Arguments
420    ///
421    /// * `arguments` - The arguments passed to the tool as a JSON value.
422    /// * `context` - Runtime context containing session ID and optional stores.
423    ///
424    /// # Returns
425    ///
426    /// A `ToolExecutionResult` indicating success, tool error, or internal error.
427    async fn execute_with_context(
428        &self,
429        arguments: Value,
430        _context: &ToolContext,
431    ) -> ToolExecutionResult {
432        // Default: delegate to execute(), ignoring context
433        self.execute(arguments).await
434    }
435
436    /// Returns true if this tool requires context for execution.
437    ///
438    /// Tools that need session context (like filesystem tools) should
439    /// override this to return true.
440    fn requires_context(&self) -> bool {
441        false
442    }
443
444    /// Runtime services that must be present before this tool can be exposed.
445    ///
446    /// Context-aware tools should declare hard requirements here. Optional
447    /// services that only enable extra behavior should not be listed.
448    fn required_context_services(&self) -> &'static [ToolContextService] {
449        &[]
450    }
451
452    /// Returns the tool policy (auto or requires_approval).
453    ///
454    /// Default is `Auto` which means the tool executes immediately.
455    /// Override to return `RequiresApproval` for sensitive operations.
456    fn policy(&self) -> ToolPolicy {
457        ToolPolicy::Auto
458    }
459
460    /// Returns semantic hints describing the tool's behavioral properties.
461    ///
462    /// Override to provide hints like readonly, destructive, idempotent, etc.
463    /// Default is empty (all hints unspecified).
464    fn hints(&self) -> ToolHints {
465        ToolHints::default()
466    }
467
468    /// Returns backend-authored narration for a call to this tool, e.g.
469    /// "Read AGENTS.md".
470    ///
471    /// The owning capability's default [`crate::capabilities::Capability::narrate`]
472    /// dispatches here for the tool whose `name()` matches the call. Return
473    /// `None` to accept the generic `narration_noun`/display-name fallback.
474    /// Implementations should use the phrasing helpers in
475    /// [`crate::tool_narration`] (`narrate_read_file`, `narrate_shell_exec`, …)
476    /// so wording and localization stay consistent.
477    fn narrate(
478        &self,
479        _tool_call: &crate::tool_types::ToolCall,
480        _phase: crate::tool_narration::ToolNarrationPhase,
481        _locale: Option<&str>,
482        _ctx: crate::tool_narration::ToolNarrationContext<'_>,
483    ) -> Option<String> {
484        None
485    }
486
487    /// Returns native background execution support when this tool opts into
488    /// detached execution via `hints().supports_background`.
489    fn as_background_executable(&self) -> Option<&dyn BackgroundExecutableTool> {
490        None
491    }
492
493    /// Deferral policy for progressive tool-schema disclosure (tool search).
494    /// Hot-path or "consult-first" tools can return [`DeferrablePolicy::Never`]
495    /// to always keep their full schema directly callable. Defaults to
496    /// [`DeferrablePolicy::Automatic`].
497    fn deferrable_policy(&self) -> DeferrablePolicy {
498        DeferrablePolicy::default()
499    }
500
501    /// Convert this tool to a ToolDefinition for the agent config.
502    ///
503    /// This is used by ToolRegistry to generate tool definitions
504    /// for the LLM provider.
505    fn to_definition(&self) -> ToolDefinition {
506        ToolDefinition::Builtin(BuiltinTool {
507            name: self.name().to_string(),
508            display_name: self.display_name().map(|s| s.to_string()),
509            description: self.description().to_string(),
510            parameters: self.parameters_schema(),
511            policy: self.policy(),
512            category: None,
513            deferrable: self.deferrable_policy(),
514            hints: self.hints(),
515            full_parameters: None,
516        })
517    }
518}
519
520// ============================================================================
521// ToolRegistry - Collection of Tools
522// ============================================================================
523
524/// A registry that holds multiple tools and implements ToolExecutor.
525///
526/// ToolRegistry provides a convenient way to manage multiple tools and
527/// integrate them with the agent loop. It implements `ToolExecutor` so
528/// it can be used directly with `AgentLoop`.
529///
530/// # Example
531///
532/// ```ignore
533/// use everruns_core::tools::{Tool, ToolRegistry};
534///
535/// // Create registry and add tools
536/// let mut registry = ToolRegistry::new();
537/// registry.register(Box::new(GetCurrentTime));
538/// registry.register(Box::new(GetWeather));
539///
540/// // Get tool definitions for agent config
541/// let definitions = registry.tool_definitions();
542///
543/// // Use with agent loop
544/// let agent_loop = AgentLoop::new(config, emitter, store, llm, registry);
545/// ```
546#[derive(Default, Clone)]
547pub struct ToolRegistry {
548    tools: HashMap<String, Arc<dyn Tool>>,
549}
550
551impl ToolRegistry {
552    /// Create a new empty tool registry
553    pub fn new() -> Self {
554        Self {
555            tools: HashMap::new(),
556        }
557    }
558
559    /// Create a tool registry with default built-in tools.
560    ///
561    /// This includes `report_progress`, the neutral progress-reporting
562    /// contract tool. Test doubles such as echo tools belong to test-support
563    /// or the test that owns them.
564    ///
565    /// Test fixture tools (test math/weather) are NOT included: they moved to
566    /// the `everruns-test-support` crate (EVE-875) and are registered
567    /// explicitly by tests that need them.
568    pub fn with_defaults() -> Self {
569        use crate::progress_reporting::ReportProgressTool;
570
571        let builder = ToolRegistry::builder()
572            // NOTE: `spawn_background` is intentionally NOT a default tool —
573            // it is contributed by the `background_execution` capability,
574            // which is auto-activated by
575            // `collect_capabilities_with_configs` whenever a collected tool
576            // declares `ToolHints::supports_background = Some(true)`. Keeping
577            // it out of defaults preserves the lockstep contract between
578            // model-visible tools and the worker execution registry: the
579            // executor only knows about `spawn_background` when the model
580            // can also see it.
581            .tool(ReportProgressTool);
582
583        builder.build()
584    }
585
586    /// Create a tool registry for autonomous scheduled monitor probes.
587    ///
588    /// Probe execution currently uses a scheduler-local [`ToolContext`] instead
589    /// of the fully populated worker/API executor context. Keep this registry to
590    /// context-free tools so scheduled probes cannot bypass session-scoped
591    /// controls such as network ACLs, egress routing, storage, or filesystem
592    /// mediation.
593    pub fn with_monitor_probe_defaults() -> Self {
594        Self::new()
595    }
596
597    /// Register a tool with the registry.
598    ///
599    /// If a tool with the same name already exists, it will be replaced.
600    pub fn register(&mut self, tool: impl Tool + 'static) {
601        self.tools.insert(tool.name().to_string(), Arc::new(tool));
602    }
603
604    /// Register a boxed tool
605    pub fn register_boxed(&mut self, tool: Box<dyn Tool>) {
606        self.tools.insert(tool.name().to_string(), Arc::from(tool));
607    }
608
609    /// Register an Arc-wrapped tool
610    pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
611        self.tools.insert(tool.name().to_string(), tool);
612    }
613
614    /// Get a tool by name
615    pub fn get(&self, name: &str) -> Option<&Arc<dyn Tool>> {
616        self.tools.get(name)
617    }
618
619    /// Check if a tool is registered
620    pub fn has(&self, name: &str) -> bool {
621        self.tools.contains_key(name)
622    }
623
624    /// Get the number of registered tools
625    pub fn len(&self) -> usize {
626        self.tools.len()
627    }
628
629    /// Check if the registry is empty
630    pub fn is_empty(&self) -> bool {
631        self.tools.is_empty()
632    }
633
634    /// Get all tool names
635    pub fn tool_names(&self) -> Vec<&str> {
636        self.tools.keys().map(|s| s.as_str()).collect()
637    }
638
639    /// Get tool definitions for use in RuntimeAgent.
640    ///
641    /// Returns a Vec of ToolDefinition that can be passed to
642    /// `RuntimeAgent::with_tools()`.
643    pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
644        self.tools.values().map(|t| t.to_definition()).collect()
645    }
646
647    /// Fail configuration before model exposure when a registered tool is
648    /// missing a runtime service it declares as required.
649    pub fn validate_context_services(&self, services: &ToolContextServices) -> Result<()> {
650        let mut tools: Vec<_> = self.tools.values().collect();
651        tools.sort_by_key(|tool| tool.name());
652        for tool in tools {
653            for service in tool.required_context_services() {
654                if !services.provides(*service) {
655                    return Err(crate::error::AgentLoopError::config(format!(
656                        "tool \"{}\" requires unavailable ToolContext service {}",
657                        tool.name(),
658                        service.name(),
659                    )));
660                }
661            }
662        }
663        Ok(())
664    }
665
666    /// Remove a tool from the registry
667    pub fn unregister(&mut self, name: &str) -> Option<Arc<dyn Tool>> {
668        self.tools.remove(name)
669    }
670
671    /// Clear all tools from the registry
672    pub fn clear(&mut self) {
673        self.tools.clear();
674    }
675
676    /// Create a builder for fluent tool registration
677    pub fn builder() -> ToolRegistryBuilder {
678        ToolRegistryBuilder::new()
679    }
680}
681
682impl std::fmt::Debug for ToolRegistry {
683    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684        f.debug_struct("ToolRegistry")
685            .field("tools", &self.tool_names())
686            .finish()
687    }
688}
689
690#[async_trait]
691impl ToolExecutor for ToolRegistry {
692    async fn execute(
693        &self,
694        tool_call: &ToolCall,
695        _tool_def: &ToolDefinition,
696    ) -> Result<ToolResult> {
697        let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
698            crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
699        })?;
700
701        let result = tool.execute(tool_call.arguments.clone()).await;
702        Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
703    }
704
705    async fn execute_with_context(
706        &self,
707        tool_call: &ToolCall,
708        _tool_def: &ToolDefinition,
709        context: &ToolContext,
710    ) -> Result<ToolResult> {
711        let tool = self.tools.get(&tool_call.name).ok_or_else(|| {
712            crate::error::AgentLoopError::tool(format!("Tool not found: {}", tool_call.name))
713        })?;
714
715        // Use execute_with_context for all tools - context-aware tools will use it,
716        // regular tools will delegate to execute() via the default implementation
717        let result = tool
718            .execute_with_context(tool_call.arguments.clone(), context)
719            .await;
720        Ok(result.into_tool_result(&tool_call.id, &tool_call.name))
721    }
722}
723
724// ============================================================================
725// ToolRegistryBuilder - Fluent API for Building Registry
726// ============================================================================
727
728/// Builder for creating a ToolRegistry with a fluent API.
729///
730/// # Example
731///
732/// ```ignore
733/// let registry = ToolRegistry::builder()
734///     .tool(GetCurrentTime)
735///     .tool(GetWeather)
736///     .build();
737/// ```
738pub struct ToolRegistryBuilder {
739    registry: ToolRegistry,
740}
741
742impl ToolRegistryBuilder {
743    /// Create a new builder
744    pub fn new() -> Self {
745        Self {
746            registry: ToolRegistry::new(),
747        }
748    }
749
750    /// Add a tool to the registry
751    pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
752        self.registry.register(tool);
753        self
754    }
755
756    /// Add a boxed tool to the registry
757    pub fn tool_boxed(mut self, tool: Box<dyn Tool>) -> Self {
758        self.registry.register_boxed(tool);
759        self
760    }
761
762    /// Add an Arc-wrapped tool to the registry
763    pub fn tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
764        self.registry.register_arc(tool);
765        self
766    }
767
768    /// Build the registry
769    pub fn build(self) -> ToolRegistry {
770        self.registry
771    }
772}
773
774impl Default for ToolRegistryBuilder {
775    fn default() -> Self {
776        Self::new()
777    }
778}
779
780// ============================================================================
781// Built-in Tools
782// ============================================================================
783
784/// A tool that echoes back its arguments (useful for testing).
785#[cfg(test)]
786pub struct EchoTool;
787
788#[cfg(test)]
789#[async_trait]
790impl Tool for EchoTool {
791    fn name(&self) -> &str {
792        "echo"
793    }
794
795    fn display_name(&self) -> Option<&str> {
796        Some("Echo")
797    }
798
799    fn description(&self) -> &str {
800        "Echo back the provided message. Useful for testing tool execution."
801    }
802
803    fn parameters_schema(&self) -> Value {
804        serde_json::json!({
805            "type": "object",
806            "properties": {
807                "message": {
808                    "type": "string",
809                    "description": "The message to echo back"
810                }
811            },
812            "required": ["message"],
813            "additionalProperties": false
814        })
815    }
816
817    fn hints(&self) -> ToolHints {
818        ToolHints::default()
819            .with_readonly(true)
820            .with_idempotent(true)
821    }
822
823    async fn execute(&self, arguments: Value) -> ToolExecutionResult {
824        let message = arguments
825            .get("message")
826            .and_then(|v| v.as_str())
827            .unwrap_or("");
828
829        ToolExecutionResult::success(serde_json::json!({
830            "echoed": message,
831            "length": message.len()
832        }))
833    }
834}
835
836/// A tool that always fails (useful for testing error handling).
837#[cfg(test)]
838pub struct FailingTool {
839    error_message: String,
840    use_internal_error: bool,
841}
842
843#[cfg(test)]
844impl FailingTool {
845    /// Create a failing tool with a tool-level error
846    pub fn with_tool_error(message: impl Into<String>) -> Self {
847        Self {
848            error_message: message.into(),
849            use_internal_error: false,
850        }
851    }
852
853    /// Create a failing tool with an internal error
854    pub fn with_internal_error(message: impl Into<String>) -> Self {
855        Self {
856            error_message: message.into(),
857            use_internal_error: true,
858        }
859    }
860}
861
862#[cfg(test)]
863impl Default for FailingTool {
864    fn default() -> Self {
865        Self::with_tool_error("Tool execution failed")
866    }
867}
868
869#[cfg(test)]
870#[async_trait]
871impl Tool for FailingTool {
872    fn name(&self) -> &str {
873        "failing_tool"
874    }
875
876    fn display_name(&self) -> Option<&str> {
877        Some("Failing Tool")
878    }
879
880    fn description(&self) -> &str {
881        "A tool that always fails (for testing error handling)"
882    }
883
884    fn parameters_schema(&self) -> Value {
885        serde_json::json!({
886            "type": "object",
887            "properties": {},
888            "additionalProperties": false
889        })
890    }
891
892    fn hints(&self) -> ToolHints {
893        ToolHints::default()
894            .with_readonly(true)
895            .with_idempotent(true)
896    }
897
898    async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
899        if self.use_internal_error {
900            ToolExecutionResult::internal_error_msg(&self.error_message)
901        } else {
902            ToolExecutionResult::tool_error(&self.error_message)
903        }
904    }
905}
906
907// ============================================================================
908// Tests
909// ============================================================================
910
911#[cfg(test)]
912mod tests {
913    use super::*;
914
915    struct RequiresOrgId;
916
917    #[async_trait]
918    impl Tool for RequiresOrgId {
919        fn name(&self) -> &str {
920            "requires_org_id"
921        }
922
923        fn description(&self) -> &str {
924            "Exercises required ToolContext service validation"
925        }
926
927        fn parameters_schema(&self) -> Value {
928            serde_json::json!({"type": "object", "additionalProperties": false})
929        }
930
931        fn required_context_services(&self) -> &'static [ToolContextService] {
932            &[ToolContextService::OrgId]
933        }
934
935        async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
936            ToolExecutionResult::success(Value::Null)
937        }
938    }
939
940    #[test]
941    fn required_context_service_validation_is_structured() {
942        let mut registry = ToolRegistry::new();
943        registry.register(RequiresOrgId);
944
945        let error = registry
946            .validate_context_services(&ToolContextServices::default())
947            .expect_err("missing required service must fail before tool exposure");
948
949        assert!(matches!(
950            error,
951            crate::AgentLoopError::Configuration(message)
952                if message.contains("requires_org_id") && message.contains("OrgId")
953        ));
954    }
955
956    #[test]
957    fn required_context_service_validation_accepts_supplied_service() {
958        let mut registry = ToolRegistry::new();
959        registry.register(RequiresOrgId);
960        let services = ToolContextServices {
961            org_id: Some(crate::typed_id::OrgId::from_seed(1)),
962            ..ToolContextServices::default()
963        };
964
965        registry
966            .validate_context_services(&services)
967            .expect("advertised required service should validate");
968    }
969
970    #[tokio::test]
971    async fn test_echo_tool() {
972        let tool = EchoTool;
973
974        let result = tool
975            .execute(serde_json::json!({"message": "Hello, world!"}))
976            .await;
977
978        if let ToolExecutionResult::Success(value) = result {
979            assert_eq!(
980                value.get("echoed").unwrap().as_str().unwrap(),
981                "Hello, world!"
982            );
983            assert_eq!(value.get("length").unwrap().as_u64().unwrap(), 13);
984        } else {
985            panic!("Expected success");
986        }
987    }
988
989    #[tokio::test]
990    async fn test_failing_tool_with_tool_error() {
991        let tool = FailingTool::with_tool_error("Something went wrong");
992
993        let result = tool.execute(serde_json::json!({})).await;
994
995        if let ToolExecutionResult::ToolError(msg) = result {
996            assert_eq!(msg, "Something went wrong");
997        } else {
998            panic!("Expected tool error");
999        }
1000    }
1001
1002    #[tokio::test]
1003    async fn test_failing_tool_with_internal_error() {
1004        let tool = FailingTool::with_internal_error("Database connection failed");
1005
1006        let result = tool.execute(serde_json::json!({})).await;
1007
1008        if let ToolExecutionResult::InternalError(err) = result {
1009            assert_eq!(err.message, "Database connection failed");
1010        } else {
1011            panic!("Expected internal error");
1012        }
1013    }
1014
1015    #[tokio::test]
1016    async fn test_tool_result_conversion() {
1017        // Success
1018        let result = ToolExecutionResult::success(serde_json::json!({"value": 42}));
1019        let tool_result = result.into_tool_result("call_1", "test_tool");
1020        assert!(tool_result.error.is_none());
1021        assert_eq!(tool_result.result.unwrap()["value"], 42);
1022
1023        // Tool error (packaged as {"error": "..."} in result field, also sets error)
1024        let result = ToolExecutionResult::tool_error("Invalid input");
1025        let tool_result = result.into_tool_result("call_2", "test_tool");
1026        assert_eq!(tool_result.error.as_deref(), Some("Invalid input"));
1027        assert_eq!(
1028            tool_result.result.unwrap(),
1029            serde_json::json!({"error": "Invalid input"})
1030        );
1031
1032        // Internal error (packaged as {"error": "..."} with generic message)
1033        let result = ToolExecutionResult::internal_error_msg("Secret database error");
1034        let tool_result = result.into_tool_result("call_3", "test_tool");
1035        assert_eq!(
1036            tool_result.error.as_deref(),
1037            Some("An internal error occurred while executing the tool")
1038        );
1039        assert_eq!(
1040            tool_result.result.unwrap(),
1041            serde_json::json!({"error": "An internal error occurred while executing the tool"})
1042        );
1043    }
1044
1045    #[tokio::test]
1046    async fn test_tool_registry() {
1047        let mut registry = ToolRegistry::new();
1048        registry.register(EchoTool);
1049
1050        assert_eq!(registry.len(), 1);
1051        assert!(registry.has("echo"));
1052        assert!(!registry.has("nonexistent"));
1053
1054        let definitions = registry.tool_definitions();
1055        assert_eq!(definitions.len(), 1);
1056    }
1057
1058    #[tokio::test]
1059    async fn test_tool_registry_builder() {
1060        let registry = ToolRegistry::builder().tool(EchoTool).build();
1061
1062        assert_eq!(registry.len(), 1);
1063    }
1064
1065    #[test]
1066    fn test_tool_display_name_in_definition() {
1067        let tool = EchoTool;
1068        assert_eq!(tool.display_name(), Some("Echo"));
1069
1070        let def = tool.to_definition();
1071        assert_eq!(def.display_name(), Some("Echo"));
1072    }
1073
1074    #[test]
1075    fn test_success_with_raw_output_object_preserves_shape() {
1076        let res = ToolExecutionResult::success_with_raw_output(
1077            serde_json::json!({"stdout": "hello"}),
1078            "raw stdout bytes".to_string(),
1079        );
1080        let tr = res.into_tool_result("call_1", "demo");
1081        assert_eq!(tr.result.as_ref().unwrap()["stdout"], "hello");
1082        assert!(
1083            tr.result
1084                .as_ref()
1085                .unwrap()
1086                .as_object()
1087                .unwrap()
1088                .get("_raw_output")
1089                .is_none(),
1090            "sidecar key must not leak to the LLM-visible result"
1091        );
1092        assert_eq!(tr.raw_output.as_deref(), Some("raw stdout bytes"));
1093    }
1094
1095    #[test]
1096    fn test_success_with_raw_output_scalar_unwraps_to_string() {
1097        let res = ToolExecutionResult::success_with_raw_output(
1098            "compact summary".to_string(),
1099            "full output bytes".to_string(),
1100        );
1101        let tr = res.into_tool_result("call_1", "demo");
1102        assert_eq!(
1103            tr.result,
1104            Some(serde_json::Value::String("compact summary".into()))
1105        );
1106        assert_eq!(tr.raw_output.as_deref(), Some("full output bytes"));
1107    }
1108
1109    #[test]
1110    fn test_success_result_with_raw_output_scalar_key_is_not_unwrapped() {
1111        let res = ToolExecutionResult::success(
1112            serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}),
1113        );
1114        let tr = res.into_tool_result("call_1", "demo");
1115        assert_eq!(
1116            tr.result,
1117            Some(serde_json::json!({"_raw_output_scalar": "user_value", "kept": true}))
1118        );
1119        assert_eq!(tr.raw_output, None);
1120    }
1121
1122    #[test]
1123    fn test_success_result_with_only_raw_output_scalar_key_is_not_unwrapped() {
1124        // Single-key object with _raw_output_scalar must not be mistaken for a
1125        // success_with_raw_output carrier when raw_output is absent.
1126        let res = ToolExecutionResult::success(serde_json::json!({"_raw_output_scalar": "v"}));
1127        let tr = res.into_tool_result("call_1", "demo");
1128        assert_eq!(
1129            tr.result,
1130            Some(serde_json::json!({"_raw_output_scalar": "v"}))
1131        );
1132        assert_eq!(tr.raw_output, None);
1133    }
1134
1135    #[test]
1136    fn test_echo_tool_display_name() {
1137        let tool = EchoTool;
1138        assert_eq!(tool.display_name(), Some("Echo"));
1139
1140        let def = tool.to_definition();
1141        assert_eq!(def.display_name(), Some("Echo"));
1142    }
1143
1144    #[test]
1145    fn test_all_default_tools_have_display_names() {
1146        let registry = ToolRegistry::with_defaults();
1147        let definitions = registry.tool_definitions();
1148
1149        for def in &definitions {
1150            assert!(
1151                def.display_name().is_some(),
1152                "Tool '{}' should have a display_name",
1153                def.name()
1154            );
1155        }
1156    }
1157
1158    #[tokio::test]
1159    async fn test_tool_registry_as_executor() {
1160        let mut registry = ToolRegistry::new();
1161        registry.register(EchoTool);
1162
1163        let tool_call = ToolCall {
1164            id: "call_1".to_string(),
1165            name: "echo".to_string(),
1166            arguments: serde_json::json!({"message": "test"}),
1167        };
1168
1169        let tool_def = registry.get("echo").unwrap().to_definition();
1170        let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1171
1172        assert!(result.error.is_none());
1173        assert_eq!(result.result.unwrap()["echoed"], "test");
1174    }
1175
1176    #[test]
1177    fn test_tool_to_definition() {
1178        let tool = EchoTool;
1179        let def = tool.to_definition();
1180
1181        let ToolDefinition::Builtin(builtin) = def else {
1182            panic!("expected Builtin variant");
1183        };
1184        assert_eq!(builtin.name, "echo");
1185        assert_eq!(builtin.policy, ToolPolicy::Auto);
1186    }
1187
1188    #[test]
1189    fn test_with_defaults_has_expected_tools() {
1190        let registry = ToolRegistry::with_defaults();
1191
1192        // Core execution-contract tools only.
1193        // spawn_background is contributed by the background_execution
1194        // capability (auto-activated) — it must NOT be in defaults.
1195        assert!(
1196            !registry.has("spawn_background"),
1197            "spawn_background must NOT be in defaults — it comes from the \
1198             background_execution capability"
1199        );
1200        assert!(
1201            registry.has("report_progress"),
1202            "should have report_progress"
1203        );
1204
1205        // Test fixture tools moved to everruns-test-support (EVE-875) and
1206        // must NOT be in defaults.
1207        assert!(!registry.has("add"), "add must NOT be in defaults");
1208        assert!(
1209            !registry.has("get_weather"),
1210            "get_weather must NOT be in defaults"
1211        );
1212
1213        // Environment-backed tools are composed by the host, not core defaults.
1214        for tool in ["read_file", "write_file", "bash", "web_fetch"] {
1215            assert!(!registry.has(tool), "`{tool}` must not be a core default");
1216        }
1217
1218        assert_eq!(registry.len(), 1, "should have one core default tool");
1219    }
1220
1221    #[tokio::test]
1222    async fn test_with_defaults_tools_are_executable() {
1223        let registry = ToolRegistry::with_defaults();
1224
1225        // The neutral progress contract remains executable as a core default.
1226        let tool_call = ToolCall {
1227            id: "call_1".to_string(),
1228            name: "report_progress".to_string(),
1229            arguments: serde_json::json!({
1230                "status": "completed",
1231                "summary": "Boundary audit complete"
1232            }),
1233        };
1234
1235        let tool_def = registry.get("report_progress").unwrap().to_definition();
1236        let result = registry.execute(&tool_call, &tool_def).await.unwrap();
1237
1238        assert!(result.error.is_none());
1239        assert_eq!(result.result.unwrap()["summary"], "Boundary audit complete");
1240    }
1241
1242    /// Regression: with_defaults() must NOT include capability-provided tools like
1243    /// 'bash'. These tools come from capabilities and must be registered separately.
1244    /// If bash were in defaults, the harness capability fallback would be masked.
1245    #[test]
1246    fn test_with_defaults_excludes_capability_only_tools() {
1247        let registry = ToolRegistry::with_defaults();
1248
1249        // bash comes from bashkit_shell capability, not defaults
1250        assert!(
1251            !registry.has("bash"),
1252            "bash must not be in defaults — it comes from bashkit_shell capability"
1253        );
1254        // kv_store/secret_store come from session_storage capability
1255        assert!(
1256            !registry.has("kv_store"),
1257            "kv_store must not be in defaults — it comes from session_storage capability"
1258        );
1259        // spawn_background comes from background_execution capability and is
1260        // auto-activated by `collect_capabilities_with_configs` when a
1261        // background-capable tool is present (see EVE-501).
1262        assert!(
1263            !registry.has("spawn_background"),
1264            "spawn_background must not be in defaults — it comes from the \
1265             background_execution capability (auto-activated by tool hints)"
1266        );
1267    }
1268
1269    // =========================================================================
1270    // Cooperative cancellation tests
1271    // =========================================================================
1272
1273    // Minimal in-memory SessionTaskRegistry for cancel tests.
1274    // (Mirrors the double in capabilities/session_tasks.rs — kept local because
1275    //  that module is private.)
1276
1277    // -------------------------------------------------------------------------
1278    // reattach_background_run early-guard tests
1279    // -------------------------------------------------------------------------
1280}