Skip to main content

sim_kernel/
effect.rs

1//! The effect contract: capability-gated requests resolved by an implementation.
2//!
3//! The kernel defines the [`Effect`] record, its replay-key identity, and the
4//! control effect protocol. Libraries mint behavior-specific effect kinds.
5
6use 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
18/// Version tag mixed into every effect replay-key preimage.
19pub const EFFECT_REPLAY_VERSION: &str = "sim6-effect-replay-v1";
20
21/// A capability-gated request resolved by an effect handler.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Effect {
24    /// Fresh handle identifying this effect occurrence.
25    pub id: Ref,
26    /// Symbol naming the effect kind.
27    pub kind: Symbol,
28    /// Reference to the effect's subject.
29    pub subject: Ref,
30    /// Reference to the effect's input.
31    pub input: Ref,
32    /// Shape the effect result must satisfy.
33    pub result_shape: Ref,
34    /// Operation key used to resume the effect.
35    pub resume_op: OpKey,
36    /// Operation key used to abort the effect.
37    pub abort_op: OpKey,
38    /// Capabilities the caller must hold for the effect to run.
39    pub requires: Vec<CapabilityName>,
40    /// Content-addressed replay key, once computed.
41    pub replay_key: Option<ContentId>,
42}
43
44impl Effect {
45    /// Build an effect with a fresh handle id and no requirements.
46    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    /// Override the effect's id.
69    pub fn with_id(mut self, id: Ref) -> Self {
70        self.id = id;
71        self
72    }
73
74    /// Add one required capability to the effect.
75    pub fn requiring(mut self, capability: CapabilityName) -> Self {
76        self.requires.push(capability);
77        self
78    }
79
80    /// Replace the effect's required capabilities.
81    pub fn with_requirements(mut self, requires: Vec<CapabilityName>) -> Self {
82        self.requires = requires;
83        self
84    }
85
86    /// Compute and attach the replay key for the given implementation.
87    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    /// Return the replay key, computing and caching it if absent.
93    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/// Ledger record tracking one effect's request, resolution, and outcome.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct EffectRecord {
106    /// Reference to the recorded effect.
107    pub effect: Ref,
108    /// Reference to the requested event.
109    pub requested_event: Ref,
110    /// Reference to the resolved (or failed) event, once known.
111    pub resolved_event: Option<Ref>,
112    /// Reference to the effect result, once known.
113    pub result: Option<Ref>,
114    /// Whether the effect was aborted rather than resolved.
115    pub aborted: bool,
116}
117
118/// Resolve an effect end to end: record the request, enforce capabilities,
119/// reuse any cassette result, otherwise run `perform`, then record the outcome.
120///
121/// # Examples
122///
123/// ```
124/// # use std::sync::Arc;
125/// # use sim_kernel::{DefaultFactory, HandleSeed, NoopEvalPolicy};
126/// # use sim_kernel::env::Cx;
127/// # use sim_kernel::effect::{Effect, resolve_effect, effect_resume_op_key, effect_abort_op_key};
128/// # use sim_kernel::id::Symbol;
129/// # use sim_kernel::ref_id::Ref;
130/// let mut cx = Cx::new(
131///     Arc::new(NoopEvalPolicy),
132///     Arc::new(DefaultFactory),
133///     HandleSeed::new(7),
134/// );
135/// let effect = Effect::new(
136///     cx.fresh_handle(),
137///     Symbol::qualified("effect", "tool-call"),
138///     Ref::Symbol(Symbol::qualified("test", "tool")),
139///     Ref::Symbol(Symbol::new("input")),
140///     Ref::Symbol(Symbol::qualified("core", "Any")),
141///     effect_resume_op_key(),
142///     effect_abort_op_key(),
143/// );
144/// let result = Ref::Symbol(Symbol::new("ok"));
145/// let got = resolve_effect(&mut cx, effect, {
146///     let result = result.clone();
147///     move |_cx, _effect| Ok(result)
148/// })
149/// .unwrap();
150/// assert_eq!(got, result);
151/// assert_eq!(cx.effect_ledger().records().len(), 1);
152/// ```
153pub 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
197/// Content-id of the effect's replay-key preimage for `implementation`.
198pub fn effect_replay_key(effect: &Effect, implementation: Option<Ref>) -> Result<ContentId> {
199    effect_replay_preimage(effect, implementation).content_id()
200}
201
202/// Build the canonical datum hashed into an effect's replay key.
203pub 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
246/// Well-known kind symbol for control-prompt effects.
247pub fn effect_control_prompt_kind() -> Symbol {
248    effect_symbol("control-prompt")
249}
250
251/// Well-known kind symbol for control-capture effects.
252pub fn effect_control_capture_kind() -> Symbol {
253    effect_symbol("control-capture")
254}
255
256/// Well-known kind symbol for control-abort effects.
257pub fn effect_control_abort_kind() -> Symbol {
258    effect_symbol("control-abort")
259}
260
261/// Well-known kind symbol for control-resume effects.
262pub fn effect_control_resume_kind() -> Symbol {
263    effect_symbol("control-resume")
264}
265
266/// Operation key effects use to resume a handler.
267pub fn effect_resume_op_key() -> OpKey {
268    OpKey::new(effect_symbol("control"), Symbol::new("resume"), 1)
269}
270
271/// Operation key effects use to abort a handler.
272pub 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}