Skip to main content

kernel/
model.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The model seam (seam registry ARCHITECTURE 3).
7//! Every provider call carries a `BuildingPolicy` value; the policy is
8//! *defined* here (kernel cannot name outer crates) and *evaluated* by
9//! `city::policy` โ€” dependency inversion, same as the ledger seam.
10//! The part `crates/kernel/spec/Model.lean` specifies this module.
11
12mod image;
13mod mode;
14mod seam;
15mod usage;
16mod window;
17mod wire;
18
19pub use image::{ImageRef, ImageType};
20pub use mode::{AdmissionRequirement, LandingPolicy, Mode, RunPolicy};
21pub use seam::{ModelRequest, ModelReturn, content_from_message, message_payload};
22pub use usage::{CacheCount, ModelUsage};
23pub use window::Window;
24pub use wire::{
25    BuildingPolicy, Ceiling, ChatMessage, ChatRequest, ChatResponse, ContentBlock, DialectKind,
26    Effort, Increment, MessageBreakpoint, ModelTag, Role, StopReason, SystemBlock, ToolDef,
27};
28
29use crate::error::AxError;
30use crate::tool::ToolCall;
31
32/// Text arriving from a provider before the call has finished.
33///
34/// One argument and no return value, because an increment is not a
35/// decision: nothing downstream may branch on it, and a sink that could
36/// refuse would make a display detail able to fail a call.
37///
38/// **What arrives here is not history.** The record of what the model
39/// said is written once, from [`ModelReturn`], after the call settles.
40/// Increments are a thing to look at while waiting, and the two can
41/// disagree โ€” a provider may revise, and a cut stream leaves increments
42/// behind that no `ModelReturn` ever confirms. Where they disagree the
43/// settled text wins, and that rule is held on the far side of the wire
44/// by the client.
45pub type Increments<'a> = &'a mut dyn FnMut(&Increment);
46
47/// A tool call handed over the moment its block is complete, before the
48/// call it belongs to has settled.
49///
50/// Separate from [`Increments`] because a caller acts on it: it may start
51/// a read-only tool while the model is still generating. **What arrives
52/// here is not history either.** The ledger records tool calls from
53/// [`ModelReturn`] after the call settles; when the call fails, a caller
54/// discards whatever it derived from these. What a caller may start, and
55/// in which order it records the results, is fixed by
56/// `crates/runtime/spec/Turn/Speculation.lean`.
57pub type EarlyCalls<'a> = &'a mut dyn FnMut(&ToolCall);
58
59/// The accounts an adapter can call as, in priority order, and the one
60/// its next request goes out on. Answered once before a round opens
61/// (`crates/kernel/spec/AccountRecovery.lean` ยง8-86), never per attempt.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct AccountRoster {
64    pub provider: String,
65    /// The order is the priority.
66    pub accounts: Vec<RosterEntry>,
67    /// The account the next request goes out on.
68    pub current: crate::ServerLabel,
69    /// How many more times one account is asked before the next takes
70    /// the request.
71    pub retries: crate::account_recovery::AccountRetries,
72}
73
74/// One account of a roster, and whether its reference can be redeemed
75/// right now (kernel D55). Neither the key nor the reference is here.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct RosterEntry {
78    pub account: crate::ServerLabel,
79    pub usable: bool,
80}
81
82/// The model port. Production adapter: gateway::endpoint;
83/// second adapter: citysim scripted model. Implementations never sample
84/// clocks or read global state.
85pub trait Model {
86    /// The non-secret identity recorded before each actual request: the
87    /// roster's provider and current account, answered without asking
88    /// the vault.
89    fn provider_account(&self) -> Option<crate::event::record::ProviderAccountBinding> {
90        None
91    }
92    /// The accounts this adapter can call as; `None` for an adapter with
93    /// no named account. With two accounts or more each entry's
94    /// `usable` asks the vault, so a caller asks once per round.
95    fn account_roster(&self) -> Option<AccountRoster> {
96        None
97    }
98    /// Selects the account every later request of this adapter uses,
99    /// without sending a request; a name outside the roster leaves the
100    /// selection as it was. The credential is redeemed at call time, so
101    /// selecting cannot fail.
102    fn select_account(&mut self, _account: &crate::ServerLabel) {}
103    fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError>;
104
105    /// The same call, reporting text as it arrives.
106    ///
107    /// The default answers by calling [`Model::call`] and reporting
108    /// nothing, which is the honest behaviour for an adapter that has no
109    /// stream: a caller gets the same `ModelReturn` at the same moment
110    /// and simply sees no increments. An adapter that overrides this owes
111    /// the same `ModelReturn` it would have returned from `call`,
112    /// including the same failures โ€” a stream cut halfway is a read
113    /// error, never a shortened answer.
114    ///
115    /// # Errors
116    /// Whatever [`Model::call`] would fail with.
117    fn call_streaming(
118        &mut self,
119        req: &ModelRequest,
120        _onto: Increments<'_>,
121    ) -> Result<ModelReturn, AxError> {
122        self.call(req)
123    }
124
125    /// The streaming call, also handing over each tool call as soon as it
126    /// is complete.
127    ///
128    /// The default hands nothing over and answers with
129    /// [`Model::call_streaming`], which is honest for an adapter whose
130    /// wire cannot say when a call is complete: the caller simply gets no
131    /// head start. An adapter that overrides this owes the same
132    /// `ModelReturn` and failures as `call_streaming`, and every call it
133    /// hands over equals the one in that `ModelReturn`.
134    ///
135    /// # Errors
136    /// Whatever [`Model::call_streaming`] would fail with.
137    fn call_speculating(
138        &mut self,
139        req: &ModelRequest,
140        onto: Increments<'_>,
141        _early: EarlyCalls<'_>,
142    ) -> Result<ModelReturn, AxError> {
143        self.call_streaming(req, onto)
144    }
145}
146
147#[cfg(feature = "conformance")]
148pub mod conformance;
149
150#[cfg(test)]
151#[allow(
152    clippy::unwrap_used,
153    clippy::expect_used,
154    clippy::panic,
155    clippy::indexing_slicing,
156    reason = "test code"
157)]
158mod tests;