use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::kernel::identity::RunId;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum RuntimeEffect {
LLMCall { provider: String, input: Value },
ToolCall { tool: String, input: Value },
StateWrite {
step_id: Option<String>,
payload: Value,
},
InterruptRaise { value: Value },
}
pub trait EffectSink: Send + Sync {
fn record(&self, run_id: &RunId, effect: &RuntimeEffect);
}
#[derive(Debug, Default)]
pub struct NoopEffectSink;
impl EffectSink for NoopEffectSink {
fn record(&self, _run_id: &RunId, _effect: &RuntimeEffect) {}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn runtime_effect_llm_call_roundtrip() {
let e = RuntimeEffect::LLMCall {
provider: "openai".to_string(),
input: serde_json::json!({"model": "gpt-4"}),
};
let j = serde_json::to_value(&e).unwrap();
let _: RuntimeEffect = serde_json::from_value(j).unwrap();
}
#[test]
fn noop_effect_sink_accepts_all() {
let sink = NoopEffectSink;
let run_id: RunId = "test-run".into();
sink.record(
&run_id,
&RuntimeEffect::StateWrite {
step_id: None,
payload: serde_json::json!(null),
},
);
}
#[test]
fn effect_sink_can_count() {
struct CountSink(AtomicUsize);
impl EffectSink for CountSink {
fn record(&self, _: &RunId, _: &RuntimeEffect) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
let sink = CountSink(AtomicUsize::new(0));
let run_id: RunId = "test-run".into();
sink.record(
&run_id,
&RuntimeEffect::ToolCall {
tool: "t".into(),
input: serde_json::json!(()),
},
);
sink.record(
&run_id,
&RuntimeEffect::InterruptRaise {
value: serde_json::json!(true),
},
);
assert_eq!(sink.0.load(Ordering::Relaxed), 2);
}
}