Skip to main content

sim_lib_agent/
tools.rs

1use crate::util::{
2    expr_to_value, installed_codecs, keyword, literal_expr, parse_capabilities_expr,
3    shape_from_expr, shape_id, string_literal, symbol_from_eval, symbol_from_value, symbol_of,
4};
5use crate::{AGENT_LIB_ID, Component, ComponentKind, TOOL_EXPORT_KIND};
6use sim_kernel::{
7    Args, CORE_FUNCTION_CLASS_ID, Callable, CapabilityName, ClassRef, Cx, Error, EvalReply,
8    ExportKind, ExportRecord, ExportState, Expr, Object, Result, ShapeRef, Symbol, Value,
9};
10use sim_lib_server::{
11    EvalSite, FrameKind, ServerAddress, ServerFrame, eval_request_from_frame,
12    server_frame_from_reply,
13};
14use sim_shape::{ShapeReport, check_value_report};
15use std::{any::Any, sync::Arc};
16
17#[derive(Clone)]
18/// A callable tool an agent can invoke, with shape-checked arguments and
19/// results, a category, and required capabilities.
20pub struct Tool {
21    /// The tool's name.
22    pub symbol: Symbol,
23    /// Human-readable description of what the tool does.
24    pub description: String,
25    /// Shape the argument list is checked against before invocation.
26    pub args_shape: ShapeRef,
27    /// Optional shape the result is checked against after invocation.
28    pub result_shape: Option<ShapeRef>,
29    /// Category grouping this tool with related tools.
30    pub category: Symbol,
31    /// Capabilities required to invoke the tool.
32    pub capabilities: Vec<CapabilityName>,
33    /// The underlying callable that implements the tool.
34    pub function: Value,
35    pub(crate) address: ServerAddress,
36    pub(crate) codecs: Vec<Symbol>,
37}
38
39impl Tool {
40    fn args_match(&self, cx: &mut Cx, args: &[Value]) -> Result<ShapeReport> {
41        let args_value = cx.factory().list(args.to_vec())?;
42        check_value_report(cx, &self.args_shape, args_value)
43    }
44
45    fn check_result_shape(&self, cx: &mut Cx, value: Value) -> Result<Value> {
46        let Some(result_shape) = &self.result_shape else {
47            return Ok(value);
48        };
49        let matched = check_value_report(cx, result_shape, value.clone())?;
50        if matched.accepted {
51            Ok(value)
52        } else {
53            Err(Error::WrongShape {
54                expected: shape_id(result_shape),
55                diagnostics: matched.diagnostics,
56            })
57        }
58    }
59
60    pub(crate) fn call_values(&self, cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
61        for capability in &self.capabilities {
62            cx.require(capability)?;
63        }
64        let matched = self.args_match(cx, &args)?;
65        if !matched.accepted {
66            return Err(Error::WrongShape {
67                expected: shape_id(&self.args_shape),
68                diagnostics: matched.diagnostics,
69            });
70        }
71        let value = cx.call_value(self.function.clone(), Args::new(args))?;
72        self.check_result_shape(cx, value)
73    }
74
75    pub(crate) fn metadata_entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
76        let capabilities = cx.factory().list(
77            self.capabilities
78                .iter()
79                .map(|capability| cx.factory().string(capability.as_str().to_owned()))
80                .collect::<Result<Vec<_>>>()?,
81        )?;
82        let result_shape = match &self.result_shape {
83            Some(shape) => shape.clone(),
84            None => cx.factory().nil()?,
85        };
86        Ok(vec![
87            (
88                Symbol::new("kind"),
89                cx.factory().symbol(tool_export_kind_symbol())?,
90            ),
91            (
92                Symbol::new("name"),
93                cx.factory().symbol(self.symbol.clone())?,
94            ),
95            (
96                Symbol::new("description"),
97                cx.factory().string(self.description.clone())?,
98            ),
99            (Symbol::new("args"), self.args_shape.clone()),
100            (Symbol::new("result"), result_shape),
101            (
102                Symbol::new("category"),
103                cx.factory().symbol(self.category.clone())?,
104            ),
105            (Symbol::new("capabilities"), capabilities),
106        ])
107    }
108
109    pub(crate) fn model_descriptor_expr(&self, cx: &mut Cx) -> Result<Expr> {
110        let args = self.args_shape.object().as_expr(cx)?;
111        let result = self
112            .result_shape
113            .as_ref()
114            .map(|shape| shape.object().as_expr(cx))
115            .transpose()?
116            .unwrap_or(Expr::Nil);
117        let capabilities = self
118            .capabilities
119            .iter()
120            .map(|capability| Expr::String(capability.as_str().to_owned()))
121            .collect();
122        Ok(Expr::Map(vec![
123            (
124                Expr::Symbol(Symbol::new("name")),
125                Expr::Symbol(self.symbol.clone()),
126            ),
127            (
128                Expr::Symbol(Symbol::new("description")),
129                Expr::String(self.description.clone()),
130            ),
131            (Expr::Symbol(Symbol::new("args")), args),
132            (Expr::Symbol(Symbol::new("result")), result),
133            (
134                Expr::Symbol(Symbol::new("category")),
135                Expr::Symbol(self.category.clone()),
136            ),
137            (
138                Expr::Symbol(Symbol::new("capabilities")),
139                Expr::List(capabilities),
140            ),
141        ]))
142    }
143
144    fn metadata_value(&self, cx: &mut Cx) -> Result<Value> {
145        let entries = self.metadata_entries(cx)?;
146        cx.factory().table(entries)
147    }
148}
149
150impl Object for Tool {
151    fn display(&self, _cx: &mut Cx) -> Result<String> {
152        Ok(format!("#<tool {}>", self.symbol))
153    }
154
155    fn as_any(&self) -> &dyn Any {
156        self
157    }
158}
159
160impl sim_kernel::ObjectCompat for Tool {
161    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
162        if let Some(value) = cx
163            .registry()
164            .class_by_symbol(&Symbol::qualified("core", "Function"))
165        {
166            return Ok(value.clone());
167        }
168        cx.factory().class_stub(
169            CORE_FUNCTION_CLASS_ID,
170            Symbol::qualified("core", "Function"),
171        )
172    }
173    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
174        self.as_table(cx)?.object().as_expr(cx)
175    }
176    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
177        self.metadata_value(cx)
178    }
179    fn as_callable(&self) -> Option<&dyn Callable> {
180        Some(self)
181    }
182}
183
184impl Callable for Tool {
185    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
186        self.call_values(cx, args.into_vec())
187    }
188}
189
190impl EvalSite for Tool {
191    fn site_kind(&self) -> &'static str {
192        TOOL_EXPORT_KIND
193    }
194
195    fn address(&self) -> &ServerAddress {
196        &self.address
197    }
198
199    fn codecs(&self) -> &[Symbol] {
200        &self.codecs
201    }
202
203    fn answer(&self, cx: &mut Cx, frame: ServerFrame) -> Result<ServerFrame> {
204        if frame.kind != FrameKind::Request {
205            return Err(Error::Eval(format!(
206                "tool {} cannot answer frame kind {}",
207                self.symbol,
208                frame.kind.as_symbol()
209            )));
210        }
211        let consistency = frame.envelope.consistency;
212        let request = eval_request_from_frame(cx, &frame)?;
213        let args = expr_args_to_values(cx, request.expr)?;
214        let value = self.call_values(cx, args)?;
215        let diagnostics = cx.take_diagnostics();
216        server_frame_from_reply(
217            cx,
218            &frame.codec,
219            EvalReply {
220                value,
221                diagnostics,
222                trace: None,
223            },
224            consistency,
225        )
226    }
227
228    fn as_any(&self) -> &dyn Any {
229        self
230    }
231}
232
233impl Component for Tool {
234    fn kind(&self) -> ComponentKind {
235        ComponentKind::Tool
236    }
237
238    fn name(&self) -> &Symbol {
239        &self.symbol
240    }
241
242    fn capabilities(&self) -> &[CapabilityName] {
243        &self.capabilities
244    }
245
246    fn reflect(&self, cx: &mut Cx) -> Result<Expr> {
247        self.metadata_value(cx)?.object().as_expr(cx)
248    }
249}
250
251#[derive(Default)]
252pub(crate) struct ToolFilter {
253    pub(crate) category: Option<Symbol>,
254    pub(crate) agent: Option<Symbol>,
255}
256
257pub(crate) fn define_tool(cx: &mut Cx, exprs: Vec<Expr>) -> Result<Value> {
258    let Some((name_expr, rest)) = exprs.split_first() else {
259        return Err(Error::Eval(
260            "agent/defun expects a name, metadata options, and a callable body".to_owned(),
261        ));
262    };
263    let name = symbol_of(name_expr, "agent/defun expects a symbol name")?;
264    if rest.len() < 2 {
265        return Err(Error::Eval(
266            "agent/defun expects metadata options followed by a callable body".to_owned(),
267        ));
268    }
269    let (body_expr, option_exprs) = rest
270        .split_last()
271        .ok_or_else(|| Error::Eval("agent/defun requires a callable body".to_owned()))?;
272    if !option_exprs.len().is_multiple_of(2) {
273        return Err(Error::Eval(
274            "agent/defun options must be key/value pairs".to_owned(),
275        ));
276    }
277
278    let mut description = None;
279    let mut args_shape = None;
280    let mut result_shape = None;
281    let mut category = None;
282    let mut capabilities = Vec::new();
283
284    for pair in option_exprs.chunks(2) {
285        let key = keyword(&pair[0])?;
286        match key.as_str() {
287            "description" => {
288                description = Some(string_literal(
289                    &pair[1],
290                    "agent/defun :description expects a string",
291                )?);
292            }
293            "args" => {
294                args_shape = Some(shape_from_expr(cx, &pair[1], &name, "args")?);
295            }
296            "result" => {
297                result_shape = Some(shape_from_expr(cx, &pair[1], &name, "result")?);
298            }
299            "category" => {
300                category = Some(symbol_of(
301                    literal_expr(&pair[1]),
302                    "agent/defun :category expects a symbol",
303                )?);
304            }
305            "capable" => {
306                capabilities = parse_capabilities_expr(literal_expr(&pair[1]))?;
307            }
308            other => return Err(Error::Eval(format!("agent/defun: unknown option :{other}"))),
309        }
310    }
311
312    let function = cx.eval_expr(body_expr.clone())?;
313    if function.object().as_callable().is_none() {
314        return Err(Error::TypeMismatch {
315            expected: "callable",
316            found: "non-callable",
317        });
318    }
319
320    let tool = Arc::new(Tool {
321        symbol: name.clone(),
322        description: description
323            .ok_or_else(|| Error::Eval("agent/defun requires :description".to_owned()))?,
324        args_shape: args_shape
325            .ok_or_else(|| Error::Eval("agent/defun requires :args".to_owned()))?,
326        result_shape,
327        category: category.unwrap_or_else(|| Symbol::new("general")),
328        capabilities,
329        function,
330        address: ServerAddress::Local,
331        codecs: installed_codecs(cx),
332    });
333    let value = cx.factory().opaque(tool.clone())?;
334    register_tool(cx, tool, value.clone())?;
335    Ok(value)
336}
337
338pub(crate) fn parse_tools_exprs(cx: &mut Cx, exprs: Vec<Expr>) -> Result<Value> {
339    if exprs.is_empty() {
340        return list_tools_value(cx, ToolFilter::default());
341    }
342    if !exprs.len().is_multiple_of(2) {
343        return Err(Error::Eval(
344            "agent/tools options must be key/value pairs".to_owned(),
345        ));
346    }
347    let mut filter = ToolFilter::default();
348    for pair in exprs.chunks(2) {
349        let key = keyword(&pair[0])?;
350        match key.as_str() {
351            "category" => {
352                filter.category = Some(symbol_from_eval(
353                    cx,
354                    &pair[1],
355                    "agent/tools :category expects a symbol",
356                )?);
357            }
358            "agent" => {
359                filter.agent = Some(symbol_from_eval(
360                    cx,
361                    &pair[1],
362                    "agent/tools :agent expects a symbol",
363                )?);
364            }
365            other => return Err(Error::Eval(format!("agent/tools: unknown option :{other}"))),
366        }
367    }
368    list_tools_value(cx, filter)
369}
370
371pub(crate) fn call_tool_value(cx: &mut Cx, args: Args) -> Result<Value> {
372    let Some(target) = args.values().first() else {
373        return Err(Error::Eval(
374            "agent/call-tool expects a tool name or tool value".to_owned(),
375        ));
376    };
377    let tool_value = resolve_tool_value(cx, target.clone())?;
378    let tool = tool_value
379        .object()
380        .downcast_ref::<Tool>()
381        .ok_or(Error::TypeMismatch {
382            expected: "tool",
383            found: "non-tool",
384        })?;
385    tool.call_values(cx, args.values()[1..].to_vec())
386}
387
388pub(crate) fn list_tools_value(cx: &mut Cx, filter: ToolFilter) -> Result<Value> {
389    let tools = registered_tools(cx)?
390        .into_iter()
391        .filter(|tool| {
392            filter
393                .category
394                .as_ref()
395                .is_none_or(|category| &tool.category == category)
396        })
397        .filter(|_| filter.agent.is_none())
398        .map(|tool| tool.metadata_value(cx))
399        .collect::<Result<Vec<_>>>()?;
400    cx.factory().list(tools)
401}
402
403pub(crate) fn register_tool(cx: &mut Cx, tool: Arc<Tool>, value: Value) -> Result<()> {
404    let lib = Symbol::new(AGENT_LIB_ID);
405    cx.registry_mut()
406        .register_value_for_lib(&lib, tool.symbol.clone(), value)?;
407    cx.registry_mut().append_export_record(
408        &lib,
409        ExportRecord {
410            kind: tool_export_kind(),
411            symbol: tool.symbol.clone(),
412            state: ExportState::Resolved {
413                id: sim_kernel::RuntimeId::Value,
414            },
415        },
416    )?;
417    Ok(())
418}
419
420pub(crate) fn resolve_tool_by_symbol(cx: &mut Cx, symbol: &Symbol) -> Result<Tool> {
421    let tool_value = cx.resolve_value(symbol)?;
422    tool_value
423        .object()
424        .downcast_ref::<Tool>()
425        .cloned()
426        .ok_or(Error::TypeMismatch {
427            expected: "tool",
428            found: "non-tool",
429        })
430}
431
432fn resolve_tool_value(cx: &mut Cx, value: Value) -> Result<Value> {
433    if value.object().downcast_ref::<Tool>().is_some() {
434        return Ok(value);
435    }
436    let symbol = symbol_from_value(
437        cx,
438        value,
439        "agent/call-tool expects a tool symbol or tool value",
440    )?;
441    let tool_value = cx.resolve_value(&symbol)?;
442    if tool_value.object().downcast_ref::<Tool>().is_some() {
443        Ok(tool_value)
444    } else {
445        Err(Error::TypeMismatch {
446            expected: "tool",
447            found: "non-tool",
448        })
449    }
450}
451
452fn registered_tools(cx: &Cx) -> Result<Vec<Arc<Tool>>> {
453    let loaded = cx
454        .registry()
455        .lib(&Symbol::new(AGENT_LIB_ID))
456        .ok_or_else(|| Error::Lib("agent lib is not installed".to_owned()))?;
457    let tools = loaded
458        .exports
459        .iter()
460        .filter(|record| record.kind == tool_export_kind())
461        .filter_map(|record| cx.registry().value_by_symbol(&record.symbol))
462        .filter_map(|value| value.object().downcast_ref::<Tool>())
463        .cloned()
464        .map(Arc::new)
465        .collect();
466    Ok(tools)
467}
468
469fn expr_args_to_values(cx: &mut Cx, expr: Expr) -> Result<Vec<Value>> {
470    match expr {
471        Expr::List(items) | Expr::Vector(items) => items
472            .iter()
473            .map(|item| expr_to_value(cx, item))
474            .collect::<Result<Vec<_>>>(),
475        other => Ok(vec![expr_to_value(cx, &other)?]),
476    }
477}
478
479fn tool_export_kind_symbol() -> Symbol {
480    Symbol::new(TOOL_EXPORT_KIND)
481}
482
483pub(crate) fn tool_export_kind() -> ExportKind {
484    ExportKind::new(tool_export_kind_symbol())
485}