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#[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 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 pub fn card(&self) -> &SkillCard {
64 &self.card
65 }
66
67 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}