runtime/tools/
describe.rs1use std::sync::{Arc, Mutex};
12
13use kernel::{
14 AxCode, AxError, CostTier, Effect, Payload, RenderIntent, Temporal, Tool, ToolCall, ToolMeta,
15 ToolName, ToolOutcome,
16};
17use serde_json::Value;
18
19use crate::catalog::Catalog;
20
21const ACTION: &str = "describe";
22
23pub struct DescribeTool {
26 catalog: Arc<Mutex<Catalog>>,
27 meta: ToolMeta,
28}
29
30impl DescribeTool {
31 pub const NAME: &'static str = "describe";
34
35 pub fn new(catalog: Arc<Mutex<Catalog>>) -> Result<DescribeTool, AxError> {
39 Ok(DescribeTool {
40 catalog,
41 meta: ToolMeta {
42 name: ToolName::parse(Self::NAME)?,
43 disclosure: "Get the whole guide of a capability this building admits: a tool's \
44 description and input schema, or a skill's line. Give its whole \
45 name, or a few words to get the closest names."
46 .to_owned(),
47 params: Payload::of(&serde_json::json!({
48 "type": "object",
49 "properties": {
50 "name": { "type": "string", "description": "a name from the dormant \
51 index, or a few words of what you need" },
52 },
53 "required": ["name"],
54 }))?,
55 effect: Effect::Read,
56 cost_tier: CostTier::Free,
57 timeout: None,
58 render: RenderIntent::Generic,
59 temporal: Temporal::Timeless,
60 },
61 })
62 }
63}
64
65impl Tool for DescribeTool {
66 fn meta(&self) -> &ToolMeta {
67 &self.meta
68 }
69
70 fn invoke(&self, call: &ToolCall) -> Result<ToolOutcome, AxError> {
71 if call.name != self.meta.name {
72 return Err(AxError::failure(
73 AxCode::InvalidArgs,
74 ACTION,
75 format!("call routed to the wrong tool: {}", call.name.as_str()),
76 )
77 .with_recovery(format!(
78 "call `{}`, the name this tool answers to",
79 Self::NAME
80 )));
81 }
82 let asked = call
83 .args
84 .as_map()
85 .get("name")
86 .and_then(Value::as_str)
87 .ok_or_else(|| {
88 AxError::failure(
89 AxCode::InvalidArgs,
90 ACTION,
91 "missing string argument `name`",
92 )
93 .with_recovery("pass `name`: a capability's name, or a few words")
94 })?;
95 let guide = self
96 .catalog
97 .lock()
98 .map_err(|_| poisoned())?
99 .describe(asked)?;
100 Ok(ToolOutcome {
101 result: Payload::of(&serde_json::json!({ "text": guide }))?,
102 attachments: Vec::new(),
103 })
104 }
105}
106
107pub(super) fn poisoned() -> AxError {
110 AxError::failure(
111 AxCode::StorageFatal,
112 ACTION,
113 "the catalog was left locked by a thread that died",
114 )
115 .with_recovery(
116 "end this run and resume it: the catalog cannot be trusted again inside a process \
117 where a thread died holding it",
118 )
119}