use std::collections::BTreeMap;
pub use kernel::event::record::SkillPin;
use kernel::{AxCode, AxError, B3Hash, ToolCall, ToolDef, ToolMeta};
use crate::mode::catalog_entry;
use kernel::Mode;
mod dormant;
mod fit;
mod guide;
pub const DORMANT_INDEX_CEILING: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogEntry {
pub name: String,
pub disclosure: String,
pub expansion: String,
pub hash: Option<B3Hash>,
pub package: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expansion {
Skill {
addr: String,
package: Option<String>,
},
Said { text: String },
}
#[derive(Debug, Default)]
pub struct Catalog {
tools: BTreeMap<String, ToolDef>,
skills: BTreeMap<String, Admitted>,
mode: Option<Mode>,
}
#[derive(Debug)]
struct Admitted {
entry: CatalogEntry,
kept: Kept,
}
#[derive(Debug, Clone, Copy)]
enum Kept {
Shelved,
Carried,
}
impl Catalog {
pub fn new() -> Catalog {
Catalog::default()
}
pub fn admit_tool(&mut self, meta: &ToolMeta) -> Result<(), AxError> {
if meta.disclosure.trim().is_empty() {
return Err(AxError::failure(
AxCode::InvalidArgs,
"admit tool to catalog",
format!("{}: empty disclosure", meta.name),
)
.with_recovery(format!(
"write one sentence in the `disclosure` field of `{}`'s `ToolMeta` \
saying what the tool does",
meta.name
)));
}
let key = meta.name.as_str().to_owned();
if self.tools.contains_key(&key) {
return Err(AxError::failure(
AxCode::InvalidArgs,
"admit tool to catalog",
format!("{key}: duplicate name"),
)
.with_recovery(format!(
"rename one of the two tools declaring `{key}` in its `ToolMeta`: a \
catalog name points at one tool"
)));
}
self.tools.insert(
key,
ToolDef {
name: meta.name.clone(),
description: meta.disclosure.clone(),
input_schema: meta.params.clone(),
},
);
Ok(())
}
pub fn admit_skill(&mut self, entry: CatalogEntry) -> Result<(), AxError> {
self.admit(entry, Kept::Shelved)
}
pub fn admit_carried_skill(&mut self, entry: CatalogEntry) -> Result<(), AxError> {
self.admit(entry, Kept::Carried)
}
fn admit(&mut self, entry: CatalogEntry, kept: Kept) -> Result<(), AxError> {
if entry.name.trim().is_empty() || entry.disclosure.trim().is_empty() {
return Err(AxError::failure(
AxCode::InvalidArgs,
"admit skill to catalog",
"empty name or disclosure",
)
.with_recovery(
"give the SKILL file a title and a one-line description; the catalog \
shows both and a resident picks the skill by them",
));
}
if self.skills.contains_key(&entry.name) {
return Err(AxError::failure(
AxCode::InvalidArgs,
"admit skill to catalog",
format!("{}: duplicate name", entry.name),
)
.with_recovery(format!(
"rename one of the two SKILL files titled `{}`: a catalog name points \
at one skill",
entry.name
)));
}
self.skills
.insert(entry.name.clone(), Admitted { entry, kept });
Ok(())
}
pub fn set_mode(&mut self, mode: Mode) {
self.mode = Some(mode);
}
pub fn render(&self) -> String {
let mut out = String::from(
"Catalog: what you can reach beyond the tools listed with this request. \
Open an entry by name with `read` before first use.\n",
);
if let Some(mode) = self.mode {
let entry = catalog_entry(mode);
out.push_str("- ");
out.push_str(&entry.name);
out.push_str(": ");
out.push_str(&entry.disclosure);
out.push('\n');
}
let dev = crate::mode::dev_entry();
out.push_str("- ");
out.push_str(&dev.name);
out.push_str(": ");
out.push_str(&dev.disclosure);
out.push('\n');
out.push_str(&dormant::index(&self.dormant_entries()));
out
}
fn dormant_entries(&self) -> Vec<dormant::Entry<'_>> {
self.tools
.iter()
.filter(|(name, _)| !self.is_core(name))
.map(|(name, def)| dormant::Entry {
label: name.clone(),
about: &def.description,
})
.chain(self.skills.iter().map(|(name, admitted)| dormant::Entry {
label: format!("skill {name}"),
about: &admitted.entry.disclosure,
}))
.collect()
}
fn is_core(&self, name: &str) -> bool {
self.mode.is_some() && crate::mode::in_some_core(name)
}
pub fn resolve_call(&self, call: &ToolCall) -> Result<ToolCall, AxError> {
fit::resolve(&self.tools, call)
}
pub fn tool_defs(&self) -> Vec<ToolDef> {
self.tools
.iter()
.filter(|(name, _)| self.is_core(name))
.map(|(_, def)| def.clone())
.collect()
}
pub fn skill_pins(&self) -> Vec<SkillPin> {
self.skills
.iter()
.filter_map(|(name, admitted)| {
admitted.entry.hash.map(|hash| SkillPin {
name: name.clone(),
hash,
})
})
.collect()
}
pub fn expand(&self, name: &str) -> Option<Expansion> {
if let Some(Admitted { entry, kept }) = self.skills.get(name) {
return Some(match kept {
Kept::Shelved => Expansion::Skill {
addr: entry.expansion.clone(),
package: entry.package.clone(),
},
Kept::Carried => Expansion::Said {
text: entry.expansion.clone(),
},
});
}
if let Some(mode) = self.mode {
let entry = catalog_entry(mode);
if entry.name == name {
return Some(Expansion::Said {
text: entry.expansion,
});
}
}
if name == crate::mode::DEV_ENTRY {
return Some(Expansion::Said {
text: crate::mode::dev_entry().expansion,
});
}
None
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
mod tests;