Skip to main content

sim_lib_skill/
ops.rs

1use std::sync::Arc;
2
3use sim_kernel::{
4    Args, Callable, CapabilityName, Cx, Error, Export, Object, ObjectCompat, Result, Symbol, Value,
5};
6use sim_shape::{AnyShape, ListShape, Shape, shape_value};
7
8use crate::registry::{card_from_value, transport_from_value};
9
10/// Which `skill/*` operation a [`SkillFunction`] dispatches.
11#[derive(Clone, Copy)]
12pub enum SkillFunctionKind {
13    /// `skill/install`: register a transport and bind its cards.
14    Install,
15    /// `skill/bind`: bind one or more cards to installed transports.
16    Bind,
17    /// `skill/list`: list the bound cards.
18    List,
19    /// `skill/card`: look up a single card by id or symbol.
20    Card,
21    /// `skill/call`: call a bound skill by id or symbol.
22    Call,
23    /// `skill/audit`: return the recorded cache/cassette audit log.
24    #[cfg(any(feature = "cache", feature = "cassette"))]
25    Audit,
26    /// `skill/as-tool`: project a skill as an agent tool.
27    #[cfg(feature = "agent")]
28    AsTool,
29    /// `skill/mcp-tools`: list skills as MCP tool descriptors.
30    #[cfg(feature = "mcp")]
31    McpTools,
32    /// `skill/mcp-call`: invoke a skill through the MCP call surface.
33    #[cfg(feature = "mcp")]
34    McpCall,
35    /// `skill/openai-tool`: project a skill as an OpenAI tool descriptor.
36    #[cfg(feature = "openai")]
37    OpenAiTool,
38    /// `skill/openai-tools`: project skills as OpenAI tool descriptors.
39    #[cfg(feature = "openai")]
40    OpenAiTools,
41    /// `skill/as-runner`: expose a model skill as an agent runner.
42    #[cfg(feature = "runner")]
43    AsRunner,
44    /// `skill/serve-mcp`: serve bound skills over MCP.
45    #[cfg(feature = "serve")]
46    ServeMcp,
47}
48
49impl SkillFunctionKind {
50    /// Returns the qualified symbol the operation is bound under.
51    pub fn symbol(self) -> Symbol {
52        match self {
53            SkillFunctionKind::Install => skill_install_symbol(),
54            SkillFunctionKind::Bind => skill_bind_symbol(),
55            SkillFunctionKind::List => skill_list_symbol(),
56            SkillFunctionKind::Card => skill_card_symbol(),
57            SkillFunctionKind::Call => skill_call_symbol(),
58            #[cfg(any(feature = "cache", feature = "cassette"))]
59            SkillFunctionKind::Audit => skill_audit_symbol(),
60            #[cfg(feature = "agent")]
61            SkillFunctionKind::AsTool => crate::agent::skill_as_tool_symbol(),
62            #[cfg(feature = "mcp")]
63            SkillFunctionKind::McpTools => crate::mcp::skill_mcp_tools_symbol(),
64            #[cfg(feature = "mcp")]
65            SkillFunctionKind::McpCall => crate::mcp::skill_mcp_call_symbol(),
66            #[cfg(feature = "openai")]
67            SkillFunctionKind::OpenAiTool => crate::openai::skill_openai_tool_symbol(),
68            #[cfg(feature = "openai")]
69            SkillFunctionKind::OpenAiTools => crate::openai::skill_openai_tools_symbol(),
70            #[cfg(feature = "runner")]
71            SkillFunctionKind::AsRunner => crate::runner::skill_as_runner_symbol(),
72            #[cfg(feature = "serve")]
73            SkillFunctionKind::ServeMcp => crate::serve::skill_serve_mcp_symbol(),
74        }
75    }
76}
77
78/// Callable runtime object implementing one `skill/*` operation.
79#[derive(Clone)]
80pub struct SkillFunction {
81    kind: SkillFunctionKind,
82}
83
84impl SkillFunction {
85    /// Creates a function value for the given operation `kind`.
86    pub fn new(kind: SkillFunctionKind) -> Self {
87        Self { kind }
88    }
89
90    /// Returns the symbol this function is bound under.
91    pub fn symbol(&self) -> Symbol {
92        self.kind.symbol()
93    }
94
95    /// Creates a shared function object for the given operation `kind`.
96    pub fn value(kind: SkillFunctionKind) -> Arc<Self> {
97        Arc::new(Self::new(kind))
98    }
99}
100
101impl Object for SkillFunction {
102    fn display(&self, _cx: &mut Cx) -> Result<String> {
103        Ok(format!("#<function {}>", self.symbol()))
104    }
105
106    fn as_any(&self) -> &dyn std::any::Any {
107        self
108    }
109}
110
111impl ObjectCompat for SkillFunction {
112    fn as_callable(&self) -> Option<&dyn Callable> {
113        Some(self)
114    }
115}
116
117impl Callable for SkillFunction {
118    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
119        match self.kind {
120            SkillFunctionKind::Install => install(cx, args),
121            SkillFunctionKind::Bind => bind(cx, args),
122            SkillFunctionKind::List => list(cx),
123            SkillFunctionKind::Card => card(cx, args),
124            SkillFunctionKind::Call => call(cx, args),
125            #[cfg(any(feature = "cache", feature = "cassette"))]
126            SkillFunctionKind::Audit => audit(cx),
127            #[cfg(feature = "agent")]
128            SkillFunctionKind::AsTool => crate::agent::as_tool(cx, args),
129            #[cfg(feature = "mcp")]
130            SkillFunctionKind::McpTools => crate::mcp::ops::mcp_tools(cx, args),
131            #[cfg(feature = "mcp")]
132            SkillFunctionKind::McpCall => crate::mcp::ops::mcp_call(cx, args),
133            #[cfg(feature = "openai")]
134            SkillFunctionKind::OpenAiTool => crate::openai::openai_tool(cx, args),
135            #[cfg(feature = "openai")]
136            SkillFunctionKind::OpenAiTools => crate::openai::openai_tools(cx, args),
137            #[cfg(feature = "runner")]
138            SkillFunctionKind::AsRunner => crate::runner::as_runner(cx, args),
139            #[cfg(feature = "serve")]
140            SkillFunctionKind::ServeMcp => crate::serve::serve_mcp(cx, args),
141        }
142    }
143
144    fn browse_args_shape(&self, _cx: &mut Cx) -> Result<Option<sim_kernel::ShapeRef>> {
145        let shape: Arc<dyn Shape> = match self.kind {
146            SkillFunctionKind::List => Arc::new(ListShape::new(Vec::new())),
147            #[cfg(any(feature = "cache", feature = "cassette"))]
148            SkillFunctionKind::Audit => Arc::new(ListShape::new(Vec::new())),
149            #[cfg(feature = "mcp")]
150            SkillFunctionKind::McpTools => Arc::new(ListShape::new(Vec::new())),
151            SkillFunctionKind::Card => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
152            SkillFunctionKind::Install | SkillFunctionKind::Bind | SkillFunctionKind::Call => {
153                Arc::new(AnyShape)
154            }
155            #[cfg(feature = "agent")]
156            SkillFunctionKind::AsTool => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
157            #[cfg(feature = "mcp")]
158            SkillFunctionKind::McpCall => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
159            #[cfg(feature = "openai")]
160            SkillFunctionKind::OpenAiTool => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
161            #[cfg(feature = "openai")]
162            SkillFunctionKind::OpenAiTools => Arc::new(AnyShape),
163            #[cfg(feature = "runner")]
164            SkillFunctionKind::AsRunner => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
165            #[cfg(feature = "serve")]
166            SkillFunctionKind::ServeMcp => Arc::new(AnyShape),
167        };
168        Ok(Some(shape_value(
169            Symbol::qualified(self.symbol().to_string(), "args"),
170            shape,
171        )))
172    }
173
174    fn browse_result_shape(&self, _cx: &mut Cx) -> Result<Option<sim_kernel::ShapeRef>> {
175        Ok(Some(shape_value(
176            Symbol::qualified(self.symbol().to_string(), "result"),
177            Arc::new(AnyShape),
178        )))
179    }
180}
181
182/// Returns the library exports: every `skill/*` function plus the registry
183/// value, with the feature-gated operations included when their feature is on.
184pub fn skill_exports() -> Vec<Export> {
185    let symbols = vec![
186        skill_install_symbol(),
187        skill_bind_symbol(),
188        skill_list_symbol(),
189        skill_card_symbol(),
190        skill_call_symbol(),
191    ];
192    #[cfg(any(feature = "cache", feature = "cassette"))]
193    let symbols = {
194        let mut symbols = symbols;
195        symbols.push(skill_audit_symbol());
196        symbols
197    };
198    #[cfg(feature = "agent")]
199    let symbols = {
200        let mut symbols = symbols;
201        symbols.push(crate::agent::skill_as_tool_symbol());
202        symbols
203    };
204    #[cfg(feature = "mcp")]
205    let symbols = {
206        let mut symbols = symbols;
207        symbols.extend([
208            crate::mcp::skill_mcp_tools_symbol(),
209            crate::mcp::skill_mcp_call_symbol(),
210        ]);
211        symbols
212    };
213    #[cfg(feature = "openai")]
214    let symbols = {
215        let mut symbols = symbols;
216        symbols.extend([
217            crate::openai::skill_openai_tool_symbol(),
218            crate::openai::skill_openai_tools_symbol(),
219        ]);
220        symbols
221    };
222    #[cfg(feature = "runner")]
223    let symbols = {
224        let mut symbols = symbols;
225        symbols.push(crate::runner::skill_as_runner_symbol());
226        symbols
227    };
228    #[cfg(feature = "serve")]
229    let symbols = {
230        let mut symbols = symbols;
231        symbols.push(crate::serve::skill_serve_mcp_symbol());
232        symbols
233    };
234    symbols
235        .into_iter()
236        .map(|symbol| Export::Function {
237            symbol,
238            function_id: None,
239        })
240        .chain(std::iter::once(Export::Value {
241            symbol: crate::skill_registry_symbol(),
242        }))
243        .collect()
244}
245
246fn install(cx: &mut Cx, args: Args) -> Result<Value> {
247    cx.require(&skill_install_capability())?;
248    let values = args.into_vec();
249    let Some((transport_value, card_values)) = values.split_first() else {
250        return Err(Error::Eval(
251            "skill/install expects a transport and one or more SkillCard values".to_owned(),
252        ));
253    };
254    let registry = crate::skill_registry(cx)?;
255    registry.install_transport(transport_from_value(transport_value)?)?;
256    bind_cards(cx, &registry, card_values)
257}
258
259fn bind(cx: &mut Cx, args: Args) -> Result<Value> {
260    cx.require(&skill_bind_capability())?;
261    let values = args.into_vec();
262    let registry = crate::skill_registry(cx)?;
263    bind_cards(cx, &registry, &values)
264}
265
266fn bind_cards(
267    cx: &mut Cx,
268    registry: &crate::SkillRegistry,
269    card_values: &[Value],
270) -> Result<Value> {
271    if card_values.is_empty() {
272        return Err(Error::Eval(
273            "skill/bind expects one or more SkillCard values".to_owned(),
274        ));
275    }
276    let cards = card_values
277        .iter()
278        .map(|value| card_from_value(cx, value))
279        .collect::<Result<Vec<_>>>()?;
280    for card in &cards {
281        registry.bind_card(cx, card.clone())?;
282    }
283    let values = cards
284        .iter()
285        .map(|card| card.value(cx))
286        .collect::<Result<Vec<_>>>()?;
287    cx.factory().list(values)
288}
289
290fn list(cx: &mut Cx) -> Result<Value> {
291    let registry = crate::skill_registry(cx)?;
292    let cards = registry.cards()?;
293    let values = cards
294        .iter()
295        .map(|card| card.value(cx))
296        .collect::<Result<Vec<_>>>()?;
297    cx.factory().list(values)
298}
299
300fn card(cx: &mut Cx, args: Args) -> Result<Value> {
301    let target = one_arg(args, "skill/card expects a skill id or symbol")?;
302    let registry = crate::skill_registry(cx)?;
303    let found = match target_from_value(cx, target)? {
304        SkillTarget::Id(id) => registry.card_by_id(&id)?,
305        SkillTarget::Symbol(symbol) => registry.card_by_symbol(&symbol)?,
306    };
307    match found {
308        Some(card) => card.value(cx),
309        None => cx.factory().nil(),
310    }
311}
312
313fn call(cx: &mut Cx, args: Args) -> Result<Value> {
314    let values = args.into_vec();
315    let Some((target, rest)) = values.split_first() else {
316        return Err(Error::Eval(
317            "skill/call expects a skill id or symbol followed by arguments".to_owned(),
318        ));
319    };
320    let registry = crate::skill_registry(cx)?;
321    let card = match target_from_value(cx, target.clone())? {
322        SkillTarget::Id(id) => registry.card_by_id(&id)?,
323        SkillTarget::Symbol(symbol) => registry.card_by_symbol(&symbol)?,
324    }
325    .ok_or_else(|| Error::Eval("unknown skill".to_owned()))?;
326    cx.call_function(&card.symbol, Args::new(rest.to_vec()))
327}
328
329#[cfg(any(feature = "cache", feature = "cassette"))]
330fn audit(cx: &mut Cx) -> Result<Value> {
331    cx.require(&skill_audit_capability())?;
332    crate::skill_registry(cx)?.audit_values(cx)
333}
334
335fn one_arg(args: Args, message: &'static str) -> Result<Value> {
336    let mut values = args.into_vec();
337    if values.len() == 1 {
338        Ok(values.remove(0))
339    } else {
340        Err(Error::Eval(message.to_owned()))
341    }
342}
343
344enum SkillTarget {
345    Id(String),
346    Symbol(Symbol),
347}
348
349fn target_from_value(cx: &mut Cx, value: Value) -> Result<SkillTarget> {
350    match value.object().as_expr(cx)? {
351        sim_kernel::Expr::String(id) => Ok(SkillTarget::Id(id)),
352        sim_kernel::Expr::Symbol(symbol) => Ok(SkillTarget::Symbol(symbol)),
353        _ => Err(Error::TypeMismatch {
354            expected: "skill id or symbol",
355            found: "invalid target",
356        }),
357    }
358}
359
360/// Returns the symbol for the `skill/install` operation.
361pub fn skill_install_symbol() -> Symbol {
362    Symbol::qualified("skill", "install")
363}
364
365/// Returns the symbol for the `skill/bind` operation.
366pub fn skill_bind_symbol() -> Symbol {
367    Symbol::qualified("skill", "bind")
368}
369
370/// Returns the symbol for the `skill/list` operation.
371pub fn skill_list_symbol() -> Symbol {
372    Symbol::qualified("skill", "list")
373}
374
375/// Returns the symbol for the `skill/card` operation.
376pub fn skill_card_symbol() -> Symbol {
377    Symbol::qualified("skill", "card")
378}
379
380/// Returns the symbol for the `skill/call` operation.
381pub fn skill_call_symbol() -> Symbol {
382    Symbol::qualified("skill", "call")
383}
384
385/// Returns the symbol for the `skill/audit` operation.
386#[cfg(any(feature = "cache", feature = "cassette"))]
387pub fn skill_audit_symbol() -> Symbol {
388    Symbol::qualified("skill", "audit")
389}
390
391/// Returns the capability required to call `skill/install`.
392pub fn skill_install_capability() -> CapabilityName {
393    CapabilityName::new("skill.install")
394}
395
396/// Returns the capability required to call `skill/bind`.
397pub fn skill_bind_capability() -> CapabilityName {
398    CapabilityName::new("skill.bind")
399}
400
401/// Returns the broad capability required to call any skill.
402pub fn skill_call_capability() -> CapabilityName {
403    CapabilityName::new("skill.call")
404}
405
406/// Returns the capability required to serve skills over a transport.
407pub fn skill_serve_capability() -> CapabilityName {
408    CapabilityName::new("skill.serve")
409}
410
411/// Returns the capability required to read the skill audit log.
412#[cfg(any(feature = "cache", feature = "cassette"))]
413pub fn skill_audit_capability() -> CapabilityName {
414    CapabilityName::new("skill.audit")
415}
416
417/// Returns the per-skill call capability for the skill with the given `id`.
418pub fn skill_specific_call_capability(id: &str) -> CapabilityName {
419    CapabilityName::new(format!("skill.{id}.call"))
420}