Skip to main content

sim_lib_agent_runner_core/
model.rs

1use crate::{ModelBid, ModelCard, ModelEvent, ModelEventSink};
2use sim_citizen::CitizenField;
3use sim_citizen_derive::Citizen;
4use sim_kernel::{Cx, EvalRequest, Expr, Result, Symbol};
5
6/// Executable contract for a model backend or model-like runtime surface.
7pub trait ModelRunner: Send + Sync {
8    /// Returns metadata describing the runner's advertised model surface.
9    fn card(&self) -> ModelCard;
10
11    /// Executes one non-streaming inference request.
12    fn infer(&self, cx: &mut Cx, request: ModelRequest) -> Result<ModelResponse>;
13
14    /// Executes one non-streaming inference request with its full eval envelope.
15    ///
16    /// Implementations that need the caller's capability ceiling can override
17    /// this method. The default keeps existing model-only runners compatible.
18    fn infer_request(&self, cx: &mut Cx, request: EvalRequest) -> Result<ModelResponse> {
19        self.infer(cx, ModelRequest::try_from(request.expr)?)
20    }
21
22    /// Executes one streaming inference request.
23    ///
24    /// The default implementation falls back to [`Self::infer`] and emits a
25    /// synthesized final event.
26    fn infer_stream(
27        &self,
28        cx: &mut Cx,
29        request: ModelRequest,
30        sink: &mut dyn ModelEventSink,
31    ) -> Result<ModelResponse> {
32        let response = self.infer(cx, request)?;
33        sink.emit(ModelEvent::final_of(&response))?;
34        Ok(response)
35    }
36
37    /// Executes one streaming inference request with its full eval envelope.
38    ///
39    /// The default preserves existing streaming behavior after decoding the
40    /// model request from the envelope.
41    fn infer_stream_request(
42        &self,
43        cx: &mut Cx,
44        request: EvalRequest,
45        sink: &mut dyn ModelEventSink,
46    ) -> Result<ModelResponse> {
47        self.infer_stream(cx, ModelRequest::try_from(request.expr)?, sink)
48    }
49
50    /// Returns an availability and score hint for market-style routing.
51    fn bid(&self, _request: &ModelRequest) -> Result<ModelBid> {
52        Ok(ModelBid::unavailable("runner did not implement bidding"))
53    }
54}
55
56/// Normalized model request used across providers and local runners.
57#[derive(Clone, Debug, PartialEq, Citizen)]
58#[citizen(symbol = "agent-runner/ModelRequest", version = 1)]
59pub struct ModelRequest {
60    /// Primary task expression, typically a transcript or chat request object.
61    pub task: Expr,
62    /// Prior message or context transcript items.
63    pub messages: Vec<Expr>,
64    /// Open extension fields preserved for runner-specific features.
65    pub extra: Vec<(Expr, Expr)>,
66}
67
68impl ModelRequest {
69    /// Builds a request from a task expression and message transcript.
70    pub fn new(task: Expr, messages: Vec<Expr>) -> Self {
71        Self {
72            task,
73            messages,
74            extra: Vec::new(),
75        }
76    }
77}
78
79impl Default for ModelRequest {
80    fn default() -> Self {
81        Self::new(Expr::String("citizen fixture task".to_owned()), Vec::new())
82    }
83}
84
85/// Final response returned by a runner after inference completes.
86#[derive(Clone, Debug, PartialEq, Citizen)]
87#[citizen(symbol = "agent-runner/ModelResponse", version = 1)]
88pub struct ModelResponse {
89    /// Runner identity that produced the response.
90    pub runner: Symbol,
91    /// Provider or runtime model name.
92    pub model: String,
93    /// Structured output content.
94    pub content: Vec<Expr>,
95    /// Stop reason symbol reported by the runner.
96    pub stop_reason: Symbol,
97    /// Optional usage and accounting metadata.
98    pub usage: Option<ModelUsage>,
99    /// Open extension fields preserved for runner-specific features.
100    pub extra: Vec<(Expr, Expr)>,
101}
102
103impl ModelResponse {
104    /// Constructs a response with explicit runner identity and stop reason.
105    pub fn new(
106        runner: Symbol,
107        model: impl Into<String>,
108        content: Vec<Expr>,
109        stop_reason: Symbol,
110    ) -> Self {
111        Self {
112            runner,
113            model: model.into(),
114            content,
115            stop_reason,
116            usage: None,
117            extra: Vec::new(),
118        }
119    }
120}
121
122impl Default for ModelResponse {
123    fn default() -> Self {
124        Self::new(
125            Symbol::qualified("runner", "fixture"),
126            "fixture-model",
127            vec![Expr::String("ok".to_owned())],
128            Symbol::new("stop"),
129        )
130    }
131}
132
133/// Usage and accounting information attached to a model response.
134#[derive(Clone, Debug, PartialEq, Default, Citizen)]
135#[citizen(symbol = "agent-runner/ModelUsage", version = 1)]
136pub struct ModelUsage {
137    /// Count of input tokens when reported by the backend.
138    pub input_tokens: Option<u64>,
139    /// Count of output tokens when reported by the backend.
140    pub output_tokens: Option<u64>,
141    /// End-to-end latency in milliseconds when available.
142    pub latency_ms: Option<u64>,
143    /// Monetary cost estimate in USD when available.
144    pub cost_usd: Option<f64>,
145    /// Open extension fields preserved for runner-specific accounting data.
146    pub extra: Vec<(Expr, Expr)>,
147}
148
149impl CitizenField for ModelUsage {
150    fn encode_field(&self) -> Expr {
151        Expr::List(vec![
152            self.input_tokens.encode_field(),
153            self.output_tokens.encode_field(),
154            self.latency_ms.encode_field(),
155            self.cost_usd.encode_field(),
156            self.extra.encode_field(),
157        ])
158    }
159
160    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
161        let Expr::List(items) = expr else {
162            return Err(sim_citizen::field_error(field, "expected model usage list"));
163        };
164        let [input_tokens, output_tokens, latency_ms, cost_usd, extra] = items.as_slice() else {
165            return Err(sim_citizen::field_error(
166                field,
167                format!("expected 5 model usage field(s), found {}", items.len()),
168            ));
169        };
170        Ok(Self {
171            input_tokens: Option::<u64>::decode_field_expr(input_tokens, field)?,
172            output_tokens: Option::<u64>::decode_field_expr(output_tokens, field)?,
173            latency_ms: Option::<u64>::decode_field_expr(latency_ms, field)?,
174            cost_usd: Option::<f64>::decode_field_expr(cost_usd, field)?,
175            extra: Vec::<(Expr, Expr)>::decode_field_expr(extra, field)?,
176        })
177    }
178}
179
180/// Returns the citizen class symbol for [`ModelRequest`].
181pub fn model_request_class_symbol() -> Symbol {
182    Symbol::qualified("agent-runner", "ModelRequest")
183}
184
185/// Returns the citizen class symbol for [`ModelResponse`].
186pub fn model_response_class_symbol() -> Symbol {
187    Symbol::qualified("agent-runner", "ModelResponse")
188}
189
190/// Returns the citizen class symbol for [`ModelUsage`].
191pub fn model_usage_class_symbol() -> Symbol {
192    Symbol::qualified("agent-runner", "ModelUsage")
193}