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#[derive(Clone)]
50pub enum RunnerBackend {
51 Echo {
53 model: String,
55 },
56 Cassette {
58 model: String,
60 strict: bool,
62 entries: Arc<Vec<Expr>>,
64 },
65 Fake {
67 model: String,
69 script: Arc<Mutex<VecDeque<Expr>>>,
71 delay: Duration,
73 },
74 External {
76 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}