Skip to main content

sim_lib_agent/components/
model.rs

1use crate::{Component, ComponentKind, util::expr_to_value};
2use sim_kernel::{
3    CapabilityName, ClassRef, Cx, Error, EvalFabric, EvalReply, EvalRequest, Expr, Object,
4    ObjectCompat, Result, Symbol, Value,
5};
6use sim_lib_agent_runner_core::ModelRunner;
7use sim_lib_server::{
8    EvalSite, ServerAddress, ServerFrame, StreamSink, eval_reply_from_frame,
9    server_frame_from_request,
10};
11use std::{
12    any::Any,
13    collections::VecDeque,
14    path::PathBuf,
15    sync::{Arc, Mutex},
16    time::Duration,
17};
18
19#[sim_citizen_derive::non_citizen(
20    reason = "agent component runtime shell; reconstruct from topology/Package and agent-runner/ModelCard descriptors",
21    kind = "marker"
22)]
23#[derive(Clone)]
24pub(crate) struct AgentComponent {
25    pub(crate) symbol: Symbol,
26    pub(crate) kind: ComponentKind,
27    pub(crate) capabilities: Vec<CapabilityName>,
28    pub(crate) address: ServerAddress,
29    pub(crate) codecs: Vec<Symbol>,
30    pub(crate) spec: Vec<(Symbol, Expr)>,
31    pub(crate) backend: ComponentBackend,
32}
33
34#[derive(Clone)]
35pub(crate) enum ComponentBackend {
36    Runner(RunnerBackend),
37    Planner(PlannerBackend),
38    Judge(JudgeBackend),
39    Router(RouterBackend),
40    Persona(PersonaBackend),
41    Retriever(RetrieverBackend),
42    Sandbox(SandboxBackend),
43    Recorder(RecorderBackend),
44    Voice(VoiceBackend),
45}
46
47/// The strategy a runner component uses to produce model responses.
48#[derive(Clone)]
49pub enum RunnerBackend {
50    /// Echoes the request back, attributed to a named model.
51    Echo {
52        /// Name of the model this backend reports as.
53        model: String,
54    },
55    /// Replays pre-recorded responses from a fixed cassette.
56    Cassette {
57        /// Name of the model this backend reports as.
58        model: String,
59        /// Whether replay fails when the request does not match the recording.
60        strict: bool,
61        /// The recorded response entries, replayed in order.
62        entries: Arc<Vec<Expr>>,
63    },
64    /// Emits scripted responses with a simulated delay, for testing.
65    Fake {
66        /// Name of the model this backend reports as.
67        model: String,
68        /// Queue of scripted responses, consumed per request.
69        script: Arc<Mutex<VecDeque<Expr>>>,
70        /// Artificial delay applied before each response.
71        delay: Duration,
72    },
73    /// Delegates to an external [`ModelRunner`] implementation.
74    External {
75        /// The backing model runner.
76        runner: Arc<dyn ModelRunner>,
77    },
78}
79
80#[derive(Clone)]
81pub(crate) enum PlannerBackend {
82    Budget {
83        max_turns: Option<u32>,
84        max_cost: Option<f64>,
85    },
86    Refine,
87    Parallel {
88        branches: u32,
89    },
90    Chain,
91}
92
93#[derive(Clone)]
94pub(crate) enum JudgeBackend {
95    Rubric { config: JudgeConfig },
96    RankedVote { config: JudgeConfig },
97    Threshold { threshold: f64, config: JudgeConfig },
98}
99
100#[derive(Clone)]
101pub(crate) enum RouterBackend {
102    RoundRobin {
103        targets: Vec<Symbol>,
104        cursor: Arc<Mutex<usize>>,
105    },
106    Bid {
107        targets: Vec<Symbol>,
108        metric: Symbol,
109        auction_window: Duration,
110    },
111    Sticky {
112        targets: Vec<Symbol>,
113        sticky_key: Symbol,
114    },
115}
116
117#[derive(Clone)]
118pub(crate) enum PersonaBackend {
119    Style {
120        voice: String,
121        prefix: String,
122        suffix: String,
123        max_words: usize,
124    },
125    Language {
126        language: String,
127        glossary: Vec<(String, String)>,
128    },
129    Translator {
130        from: String,
131        to: String,
132        table: Vec<(String, String)>,
133        command: Option<String>,
134    },
135}
136
137#[derive(Clone)]
138pub(crate) struct JudgeConfig {
139    pub(crate) rubric: Expr,
140    pub(crate) criteria: Vec<JudgeCriterion>,
141    pub(crate) reference: Option<String>,
142    pub(crate) style_markers: Vec<String>,
143}
144
145#[derive(Clone)]
146pub(crate) struct JudgeCriterion {
147    pub(crate) name: String,
148    pub(crate) weight: f64,
149}
150
151#[derive(Clone)]
152pub(crate) enum RetrieverBackend {
153    Vector { store: String, corpus: Vec<String> },
154    Web { endpoint: String },
155    File { root: Option<PathBuf> },
156    Db { path: PathBuf },
157}
158
159#[derive(Clone)]
160pub(crate) enum SandboxBackend {
161    Wasm {
162        region: String,
163    },
164    Subprocess {
165        command: Option<String>,
166        max_time: Duration,
167        max_output_bytes: usize,
168    },
169    CapabilityRestricted {
170        allowed: Vec<CapabilityName>,
171    },
172}
173
174#[derive(Clone)]
175pub(crate) enum RecorderBackend {
176    Journal {
177        path: Option<PathBuf>,
178        entries: crate::memory::SharedEntries,
179    },
180    Audit {
181        path: Option<PathBuf>,
182        entries: crate::memory::SharedEntries,
183    },
184    Prometheus {
185        namespace: String,
186        entries: crate::memory::SharedEntries,
187    },
188}
189
190#[derive(Clone)]
191pub(crate) enum VoiceBackend {
192    Tts {
193        voice: String,
194        command: Option<String>,
195    },
196    Stt {
197        locale: String,
198        command: Option<String>,
199    },
200}
201
202impl Object for AgentComponent {
203    fn display(&self, _cx: &mut Cx) -> Result<String> {
204        Ok(format!("#<component {}>", self.symbol))
205    }
206
207    fn as_any(&self) -> &dyn Any {
208        self
209    }
210}
211
212impl sim_kernel::ObjectCompat for AgentComponent {
213    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
214        if let Some(value) = cx
215            .registry()
216            .class_by_symbol(&Symbol::qualified("core", "Table"))
217        {
218            return Ok(value.clone());
219        }
220        cx.factory().class_stub(
221            sim_kernel::CORE_TABLE_CLASS_ID,
222            Symbol::qualified("core", "Table"),
223        )
224    }
225    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
226        self.as_table(cx)?.object().as_expr(cx)
227    }
228    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
229        let mut entries = vec![
230            (
231                Symbol::new("kind"),
232                cx.factory().symbol(component_kind_symbol(&self.kind))?,
233            ),
234            (
235                Symbol::new("name"),
236                cx.factory().symbol(self.symbol.clone())?,
237            ),
238            (
239                Symbol::new("capabilities"),
240                cx.factory().list(
241                    self.capabilities
242                        .iter()
243                        .map(|capability| cx.factory().string(capability.as_str().to_owned()))
244                        .collect::<Result<Vec<_>>>()?,
245                )?,
246            ),
247        ];
248        for (key, value) in &self.spec {
249            entries.push((key.clone(), expr_to_value(cx, value)?));
250        }
251        cx.factory().table(entries)
252    }
253    fn as_eval_fabric(&self) -> Option<&dyn EvalFabric> {
254        matches!(&self.backend, ComponentBackend::Runner(_)).then_some(self)
255    }
256}
257
258impl EvalSite for AgentComponent {
259    fn site_kind(&self) -> &'static str {
260        match self.kind {
261            ComponentKind::Runner => "runner",
262            ComponentKind::Planner => "planner",
263            ComponentKind::Judge => "judge",
264            ComponentKind::Router => "router",
265            ComponentKind::Persona => "persona",
266            ComponentKind::Retriever => "retriever",
267            ComponentKind::Sandbox => "sandbox",
268            ComponentKind::Recorder => "recorder",
269            ComponentKind::Voice => "voice",
270            ComponentKind::Tool => "tool",
271            ComponentKind::Memory => "memory",
272            ComponentKind::Topology => "topology",
273            ComponentKind::Custom(_) => "component",
274        }
275    }
276
277    fn address(&self) -> &ServerAddress {
278        &self.address
279    }
280
281    fn codecs(&self) -> &[Symbol] {
282        &self.codecs
283    }
284
285    fn answer(&self, cx: &mut Cx, frame: ServerFrame) -> Result<ServerFrame> {
286        self.backend.answer(cx, self, frame)
287    }
288
289    fn stream(&self, cx: &mut Cx, frame: ServerFrame, sink: &mut dyn StreamSink) -> Result<()> {
290        match &self.backend {
291            ComponentBackend::Runner(backend) => {
292                super::runtime::stream_runner(cx, self, backend, frame, sink)
293            }
294            _ => {
295                let reply = self.answer(cx, frame)?;
296                sink.chunk(cx, reply)?;
297                sink.end(cx)
298            }
299        }
300    }
301
302    fn as_any(&self) -> &dyn Any {
303        self
304    }
305}
306
307impl Component for AgentComponent {
308    fn kind(&self) -> ComponentKind {
309        self.kind.clone()
310    }
311
312    fn name(&self) -> &Symbol {
313        &self.symbol
314    }
315
316    fn capabilities(&self) -> &[CapabilityName] {
317        &self.capabilities
318    }
319
320    fn reflect(&self, cx: &mut Cx) -> Result<Expr> {
321        self.as_table(cx)?.object().as_expr(cx)
322    }
323}
324
325impl EvalFabric for AgentComponent {
326    fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
327        match &self.backend {
328            ComponentBackend::Runner(_) => {
329                let codec = self.codecs.first().ok_or_else(|| {
330                    Error::Eval(format!("{} has no installed codec", self.symbol))
331                })?;
332                let frame = server_frame_from_request(cx, codec, request)?;
333                let reply = self.answer(cx, frame)?;
334                eval_reply_from_frame(cx, &reply)
335            }
336            _ => Err(Error::Eval(format!(
337                "{} is not a realize target",
338                self.symbol
339            ))),
340        }
341    }
342}
343
344impl ComponentBackend {
345    fn answer(
346        &self,
347        cx: &mut Cx,
348        component: &AgentComponent,
349        frame: ServerFrame,
350    ) -> Result<ServerFrame> {
351        match self {
352            Self::Runner(backend) => super::runtime::answer_runner(cx, component, backend, frame),
353            Self::Planner(backend) => super::runtime::answer_planner(cx, component, backend, frame),
354            Self::Judge(backend) => super::runtime::answer_judge(cx, component, backend, frame),
355            Self::Router(backend) => super::runtime::answer_router(cx, component, backend, frame),
356            Self::Persona(backend) => super::runtime::answer_persona(cx, component, backend, frame),
357            Self::Retriever(backend) => {
358                super::runtime::answer_retriever(cx, component, backend, frame)
359            }
360            Self::Sandbox(backend) => super::runtime::answer_sandbox(cx, component, backend, frame),
361            Self::Recorder(backend) => {
362                super::runtime::answer_recorder(cx, component, backend, frame)
363            }
364            Self::Voice(backend) => super::runtime::answer_voice(cx, component, backend, frame),
365        }
366    }
367}
368
369pub(crate) fn component_kind_symbol(kind: &ComponentKind) -> Symbol {
370    match kind {
371        ComponentKind::Runner => Symbol::new("runner"),
372        ComponentKind::Tool => Symbol::new("tool"),
373        ComponentKind::Memory => Symbol::new("memory"),
374        ComponentKind::Planner => Symbol::new("planner"),
375        ComponentKind::Judge => Symbol::new("judge"),
376        ComponentKind::Router => Symbol::new("router"),
377        ComponentKind::Persona => Symbol::new("persona"),
378        ComponentKind::Retriever => Symbol::new("retriever"),
379        ComponentKind::Sandbox => Symbol::new("sandbox"),
380        ComponentKind::Recorder => Symbol::new("recorder"),
381        ComponentKind::Voice => Symbol::new("voice"),
382        ComponentKind::Topology => Symbol::new("topology"),
383        ComponentKind::Custom(symbol) => symbol.clone(),
384    }
385}
386
387pub(crate) fn component_value(cx: &mut Cx, component: AgentComponent) -> Result<Value> {
388    cx.factory().opaque(Arc::new(component))
389}
390
391pub(crate) fn number_expr(value: u32) -> Expr {
392    Expr::Number(sim_kernel::NumberLiteral {
393        domain: Symbol::qualified("numbers", "f64"),
394        canonical: value.to_string(),
395    })
396}
397
398pub(crate) fn number_expr_from_f64(value: f64) -> Expr {
399    Expr::Number(sim_kernel::NumberLiteral {
400        domain: Symbol::qualified("numbers", "f64"),
401        canonical: value.to_string(),
402    })
403}