Skip to main content

sim_lib_agent/components/
model.rs

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