1mod evaluation;
4mod evaluation_scorers;
5mod evaluation_store;
6mod fault;
7mod golden_trace;
8mod retrieval_evaluation;
9
10use std::{
11 collections::VecDeque,
12 sync::{Arc, Mutex},
13};
14
15use runifold_core::{
16 Budget, BudgetTracker, CapabilitySet, EventFactory, InMemoryJournal, LifecycleEvent,
17 RunContext, RunEvent, RunEventKind,
18};
19use runifold_model::{
20 Model, ModelCallContext, ModelCapabilities, ModelError, ModelErrorKind, ModelEventStream,
21 ModelFuture, ModelRef, ModelRequest, ModelResponse, ModelStreamAccumulator, ModelStreamEvent,
22};
23use serde_json::Value;
24
25pub use evaluation::{
26 EvaluationCase, EvaluationCaseId, EvaluationCaseResult, EvaluationDataset, EvaluationError,
27 EvaluationFailure, EvaluationFailureStage, EvaluationFuture, EvaluationMetrics,
28 EvaluationOutput, EvaluationReport, EvaluationRunner, EvaluationScore, EvaluationScoreSummary,
29 EvaluationScorer, EvaluationTarget, FnScorer, JsonExactMatchScorer, MetricRegression,
30 RegressionComparison, RegressionPolicy, ScoreValue,
31};
32pub use evaluation_scorers::{
33 JsonRule, JsonRuleScorer, JudgeRubric, ModelJudgeScorer, TokenOverlapScorer, WeightedJsonRule,
34};
35pub use evaluation_store::{EvaluationRepository, EvaluationStoreError, FileEvaluationRepository};
36pub use fault::{
37 FaultController, FaultInjectingModel, FaultInjectingTool, FaultScenario, ModelFault,
38 RecoveryHarness, ScenarioAssertionError, ToolFault,
39};
40pub use golden_trace::{GoldenTrace, GoldenTraceMismatch};
41pub use retrieval_evaluation::{
42 RetrievalCaseMetrics, RetrievalEvaluationCase, RetrievalEvaluationError,
43 RetrievalEvaluationReport, RetrievalEvaluationRunner,
44};
45
46type ModelScript = Result<Vec<ModelStreamEvent>, ModelError>;
47type ModelScriptQueue = Arc<Mutex<VecDeque<ModelScript>>>;
48
49#[derive(Debug)]
51pub struct RunScenario {
52 context: RunContext,
53 events: EventFactory,
54 journal: InMemoryJournal,
55}
56
57impl RunScenario {
58 pub fn new(budget: Budget) -> Self {
60 let context = RunContext::root(BudgetTracker::new(budget), CapabilitySet::new());
61 let events = EventFactory::new(context.run_id(), context.parent_run_id());
62 Self {
63 context,
64 events,
65 journal: InMemoryJournal::new(),
66 }
67 }
68
69 pub const fn context(&self) -> &RunContext {
71 &self.context
72 }
73
74 pub fn start(&self) -> RunEvent {
76 let event = self
77 .events
78 .emit(RunEventKind::Lifecycle(LifecycleEvent::Started), None);
79 self.journal.push(event.clone());
80 event
81 }
82
83 pub fn complete(&self, output: Value, caused_by: &RunEvent) -> RunEvent {
85 let event = self.events.emit(
86 RunEventKind::Lifecycle(LifecycleEvent::Completed { output }),
87 Some(caused_by.meta.event_id),
88 );
89 self.journal.push(event.clone());
90 event
91 }
92
93 pub fn recorded_events(&self) -> Vec<RunEvent> {
95 self.journal.events()
96 }
97}
98
99#[derive(Clone, Debug, Default)]
101pub struct ModelScenario {
102 events: Vec<ModelStreamEvent>,
103}
104
105impl ModelScenario {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 #[must_use]
113 pub fn then(mut self, event: ModelStreamEvent) -> Self {
114 self.events.push(event);
115 self
116 }
117
118 pub fn response(self) -> Result<ModelResponse, ModelError> {
125 let mut accumulator = ModelStreamAccumulator::new();
126 for event in self.events {
127 if let Some(response) = accumulator.push(event)? {
128 return Ok(response);
129 }
130 }
131 Err(ModelError::local(
132 runifold_model::ModelErrorKind::StreamState,
133 "scripted model stream did not complete",
134 ))
135 }
136}
137
138#[derive(Clone, Debug, Default)]
143pub struct ScriptedModel {
144 capabilities: ModelCapabilities,
145 scripts: ModelScriptQueue,
146 requests: Arc<Mutex<Vec<ModelRequest>>>,
147 contexts: Arc<Mutex<Vec<ModelCallContext>>>,
148}
149
150impl ScriptedModel {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 #[must_use]
158 pub fn with_capabilities(mut self, capabilities: ModelCapabilities) -> Self {
159 self.capabilities = capabilities;
160 self
161 }
162
163 pub fn enqueue(&self, events: impl IntoIterator<Item = ModelStreamEvent>) {
165 self.scripts()
166 .push_back(Ok(events.into_iter().collect::<Vec<_>>()));
167 }
168
169 pub fn enqueue_error(&self, error: ModelError) {
171 self.scripts().push_back(Err(error));
172 }
173
174 pub fn recorded_requests(&self) -> Vec<ModelRequest> {
176 self.requests
177 .lock()
178 .unwrap_or_else(std::sync::PoisonError::into_inner)
179 .clone()
180 }
181
182 pub fn recorded_contexts(&self) -> Vec<ModelCallContext> {
184 self.contexts
185 .lock()
186 .unwrap_or_else(std::sync::PoisonError::into_inner)
187 .clone()
188 }
189
190 fn scripts(&self) -> std::sync::MutexGuard<'_, VecDeque<ModelScript>> {
191 self.scripts
192 .lock()
193 .unwrap_or_else(std::sync::PoisonError::into_inner)
194 }
195}
196
197impl Model for ScriptedModel {
198 fn capabilities<'a>(
199 &'a self,
200 _model: &'a ModelRef,
201 ) -> ModelFuture<'a, Result<ModelCapabilities, ModelError>> {
202 let capabilities = self.capabilities.clone();
203 Box::pin(async move { Ok(capabilities) })
204 }
205
206 fn stream(
207 &self,
208 request: ModelRequest,
209 context: ModelCallContext,
210 ) -> ModelFuture<'_, Result<ModelEventStream, ModelError>> {
211 self.requests
212 .lock()
213 .unwrap_or_else(std::sync::PoisonError::into_inner)
214 .push(request);
215 self.contexts
216 .lock()
217 .unwrap_or_else(std::sync::PoisonError::into_inner)
218 .push(context);
219 let script = self.scripts().pop_front().unwrap_or_else(|| {
220 Err(ModelError::local(
221 ModelErrorKind::Protocol,
222 "scripted model has no queued invocation",
223 ))
224 });
225 Box::pin(async move {
226 let events = script?;
227 Ok(
228 Box::pin(futures_util::stream::iter(events.into_iter().map(Ok)))
229 as ModelEventStream,
230 )
231 })
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use std::collections::BTreeMap;
238
239 use runifold_core::Budget;
240 use runifold_model::{
241 ContentBlockKind, ContentPart, FinishReason, Message, Model, ModelCallContext,
242 ModelErrorKind, ModelRef, ModelRequest, ModelStreamEvent,
243 };
244
245 use super::{ModelScenario, RunScenario, ScriptedModel};
246
247 #[test]
248 fn scenario_records_a_causal_lifecycle() {
249 let scenario = RunScenario::new(Budget::default());
250 let started = scenario.start();
251 let completed = scenario.complete(serde_json::json!({"ok": true}), &started);
252 let events = scenario.recorded_events();
253
254 assert_eq!(events, vec![started.clone(), completed.clone()]);
255 assert_eq!(completed.meta.caused_by, Some(started.meta.event_id));
256 }
257
258 #[test]
259 fn model_scenario_accumulates_a_response() {
260 let response = ModelScenario::new()
261 .then(ModelStreamEvent::ResponseStarted {
262 id: Some("test-response".into()),
263 model: ModelRef::new("test", "scripted"),
264 })
265 .then(ModelStreamEvent::ContentBlockStarted {
266 index: 0,
267 kind: ContentBlockKind::Text,
268 })
269 .then(ModelStreamEvent::TextDelta {
270 index: 0,
271 text: "hello".into(),
272 })
273 .then(ModelStreamEvent::ContentBlockCompleted { index: 0 })
274 .then(ModelStreamEvent::ResponseCompleted {
275 finish_reason: FinishReason::Stop,
276 provider_metadata: BTreeMap::new(),
277 })
278 .response()
279 .unwrap();
280
281 assert_eq!(response.content, vec![ContentPart::text("hello")]);
282 }
283
284 #[test]
285 fn scripted_model_uses_the_same_stream_accumulator_as_real_adapters() {
286 let model = ScriptedModel::new();
287 model.enqueue([
288 ModelStreamEvent::ResponseStarted {
289 id: Some("test-response".into()),
290 model: ModelRef::new("test", "scripted"),
291 },
292 ModelStreamEvent::ContentPartCompleted {
293 index: 0,
294 part: ContentPart::text("hello"),
295 },
296 ModelStreamEvent::ResponseCompleted {
297 finish_reason: FinishReason::Stop,
298 provider_metadata: BTreeMap::new(),
299 },
300 ]);
301 let request = ModelRequest::new(ModelRef::new("test", "scripted"), Message::user("hello"));
302
303 let response = futures_executor::block_on(model.invoke(request, ModelCallContext::new()))
304 .expect("script should complete");
305
306 assert_eq!(response.content, vec![ContentPart::text("hello")]);
307 }
308
309 #[test]
310 fn scripted_model_invocation_observes_preexisting_cancellation() {
311 let model = ScriptedModel::new();
312 model.enqueue([]);
313 let request = ModelRequest::new(ModelRef::new("test", "scripted"), Message::user("hello"));
314 let context = ModelCallContext::new();
315 context.cancellation().cancel();
316
317 let error = futures_executor::block_on(model.invoke(request, context))
318 .expect_err("cancelled invocation must fail");
319
320 assert_eq!(error.kind, ModelErrorKind::Cancelled);
321 }
322}