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#[derive(Clone)]
49pub enum RunnerBackend {
50 Echo {
52 model: String,
54 },
55 Cassette {
57 model: String,
59 strict: bool,
61 entries: Arc<Vec<Expr>>,
63 },
64 Fake {
66 model: String,
68 script: Arc<Mutex<VecDeque<Expr>>>,
70 delay: Duration,
72 },
73 External {
75 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}