sim_lib_agent_runner_core/
model.rs1use crate::{ModelBid, ModelCard, ModelEvent, ModelEventSink};
2use sim_citizen::CitizenField;
3use sim_citizen_derive::Citizen;
4use sim_kernel::{Cx, Expr, Result, Symbol};
5
6pub trait ModelRunner: Send + Sync {
8 fn card(&self) -> ModelCard;
10
11 fn infer(&self, cx: &mut Cx, request: ModelRequest) -> Result<ModelResponse>;
13
14 fn infer_stream(
19 &self,
20 cx: &mut Cx,
21 request: ModelRequest,
22 sink: &mut dyn ModelEventSink,
23 ) -> Result<ModelResponse> {
24 let response = self.infer(cx, request)?;
25 sink.emit(ModelEvent::final_of(&response))?;
26 Ok(response)
27 }
28
29 fn bid(&self, _request: &ModelRequest) -> Result<ModelBid> {
31 Ok(ModelBid::unavailable("runner did not implement bidding"))
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Citizen)]
37#[citizen(symbol = "agent-runner/ModelRequest", version = 1)]
38pub struct ModelRequest {
39 pub task: Expr,
41 pub messages: Vec<Expr>,
43 pub extra: Vec<(Expr, Expr)>,
45}
46
47impl ModelRequest {
48 pub fn new(task: Expr, messages: Vec<Expr>) -> Self {
50 Self {
51 task,
52 messages,
53 extra: Vec::new(),
54 }
55 }
56}
57
58impl Default for ModelRequest {
59 fn default() -> Self {
60 Self::new(Expr::String("citizen fixture task".to_owned()), Vec::new())
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Citizen)]
66#[citizen(symbol = "agent-runner/ModelResponse", version = 1)]
67pub struct ModelResponse {
68 pub runner: Symbol,
70 pub model: String,
72 pub content: Vec<Expr>,
74 pub stop_reason: Symbol,
76 pub usage: Option<ModelUsage>,
78 pub extra: Vec<(Expr, Expr)>,
80}
81
82impl ModelResponse {
83 pub fn new(
85 runner: Symbol,
86 model: impl Into<String>,
87 content: Vec<Expr>,
88 stop_reason: Symbol,
89 ) -> Self {
90 Self {
91 runner,
92 model: model.into(),
93 content,
94 stop_reason,
95 usage: None,
96 extra: Vec::new(),
97 }
98 }
99}
100
101impl Default for ModelResponse {
102 fn default() -> Self {
103 Self::new(
104 Symbol::qualified("runner", "fixture"),
105 "fixture-model",
106 vec![Expr::String("ok".to_owned())],
107 Symbol::new("stop"),
108 )
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Default, Citizen)]
114#[citizen(symbol = "agent-runner/ModelUsage", version = 1)]
115pub struct ModelUsage {
116 pub input_tokens: Option<u64>,
118 pub output_tokens: Option<u64>,
120 pub latency_ms: Option<u64>,
122 pub cost_usd: Option<f64>,
124 pub extra: Vec<(Expr, Expr)>,
126}
127
128impl CitizenField for ModelUsage {
129 fn encode_field(&self) -> Expr {
130 Expr::List(vec![
131 self.input_tokens.encode_field(),
132 self.output_tokens.encode_field(),
133 self.latency_ms.encode_field(),
134 self.cost_usd.encode_field(),
135 self.extra.encode_field(),
136 ])
137 }
138
139 fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
140 let Expr::List(items) = expr else {
141 return Err(sim_citizen::field_error(field, "expected model usage list"));
142 };
143 let [input_tokens, output_tokens, latency_ms, cost_usd, extra] = items.as_slice() else {
144 return Err(sim_citizen::field_error(
145 field,
146 format!("expected 5 model usage field(s), found {}", items.len()),
147 ));
148 };
149 Ok(Self {
150 input_tokens: Option::<u64>::decode_field_expr(input_tokens, field)?,
151 output_tokens: Option::<u64>::decode_field_expr(output_tokens, field)?,
152 latency_ms: Option::<u64>::decode_field_expr(latency_ms, field)?,
153 cost_usd: Option::<f64>::decode_field_expr(cost_usd, field)?,
154 extra: Vec::<(Expr, Expr)>::decode_field_expr(extra, field)?,
155 })
156 }
157}
158
159pub fn model_request_class_symbol() -> Symbol {
161 Symbol::qualified("agent-runner", "ModelRequest")
162}
163
164pub fn model_response_class_symbol() -> Symbol {
166 Symbol::qualified("agent-runner", "ModelResponse")
167}
168
169pub fn model_usage_class_symbol() -> Symbol {
171 Symbol::qualified("agent-runner", "ModelUsage")
172}