Skip to main content

sim_lib_agent_runner_core/
events.rs

1use crate::{ModelRequest, ModelResponse, ModelUsage};
2use sim_kernel::{Cx, Datum, DatumStore, Event, EventKind, Expr, Ref, Result, Symbol};
3
4/// Sink for streaming model events emitted during inference.
5pub trait ModelEventSink {
6    /// Receives one event from the active runner invocation.
7    fn emit(&mut self, event: ModelEvent) -> Result<()>;
8}
9
10/// In-memory event sink used by tests and buffered callers.
11#[derive(Clone, Debug, Default, PartialEq)]
12pub struct VecEventSink {
13    events: Vec<ModelEvent>,
14}
15
16impl VecEventSink {
17    /// Constructs an empty event sink.
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    /// Consumes the sink and returns all collected events.
23    pub fn into_events(self) -> Vec<ModelEvent> {
24        self.events
25    }
26
27    /// Borrows the collected events.
28    pub fn events(&self) -> &[ModelEvent] {
29        &self.events
30    }
31}
32
33impl ModelEventSink for VecEventSink {
34    fn emit(&mut self, event: ModelEvent) -> Result<()> {
35        self.events.push(event);
36        Ok(())
37    }
38}
39
40/// Structured streaming event emitted by a model runner.
41#[derive(Clone, Debug, PartialEq)]
42pub struct ModelEvent {
43    /// Event kind such as `start`, `delta`, `usage`, `tool-call`, or `final`.
44    pub event: Symbol,
45    /// Runner identity that produced the event.
46    pub runner: Symbol,
47    /// Provider or runtime model name for the event.
48    pub model: String,
49    /// Correlation id for one inference span.
50    pub span_id: Expr,
51    /// Final response payload for terminal events when available.
52    pub response: Option<ModelResponse>,
53    /// Open extension fields preserved for event-specific payloads.
54    pub extra: Vec<(Expr, Expr)>,
55}
56
57impl ModelEvent {
58    /// Constructs a generic event record.
59    pub fn new(event: Symbol, runner: Symbol, model: impl Into<String>, span_id: Expr) -> Self {
60        Self {
61            event,
62            runner,
63            model: model.into(),
64            span_id,
65            response: None,
66            extra: Vec::new(),
67        }
68    }
69
70    /// Constructs a `start` event.
71    pub fn start(runner: Symbol, model: impl Into<String>, span_id: Expr) -> Self {
72        Self::new(Symbol::new("start"), runner, model, span_id)
73    }
74
75    /// Constructs a text delta event.
76    pub fn delta_text(
77        runner: Symbol,
78        model: impl Into<String>,
79        span_id: Expr,
80        text: impl Into<String>,
81    ) -> Self {
82        Self::new(Symbol::new("delta"), runner, model, span_id).with_text(text)
83    }
84
85    /// Constructs a usage event.
86    pub fn usage(
87        runner: Symbol,
88        model: impl Into<String>,
89        span_id: Expr,
90        usage: ModelUsage,
91    ) -> Self {
92        Self::new(Symbol::new("usage"), runner, model, span_id)
93            .with_field("usage", Expr::from(usage))
94    }
95
96    /// Constructs an error event with a text payload.
97    pub fn error_text(
98        runner: Symbol,
99        model: impl Into<String>,
100        span_id: Expr,
101        message: impl Into<String>,
102    ) -> Self {
103        Self::new(Symbol::new("error"), runner, model, span_id).with_text(message)
104    }
105
106    /// Constructs a tool-call event.
107    pub fn tool_call(
108        runner: Symbol,
109        model: impl Into<String>,
110        span_id: Expr,
111        tool_call: Expr,
112    ) -> Self {
113        Self::new(Symbol::new("tool-call"), runner, model, span_id)
114            .with_field("tool-call", tool_call)
115    }
116
117    /// Constructs a terminal `final` event from a completed response.
118    pub fn final_of(response: &ModelResponse) -> Self {
119        Self {
120            event: Symbol::new("final"),
121            runner: response.runner.clone(),
122            model: response.model.clone(),
123            span_id: Expr::Symbol(Symbol::new("final")),
124            response: Some(response.clone()),
125            extra: Vec::new(),
126        }
127    }
128
129    /// Appends an extra field to the event payload.
130    pub fn with_field(mut self, key: &str, value: Expr) -> Self {
131        self.extra.push((Expr::Symbol(Symbol::new(key)), value));
132        self
133    }
134
135    fn with_text(self, text: impl Into<String>) -> Self {
136        self.with_field("text", Expr::String(text.into()))
137    }
138
139    /// Converts this model event into a kernel chunk event with a content ref.
140    pub fn to_kernel_event(&self, cx: &mut Cx, run: Ref, seq: u64) -> Result<Event> {
141        model_expr_event(cx, run, seq, Expr::from(self.clone()))
142    }
143}
144
145/// Converts a model request into a kernel chunk event with a content ref.
146pub fn model_request_kernel_event(
147    cx: &mut Cx,
148    run: Ref,
149    seq: u64,
150    request: &ModelRequest,
151) -> Result<Event> {
152    model_expr_event(cx, run, seq, Expr::from(request.clone()))
153}
154
155fn model_expr_event(cx: &mut Cx, run: Ref, seq: u64, expr: Expr) -> Result<Event> {
156    let payload = Ref::Content(cx.datum_store_mut().intern(Datum::try_from(expr)?)?);
157    Event::new(run, seq, Vec::new(), EventKind::Chunk { payload })
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use sim_kernel::{DefaultFactory, NoopEvalPolicy};
164    use std::sync::Arc;
165
166    #[test]
167    fn model_request_and_delta_convert_to_kernel_chunk_events() {
168        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
169        let run = Ref::Symbol(Symbol::qualified("test", "run"));
170        let request = ModelRequest::new(Expr::String("task".to_owned()), Vec::new());
171        let request_event = model_request_kernel_event(&mut cx, run.clone(), 0, &request).unwrap();
172        let delta = ModelEvent::delta_text(
173            Symbol::qualified("runner", "fake"),
174            "fake/model",
175            Expr::String("span-1".to_owned()),
176            "hel",
177        );
178        let delta_event = delta.to_kernel_event(&mut cx, run.clone(), 1).unwrap();
179
180        assert!(matches!(request_event.kind, EventKind::Chunk { .. }));
181        assert!(matches!(delta_event.kind, EventKind::Chunk { .. }));
182        assert_eq!(delta_event.run, run);
183        assert_eq!(delta_event.seq, 1);
184    }
185}