Skip to main content

sim_lib_skill/
callable.rs

1use std::sync::Arc;
2
3use sim_kernel::{
4    Args, CORE_FUNCTION_CLASS_ID, Callable, ClassRef, Cx, Error, Object, ObjectCompat, Result,
5    ShapeId, ShapeRef, Symbol, Value,
6};
7use sim_shape::check_value_report;
8
9#[cfg(any(feature = "cache", feature = "cassette"))]
10use crate::record::{
11    SkillAuditEntry, SkillCallState, cache_read_allowed, cache_write_allowed, call_key,
12    cassette_record_allowed, cassette_replay_allowed, value_from_recorded_expr,
13};
14use crate::{SkillCard, SkillEventSink, SkillTransport};
15
16/// Callable runtime object that invokes a single bound skill.
17///
18/// A `SkillCallable` pairs a [`SkillCard`] with the [`SkillTransport`] that
19/// runs it. Calling it checks the requested capabilities, validates arguments
20/// against the card's input shape, dispatches through the transport (honoring
21/// any cache/cassette policy when those features are enabled), and validates
22/// the result against the output shape. It is registered as an ordinary
23/// callable function value, so agents call a skill exactly like any other
24/// function.
25#[derive(Clone)]
26pub struct SkillCallable {
27    card: SkillCard,
28    transport: Arc<dyn SkillTransport>,
29    #[cfg(any(feature = "cache", feature = "cassette"))]
30    state: SkillCallState,
31    #[cfg(any(feature = "cache", feature = "cassette"))]
32    registry: Option<crate::SkillRegistry>,
33}
34
35impl SkillCallable {
36    /// Creates a callable for `card` that dispatches through `transport`.
37    pub fn new(card: SkillCard, transport: Arc<dyn SkillTransport>) -> Self {
38        Self {
39            card,
40            transport,
41            #[cfg(any(feature = "cache", feature = "cassette"))]
42            state: SkillCallState::default(),
43            #[cfg(any(feature = "cache", feature = "cassette"))]
44            registry: None,
45        }
46    }
47
48    #[cfg(any(feature = "cache", feature = "cassette"))]
49    pub(crate) fn new_bound(
50        card: SkillCard,
51        transport: Arc<dyn SkillTransport>,
52        registry: crate::SkillRegistry,
53    ) -> Self {
54        Self {
55            card,
56            transport,
57            state: SkillCallState::default(),
58            registry: Some(registry),
59        }
60    }
61
62    /// Returns the [`SkillCard`] this callable invokes.
63    pub fn card(&self) -> &SkillCard {
64        &self.card
65    }
66
67    /// Invokes the skill with already-evaluated `args`.
68    ///
69    /// Requires the skill's capabilities, checks the arguments against the
70    /// card's input shape, dispatches through the transport (applying cache
71    /// and cassette policy when those features are enabled), and checks the
72    /// result against the output shape. The optional `events` sink receives
73    /// any streaming events the transport emits during the call.
74    pub fn call_values(
75        &self,
76        cx: &mut Cx,
77        args: Vec<Value>,
78        events: Option<&mut dyn SkillEventSink>,
79    ) -> Result<Value> {
80        cx.require(&crate::skill_call_capability())?;
81        for capability in &self.card.capabilities {
82            cx.require(capability)?;
83        }
84        let args_value = self.check_args(cx, &args)?;
85        #[cfg(any(feature = "cache", feature = "cassette"))]
86        {
87            self.call_recorded(cx, args_value, events)
88        }
89        #[cfg(not(any(feature = "cache", feature = "cassette")))]
90        {
91            let value = self.transport.call(cx, &self.card, args_value, events)?;
92            self.check_result(cx, value)
93        }
94    }
95
96    #[cfg(any(feature = "cache", feature = "cassette"))]
97    fn call_recorded(
98        &self,
99        cx: &mut Cx,
100        args_value: Value,
101        events: Option<&mut dyn SkillEventSink>,
102    ) -> Result<Value> {
103        let key = call_key(cx, &self.card, &args_value)?;
104        let cache_enabled = self.card.policy.idempotent;
105        if cache_enabled
106            && cache_read_allowed(&self.card.policy.cache)
107            && let Some(expr) = self.state.cache_lookup(&key)?
108        {
109            self.record_audit("cache-hit", "hit", "disabled")?;
110            let value = value_from_recorded_expr(cx, expr)?;
111            return self.check_result(cx, value);
112        }
113        if cassette_replay_allowed(&self.card.policy.cassette)
114            && let Some(expr) = self.state.cassette_lookup(&key)?
115        {
116            self.record_audit("cassette-hit", "disabled", "hit")?;
117            let value = value_from_recorded_expr(cx, expr)?;
118            return self.check_result(cx, value);
119        }
120        if matches!(
121            self.card.policy.cassette,
122            crate::SkillCassetteMode::ReplayOnly
123        ) {
124            self.record_audit("cassette-miss", "disabled", "miss")?;
125            return Err(Error::Eval(format!(
126                "skill cassette replay miss for {}",
127                self.card.id
128            )));
129        }
130        let value = match self.transport.call(cx, &self.card, args_value, events) {
131            Ok(value) => value,
132            Err(error) => {
133                self.record_audit("error", "miss", "miss")?;
134                return Err(error);
135            }
136        };
137        let checked = match self.check_result(cx, value) {
138            Ok(value) => value,
139            Err(error) => {
140                self.record_audit("error", "miss", "miss")?;
141                return Err(error);
142            }
143        };
144        let expr = checked.object().as_expr(cx)?;
145        let mut cache_status = "disabled";
146        if cache_enabled && cache_write_allowed(&self.card.policy.cache) {
147            self.state.cache_store(key.clone(), expr.clone())?;
148            cache_status = "stored";
149        } else if cache_enabled && cache_read_allowed(&self.card.policy.cache) {
150            cache_status = "miss";
151        }
152        let mut cassette_status = "disabled";
153        if cassette_record_allowed(&self.card.policy.cassette) {
154            self.state.cassette_store(key, expr)?;
155            cassette_status = "stored";
156        } else if cassette_replay_allowed(&self.card.policy.cassette) {
157            cassette_status = "miss";
158        }
159        self.record_audit("live", cache_status, cassette_status)?;
160        Ok(checked)
161    }
162
163    #[cfg(any(feature = "cache", feature = "cassette"))]
164    fn record_audit(
165        &self,
166        outcome: &'static str,
167        cache: &'static str,
168        cassette: &'static str,
169    ) -> Result<()> {
170        let Some(registry) = &self.registry else {
171            return Ok(());
172        };
173        let mut capabilities = self
174            .card
175            .capabilities
176            .iter()
177            .map(|capability| capability.as_str().to_owned())
178            .collect::<Vec<_>>();
179        capabilities.sort();
180        registry.record_audit(SkillAuditEntry {
181            skill_id: self.card.id.clone(),
182            transport_kind: self.card.transport_kind.clone(),
183            outcome,
184            cache,
185            cassette,
186            privacy: self.card.policy.privacy.as_symbol().to_string(),
187            capabilities,
188        })
189    }
190
191    fn check_args(&self, cx: &mut Cx, args: &[Value]) -> Result<Value> {
192        let args_value = cx.factory().list(args.to_vec())?;
193        let matched = check_value_report(cx, &self.card.input_shape, args_value.clone())?;
194        if matched.accepted {
195            Ok(args_value)
196        } else {
197            Err(Error::WrongShape {
198                expected: shape_id(&self.card.input_shape),
199                diagnostics: matched.diagnostics,
200            })
201        }
202    }
203
204    fn check_result(&self, cx: &mut Cx, value: Value) -> Result<Value> {
205        let matched = check_value_report(cx, &self.card.output_shape, value.clone())?;
206        if matched.accepted {
207            Ok(value)
208        } else {
209            Err(Error::WrongShape {
210                expected: shape_id(&self.card.output_shape),
211                diagnostics: matched.diagnostics,
212            })
213        }
214    }
215}
216
217impl Object for SkillCallable {
218    fn display(&self, _cx: &mut Cx) -> Result<String> {
219        Ok(format!("#<skill-callable {}>", self.card.id))
220    }
221
222    fn as_any(&self) -> &dyn std::any::Any {
223        self
224    }
225}
226
227impl ObjectCompat for SkillCallable {
228    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
229        if let Some(value) = cx
230            .registry()
231            .class_by_symbol(&Symbol::qualified("core", "Function"))
232        {
233            return Ok(value.clone());
234        }
235        cx.factory().class_stub(
236            CORE_FUNCTION_CLASS_ID,
237            Symbol::qualified("core", "Function"),
238        )
239    }
240
241    fn as_callable(&self) -> Option<&dyn Callable> {
242        Some(self)
243    }
244}
245
246impl Callable for SkillCallable {
247    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
248        self.call_values(cx, args.into_vec(), None)
249    }
250
251    fn browse_args_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
252        Ok(Some(self.card.input_shape.clone()))
253    }
254
255    fn browse_result_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
256        Ok(Some(self.card.output_shape.clone()))
257    }
258}
259
260fn shape_id(shape: &ShapeRef) -> ShapeId {
261    shape
262        .object()
263        .as_shape()
264        .and_then(|shape| shape.id())
265        .unwrap_or(ShapeId(0))
266}