oris_kernel/kernel/
runtime_effect.rs1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::kernel::identity::RunId;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
18pub enum RuntimeEffect {
19 LLMCall { provider: String, input: Value },
21 ToolCall { tool: String, input: Value },
23 StateWrite {
25 step_id: Option<String>,
26 payload: Value,
27 },
28 InterruptRaise { value: Value },
30}
31
32pub trait EffectSink: Send + Sync {
38 fn record(&self, run_id: &RunId, effect: &RuntimeEffect);
40}
41
42#[derive(Debug, Default)]
44pub struct NoopEffectSink;
45
46impl EffectSink for NoopEffectSink {
47 fn record(&self, _run_id: &RunId, _effect: &RuntimeEffect) {}
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use std::sync::atomic::{AtomicUsize, Ordering};
54
55 #[test]
56 fn runtime_effect_llm_call_roundtrip() {
57 let e = RuntimeEffect::LLMCall {
58 provider: "openai".to_string(),
59 input: serde_json::json!({"model": "gpt-4"}),
60 };
61 let j = serde_json::to_value(&e).unwrap();
62 let _: RuntimeEffect = serde_json::from_value(j).unwrap();
63 }
64
65 #[test]
66 fn noop_effect_sink_accepts_all() {
67 let sink = NoopEffectSink;
68 let run_id: RunId = "test-run".into();
69 sink.record(
70 &run_id,
71 &RuntimeEffect::StateWrite {
72 step_id: None,
73 payload: serde_json::json!(null),
74 },
75 );
76 }
77
78 #[test]
79 fn effect_sink_can_count() {
80 struct CountSink(AtomicUsize);
81 impl EffectSink for CountSink {
82 fn record(&self, _: &RunId, _: &RuntimeEffect) {
83 self.0.fetch_add(1, Ordering::Relaxed);
84 }
85 }
86 let sink = CountSink(AtomicUsize::new(0));
87 let run_id: RunId = "test-run".into();
88 sink.record(
89 &run_id,
90 &RuntimeEffect::ToolCall {
91 tool: "t".into(),
92 input: serde_json::json!(()),
93 },
94 );
95 sink.record(
96 &run_id,
97 &RuntimeEffect::InterruptRaise {
98 value: serde_json::json!(true),
99 },
100 );
101 assert_eq!(sink.0.load(Ordering::Relaxed), 2);
102 }
103}