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, 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 streaming inference request.
15    ///
16    /// The default implementation falls back to [`Self::infer`] and emits a
17    /// synthesized final event.
18    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    /// Returns an availability and score hint for market-style routing.
30    fn bid(&self, _request: &ModelRequest) -> Result<ModelBid> {
31        Ok(ModelBid::unavailable("runner did not implement bidding"))
32    }
33}
34
35/// Normalized model request used across providers and local runners.
36#[derive(Clone, Debug, PartialEq, Citizen)]
37#[citizen(symbol = "agent-runner/ModelRequest", version = 1)]
38pub struct ModelRequest {
39    /// Primary task expression, typically a transcript or chat request object.
40    pub task: Expr,
41    /// Prior message or context transcript items.
42    pub messages: Vec<Expr>,
43    /// Open extension fields preserved for runner-specific features.
44    pub extra: Vec<(Expr, Expr)>,
45}
46
47impl ModelRequest {
48    /// Builds a request from a task expression and message transcript.
49    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/// Final response returned by a runner after inference completes.
65#[derive(Clone, Debug, PartialEq, Citizen)]
66#[citizen(symbol = "agent-runner/ModelResponse", version = 1)]
67pub struct ModelResponse {
68    /// Runner identity that produced the response.
69    pub runner: Symbol,
70    /// Provider or runtime model name.
71    pub model: String,
72    /// Structured output content.
73    pub content: Vec<Expr>,
74    /// Stop reason symbol reported by the runner.
75    pub stop_reason: Symbol,
76    /// Optional usage and accounting metadata.
77    pub usage: Option<ModelUsage>,
78    /// Open extension fields preserved for runner-specific features.
79    pub extra: Vec<(Expr, Expr)>,
80}
81
82impl ModelResponse {
83    /// Constructs a response with explicit runner identity and stop reason.
84    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/// Usage and accounting information attached to a model response.
113#[derive(Clone, Debug, PartialEq, Default, Citizen)]
114#[citizen(symbol = "agent-runner/ModelUsage", version = 1)]
115pub struct ModelUsage {
116    /// Count of input tokens when reported by the backend.
117    pub input_tokens: Option<u64>,
118    /// Count of output tokens when reported by the backend.
119    pub output_tokens: Option<u64>,
120    /// End-to-end latency in milliseconds when available.
121    pub latency_ms: Option<u64>,
122    /// Monetary cost estimate in USD when available.
123    pub cost_usd: Option<f64>,
124    /// Open extension fields preserved for runner-specific accounting data.
125    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
159/// Returns the citizen class symbol for [`ModelRequest`].
160pub fn model_request_class_symbol() -> Symbol {
161    Symbol::qualified("agent-runner", "ModelRequest")
162}
163
164/// Returns the citizen class symbol for [`ModelResponse`].
165pub fn model_response_class_symbol() -> Symbol {
166    Symbol::qualified("agent-runner", "ModelResponse")
167}
168
169/// Returns the citizen class symbol for [`ModelUsage`].
170pub fn model_usage_class_symbol() -> Symbol {
171    Symbol::qualified("agent-runner", "ModelUsage")
172}