Skip to main content

runtime/catalog/
guide.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//! What `describe` answers (runtime D22): an admitted capability's whole
7//! guide when it is asked by its whole name, and otherwise the closest
8//! names by a fixed keyword ranking over every admitted capability's
9//! name and whole text, so a hint the dormant index cut is still found.
10
11use kernel::{AxCode, AxError};
12
13use super::{Catalog, Expansion, dormant};
14use crate::tools::CallTool;
15
16/// How many names one `describe` by keywords lists at most.
17pub(crate) const DESCRIBE_HITS: usize = 8;
18
19/// A keyword found in a capability's name counts this many times one
20/// found only in its text: the name is what the model asked about.
21const NAME_WEIGHT: u32 = 3;
22
23impl Catalog {
24    /// What `describe` answers for `asked`: the guide of the capability
25    /// it names, or the closest names to its words.
26    ///
27    /// A tool's guide is its description and input schema as registered,
28    /// unshortened, with how to run it; a skill's is its line and the
29    /// name `read` opens it by, because `read` is the one door that
30    /// opens a shelf; the mode and developer entries answer their own
31    /// text.
32    ///
33    /// # Errors
34    /// A tool schema that does not print as JSON, which its
35    /// registration already printed once.
36    pub fn describe(&self, asked: &str) -> Result<String, AxError> {
37        let asked = asked.trim();
38        if let Some(def) = self.tools.get(asked) {
39            let schema = serde_json::to_string(&def.input_schema).map_err(|err| {
40                AxError::failure(AxCode::InvalidArgs, "describe", asked.to_owned()).with_recovery(
41                    format!(
42                        "report this against runtime::catalog: a registered schema no \
43                         longer prints as JSON ({err})"
44                    ),
45                )
46            })?;
47            let how = if self.is_core(asked) {
48                "it is in your tool list; call it directly".to_owned()
49            } else {
50                format!(
51                    "run it with `{}` and the arguments {{\"name\": \"{asked}\", \"args\": \
52                     {{...}}}}",
53                    CallTool::NAME
54                )
55            };
56            return Ok(format!(
57                "tool {asked}: {how}.\n{}\ninput schema: {schema}\n",
58                def.description
59            ));
60        }
61        // The index spells a skill `skill <name>`; either spelling names it.
62        let skill = asked.strip_prefix("skill ").unwrap_or(asked);
63        if let Some(admitted) = self.skills.get(skill) {
64            return Ok(format!(
65                "skill {skill}: {}\nOpen its whole text with `read` and the path `{skill}`.\n",
66                admitted.entry.disclosure
67            ));
68        }
69        if let Some(Expansion::Said { text }) = self.expand(asked) {
70            return Ok(text);
71        }
72        Ok(self.closest(asked))
73    }
74
75    /// The admitted capabilities ranked by how many of `asked`'s words
76    /// they hold, highest first and then by name.
77    fn closest(&self, asked: &str) -> String {
78        let words: Vec<String> = asked
79            .split(|c: char| !c.is_alphanumeric())
80            .filter(|word| !word.is_empty())
81            .map(str::to_lowercase)
82            .collect();
83        let mut ranked: Vec<(u32, String, &str)> = self
84            .tools
85            .iter()
86            .map(|(name, def)| (name.clone(), def.description.as_str()))
87            .chain(self.skills.iter().map(|(name, admitted)| {
88                (format!("skill {name}"), admitted.entry.disclosure.as_str())
89            }))
90            .map(|(label, about)| (score(&words, &label, about), label, about))
91            .filter(|(score, _, _)| *score > 0)
92            .collect();
93        ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
94        if ranked.is_empty() {
95            return format!(
96                "Nothing admitted here matches `{asked}`. The dormant index in your prompt \
97                 names every capability this building admits beyond your tool list.\n"
98            );
99        }
100        let mut out: String = ranked
101            .iter()
102            .take(DESCRIBE_HITS)
103            .map(|(_, label, about)| format!("- {label}: {}\n", dormant::hint(about)))
104            .collect();
105        out.push_str("Describe one by its whole name for its guide.\n");
106        out
107    }
108}
109
110/// How well `label` and `about` answer `words`: each word found in the
111/// label counts [`NAME_WEIGHT`], each found only in the text counts one.
112fn score(words: &[String], label: &str, about: &str) -> u32 {
113    let label = label.to_lowercase();
114    let about = about.to_lowercase();
115    words
116        .iter()
117        .map(|word| {
118            if label.contains(word.as_str()) {
119                NAME_WEIGHT
120            } else {
121                u32::from(about.contains(word.as_str()))
122            }
123        })
124        .fold(0, u32::saturating_add)
125}