1use crate::{
7 capability::CapabilityName,
8 datum::Datum,
9 datum_store::DatumStore,
10 env::Cx,
11 error::{Error, Result},
12 expr::NumberLiteral,
13 id::Symbol,
14 ref_id::{ContentId, Coordinate, HandleId, Ref},
15 term::OpKey,
16};
17
18pub const EFFECT_REPLAY_VERSION: &str = "sim6-effect-replay-v1";
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Effect {
24 pub id: Ref,
26 pub kind: Symbol,
28 pub subject: Ref,
30 pub input: Ref,
32 pub result_shape: Ref,
34 pub resume_op: OpKey,
36 pub abort_op: OpKey,
38 pub requires: Vec<CapabilityName>,
40 pub replay_key: Option<ContentId>,
42}
43
44impl Effect {
45 pub fn new(
47 id: HandleId,
48 kind: Symbol,
49 subject: Ref,
50 input: Ref,
51 result_shape: Ref,
52 resume_op: OpKey,
53 abort_op: OpKey,
54 ) -> Self {
55 Self {
56 id: Ref::Handle(id),
57 kind,
58 subject,
59 input,
60 result_shape,
61 resume_op,
62 abort_op,
63 requires: Vec::new(),
64 replay_key: None,
65 }
66 }
67
68 pub fn with_id(mut self, id: Ref) -> Self {
70 self.id = id;
71 self
72 }
73
74 pub fn requiring(mut self, capability: CapabilityName) -> Self {
76 self.requires.push(capability);
77 self
78 }
79
80 pub fn with_requirements(mut self, requires: Vec<CapabilityName>) -> Self {
82 self.requires = requires;
83 self
84 }
85
86 pub fn with_replay_key(mut self, implementation: Option<Ref>) -> Result<Self> {
88 self.replay_key = Some(effect_replay_key(&self, implementation)?);
89 Ok(self)
90 }
91
92 pub fn ensure_replay_key(&mut self, implementation: Option<Ref>) -> Result<ContentId> {
94 if let Some(key) = &self.replay_key {
95 return Ok(key.clone());
96 }
97 let key = effect_replay_key(self, implementation)?;
98 self.replay_key = Some(key.clone());
99 Ok(key)
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct EffectRecord {
106 pub effect: Ref,
108 pub requested_event: Ref,
110 pub resolved_event: Option<Ref>,
112 pub result: Option<Ref>,
114 pub aborted: bool,
116}
117
118pub fn resolve_effect<F>(cx: &mut Cx, mut effect: Effect, perform: F) -> Result<Ref>
154where
155 F: FnOnce(&mut Cx, &Effect) -> Result<Ref>,
156{
157 let preimage = effect_replay_preimage(&effect, None);
158 let replay_key = match effect.replay_key.clone() {
159 Some(key) => key,
160 None => cx.datum_store_mut().intern(preimage)?,
161 };
162 effect.replay_key = Some(replay_key.clone());
163
164 let cassette_result = cx.with_effect_ledger(|cx, ledger| {
165 ledger.record_requested(cx.datum_store_mut(), effect.clone())?;
166 Ok(ledger.cassette_result(&replay_key).cloned())
167 })?;
168
169 if let Err(err) = cx.require_all(&effect.requires) {
170 record_effect_failure(cx, effect.id.clone(), &err)?;
171 return Err(err);
172 }
173
174 if let Some(result) = cassette_result {
175 cx.with_effect_ledger(|cx, ledger| {
176 ledger.record_resolved(cx.datum_store_mut(), effect.id.clone(), result.clone())?;
177 Ok(())
178 })?;
179 return Ok(result);
180 }
181
182 match perform(cx, &effect) {
183 Ok(result) => {
184 cx.with_effect_ledger(|cx, ledger| {
185 ledger.record_resolved(cx.datum_store_mut(), effect.id.clone(), result.clone())?;
186 Ok(())
187 })?;
188 Ok(result)
189 }
190 Err(err) => {
191 record_effect_failure(cx, effect.id, &err)?;
192 Err(err)
193 }
194 }
195}
196
197pub fn effect_replay_key(effect: &Effect, implementation: Option<Ref>) -> Result<ContentId> {
199 effect_replay_preimage(effect, implementation).content_id()
200}
201
202pub fn effect_replay_preimage(effect: &Effect, implementation: Option<Ref>) -> Datum {
204 let mut requires = effect.requires.clone();
205 requires.sort();
206 requires.dedup();
207 let mut fields = vec![
208 (
209 Symbol::new("version"),
210 Datum::String(EFFECT_REPLAY_VERSION.to_owned()),
211 ),
212 (Symbol::new("kind"), Datum::Symbol(effect.kind.clone())),
213 (Symbol::new("subject"), ref_datum(effect.subject.clone())),
214 (Symbol::new("input"), ref_datum(effect.input.clone())),
215 (
216 Symbol::new("result-shape"),
217 ref_datum(effect.result_shape.clone()),
218 ),
219 (
220 Symbol::new("resume-op"),
221 op_key_datum(effect.resume_op.clone()),
222 ),
223 (
224 Symbol::new("abort-op"),
225 op_key_datum(effect.abort_op.clone()),
226 ),
227 (
228 Symbol::new("requires"),
229 Datum::List(
230 requires
231 .into_iter()
232 .map(|capability| Datum::String(capability.as_str().to_owned()))
233 .collect(),
234 ),
235 ),
236 ];
237 if let Some(implementation) = implementation {
238 fields.push((Symbol::new("implementation"), ref_datum(implementation)));
239 }
240 Datum::Node {
241 tag: core_symbol("EffectReplayKey"),
242 fields,
243 }
244}
245
246pub fn effect_control_prompt_kind() -> Symbol {
248 effect_symbol("control-prompt")
249}
250
251pub fn effect_control_capture_kind() -> Symbol {
253 effect_symbol("control-capture")
254}
255
256pub fn effect_control_abort_kind() -> Symbol {
258 effect_symbol("control-abort")
259}
260
261pub fn effect_control_resume_kind() -> Symbol {
263 effect_symbol("control-resume")
264}
265
266pub fn effect_resume_op_key() -> OpKey {
268 OpKey::new(effect_symbol("control"), Symbol::new("resume"), 1)
269}
270
271pub fn effect_abort_op_key() -> OpKey {
273 OpKey::new(effect_symbol("control"), Symbol::new("abort"), 1)
274}
275
276#[cfg(test)]
277fn effect_test_kind(name: &str) -> Symbol {
278 effect_symbol(name)
279}
280
281fn record_effect_failure(cx: &mut Cx, effect: Ref, err: &Error) -> Result<()> {
282 let error_ref = error_ref(cx, err)?;
283 cx.with_effect_ledger(|cx, ledger| {
284 ledger.record_failed(cx.datum_store_mut(), effect, error_ref)?;
285 Ok(())
286 })
287}
288
289fn error_ref(cx: &mut Cx, err: &Error) -> Result<Ref> {
290 let id = cx
291 .datum_store_mut()
292 .intern(Datum::String(err.to_string()))?;
293 Ok(Ref::Content(id))
294}
295
296fn ref_datum(reference: Ref) -> Datum {
297 match reference {
298 Ref::Symbol(symbol) => Datum::Node {
299 tag: core_symbol("ref"),
300 fields: vec![
301 (Symbol::new("kind"), Datum::Symbol(core_symbol("symbol"))),
302 (Symbol::new("symbol"), Datum::Symbol(symbol)),
303 ],
304 },
305 Ref::Content(content) => Datum::Node {
306 tag: core_symbol("ref"),
307 fields: vec![
308 (Symbol::new("kind"), Datum::Symbol(core_symbol("content"))),
309 (Symbol::new("content"), content_id_datum(content)),
310 ],
311 },
312 Ref::Handle(handle) => Datum::Node {
313 tag: core_symbol("ref"),
314 fields: vec![
315 (Symbol::new("kind"), Datum::Symbol(core_symbol("handle"))),
316 (Symbol::new("id"), handle_id_datum(handle)),
317 ],
318 },
319 Ref::Coord(coordinate) => coordinate_datum(coordinate),
320 }
321}
322
323fn coordinate_datum(coordinate: Coordinate) -> Datum {
324 Datum::Node {
325 tag: core_symbol("ref"),
326 fields: vec![
327 (Symbol::new("kind"), Datum::Symbol(core_symbol("coord"))),
328 (Symbol::new("space"), Datum::Symbol(coordinate.space)),
329 (Symbol::new("ordinal"), content_id_datum(coordinate.ordinal)),
330 ],
331 }
332}
333
334fn content_id_datum(content: ContentId) -> Datum {
335 Datum::Node {
336 tag: core_symbol("content-id"),
337 fields: vec![
338 (Symbol::new("algorithm"), Datum::Symbol(content.algorithm)),
339 (Symbol::new("bytes"), Datum::Bytes(content.bytes.to_vec())),
340 ],
341 }
342}
343
344fn handle_id_datum(handle: HandleId) -> Datum {
345 Datum::Bytes(handle.0.to_be_bytes().to_vec())
346}
347
348fn op_key_datum(op: OpKey) -> Datum {
349 Datum::Node {
350 tag: core_symbol("op-key"),
351 fields: vec![
352 (Symbol::new("namespace"), Datum::Symbol(op.namespace)),
353 (Symbol::new("name"), Datum::Symbol(op.name)),
354 (
355 Symbol::new("version"),
356 Datum::Number(NumberLiteral {
357 domain: core_symbol("u16"),
358 canonical: op.version.to_string(),
359 }),
360 ),
361 ],
362 }
363}
364
365fn effect_symbol(name: &str) -> Symbol {
366 Symbol::qualified("effect", name)
367}
368
369fn core_symbol(name: &str) -> Symbol {
370 Symbol::qualified("core", name)
371}
372
373#[cfg(test)]
374mod tests {
375 use std::sync::{
376 Arc,
377 atomic::{AtomicUsize, Ordering},
378 };
379
380 use super::*;
381 use crate::EventKind;
382
383 use crate::testing::bare_cx as cx;
384
385 fn effect(input: Ref) -> Effect {
386 Effect::new(
387 HandleId::from_seed_and_sequence(crate::HandleSeed::new(7), 1),
388 effect_test_kind("tool-call"),
389 Ref::Symbol(Symbol::qualified("test", "tool")),
390 input,
391 Ref::Symbol(core_symbol("Any")),
392 effect_resume_op_key(),
393 effect_abort_op_key(),
394 )
395 }
396
397 #[test]
398 fn same_replay_preimage_gives_same_key() {
399 let left = effect(Ref::Symbol(Symbol::qualified("test", "input")));
400 let right = effect(Ref::Symbol(Symbol::qualified("test", "input")));
401
402 assert_eq!(
403 effect_replay_key(&left, None).unwrap(),
404 effect_replay_key(&right, None).unwrap()
405 );
406 }
407
408 #[test]
409 fn changed_input_gives_different_key() {
410 let left = effect(Ref::Symbol(Symbol::qualified("test", "left")));
411 let right = effect(Ref::Symbol(Symbol::qualified("test", "right")));
412
413 assert_ne!(
414 effect_replay_key(&left, None).unwrap(),
415 effect_replay_key(&right, None).unwrap()
416 );
417 }
418
419 #[test]
420 fn resolving_effect_emits_requested_and_resolved_events() {
421 let mut cx = cx();
422 let result = Ref::Symbol(Symbol::qualified("test", "result"));
423
424 let actual = resolve_effect(&mut cx, effect(Ref::Symbol(Symbol::new("input"))), {
425 let result = result.clone();
426 move |_cx, _effect| Ok(result)
427 })
428 .unwrap();
429
430 assert_eq!(actual, result);
431 let records = cx.effect_ledger().records();
432 assert_eq!(records.len(), 1);
433 assert_eq!(records[0].result, Some(result.clone()));
434 let events = cx.effect_ledger().events_for_run();
435 assert!(matches!(events[0].kind, EventKind::EffectRequested { .. }));
436 assert!(matches!(events[1].kind, EventKind::EffectResolved { .. }));
437 }
438
439 #[test]
440 fn missing_capability_denies_effect_before_performer_runs() {
441 let mut cx = cx();
442 let calls = Arc::new(AtomicUsize::new(0));
443 let err = resolve_effect(
444 &mut cx,
445 effect(Ref::Symbol(Symbol::new("input")))
446 .requiring(CapabilityName::new("test.required")),
447 {
448 let calls = calls.clone();
449 move |_cx, _effect| {
450 calls.fetch_add(1, Ordering::SeqCst);
451 Ok(Ref::Symbol(Symbol::new("unreachable")))
452 }
453 },
454 )
455 .unwrap_err();
456
457 assert!(
458 matches!(err, Error::CapabilityDenied { capability } if capability.as_str() == "test.required")
459 );
460 assert_eq!(calls.load(Ordering::SeqCst), 0);
461 assert!(cx.effect_ledger().records()[0].aborted);
462 }
463
464 #[test]
465 fn cassette_result_is_used_when_replay_key_matches() {
466 let mut cx = cx();
467 let mut effect = effect(Ref::Symbol(Symbol::new("input")));
468 let key = effect.ensure_replay_key(None).unwrap();
469 let cassette = Ref::Symbol(Symbol::qualified("test", "cassette-result"));
470 cx.effect_ledger_mut()
471 .insert_cassette_result(key, cassette.clone());
472 let calls = Arc::new(AtomicUsize::new(0));
473
474 let actual = resolve_effect(&mut cx, effect, {
475 let calls = calls.clone();
476 move |_cx, _effect| {
477 calls.fetch_add(1, Ordering::SeqCst);
478 Ok(Ref::Symbol(Symbol::new("performed")))
479 }
480 })
481 .unwrap();
482
483 assert_eq!(actual, cassette);
484 assert_eq!(calls.load(Ordering::SeqCst), 0);
485 }
486}