Skip to main content

runtime/tools/
call.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The face of `call` (`crates/runtime/Spec.lean` ยง8-61). A call through
7//! it is resolved at the tool face's entrance into the call it stands for,
8//! which then passes its own tool's doors; this tool is reached only when
9//! that resolution refused. It asks the catalog the same question again
10//! and answers its refusal, so the refusal has one author.
11
12use std::sync::{Arc, Mutex};
13
14use kernel::{
15    AxCode, AxError, CostTier, Effect, Payload, RenderIntent, Temporal, Tool, ToolCall, ToolMeta,
16    ToolName, ToolOutcome,
17};
18
19use crate::catalog::Catalog;
20
21/// The tool itself.
22pub struct CallTool {
23    catalog: Arc<Mutex<Catalog>>,
24    meta: ToolMeta,
25}
26
27impl CallTool {
28    /// The name this tool answers to, read by its registration, by
29    /// `mode::core_tools` and by the catalog's resolution.
30    pub const NAME: &'static str = "call";
31
32    /// # Errors
33    /// Propagates a malformed parameter schema, which is a build-time
34    /// defect rather than a runtime one.
35    pub fn new(catalog: Arc<Mutex<Catalog>>) -> Result<CallTool, AxError> {
36        Ok(CallTool {
37            catalog,
38            meta: ToolMeta {
39                name: ToolName::parse(Self::NAME)?,
40                disclosure: "Run a tool from the dormant index by its name. `describe` it first \
41                             for its input schema; arguments that do not fit are refused with \
42                             the schema."
43                    .to_owned(),
44                params: Payload::of(&serde_json::json!({
45                    "type": "object",
46                    "properties": {
47                        "name": { "type": "string", "description": "the tool to run, as the \
48                            dormant index or `describe` spells it" },
49                        "args": { "type": "object", "description": "the tool's own \
50                            arguments, as its input schema asks" },
51                    },
52                    "required": ["name", "args"],
53                }))?,
54                effect: Effect::Read,
55                cost_tier: CostTier::Free,
56                timeout: None,
57                render: RenderIntent::Generic,
58                temporal: Temporal::Timeless,
59            },
60        })
61    }
62}
63
64impl Tool for CallTool {
65    fn meta(&self) -> &ToolMeta {
66        &self.meta
67    }
68
69    fn invoke(&self, call: &ToolCall) -> Result<ToolOutcome, AxError> {
70        if call.name != self.meta.name {
71            return Err(AxError::failure(
72                AxCode::InvalidArgs,
73                "call a dormant tool",
74                format!("call routed to the wrong tool: {}", call.name.as_str()),
75            )
76            .with_recovery(format!(
77                "call `{}`, the name this tool answers to",
78                Self::NAME
79            )));
80        }
81        let resolved = self
82            .catalog
83            .lock()
84            .map_err(|_| super::describe::poisoned())?
85            .resolve_call(call)?;
86        // Resolution succeeds here only when the tool face that drove this
87        // call skipped it; running the tool from here would pass none of
88        // its doors.
89        Err(AxError::failure(
90            AxCode::ToolUnavailable,
91            "call a dormant tool",
92            format!("`{}` was not resolved before it ran", resolved.name),
93        )
94        .with_recovery(
95            "report this against the tool face driving this run: it must resolve a `call` \
96             before admitting it",
97        ))
98    }
99}