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, SharedPrefix, StopReason, SystemBlock,
27    ToolDef,
28};
29
30use crate::error::AxError;
31use crate::tool::ToolCall;
32
33/// Text arriving from a provider before the call has finished.
34///
35/// One argument and no return value, because an increment is not a
36/// decision: nothing downstream may branch on it, and a sink that could
37/// refuse would make a display detail able to fail a call.
38///
39/// **What arrives here is not history.** The record of what the model
40/// said is written once, from [`ModelReturn`], after the call settles.
41/// Increments are a thing to look at while waiting, and the two can
42/// disagree โ€” a provider may revise, and a cut stream leaves increments
43/// behind that no `ModelReturn` ever confirms. Where they disagree the
44/// settled text wins, and that rule is held on the far side of the wire
45/// by the client.
46pub type Increments<'a> = &'a mut dyn FnMut(&Increment);
47
48/// A tool call handed over the moment its block is complete, before the
49/// call it belongs to has settled.
50///
51/// Separate from [`Increments`] because a caller acts on it: it may start
52/// a read-only tool while the model is still generating. **What arrives
53/// here is not history either.** The ledger records tool calls from
54/// [`ModelReturn`] after the call settles; when the call fails, a caller
55/// discards whatever it derived from these. What a caller may start, and
56/// in which order it records the results, is fixed by
57/// `crates/runtime/spec/Turn/Speculation.lean`.
58pub type EarlyCalls<'a> = &'a mut dyn FnMut(&ToolCall);
59
60/// The accounts an adapter can call as, in priority order, and the one
61/// its next request goes out on. Answered once before a round opens
62/// (`crates/kernel/spec/AccountRecovery.lean` ยง8-86), never per attempt.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct AccountRoster {
65    pub provider: String,
66    /// The order is the priority.
67    pub accounts: Vec<RosterEntry>,
68    /// The account the next request goes out on.
69    pub current: crate::ServerLabel,
70    /// How many more times one account is asked before the next takes
71    /// the request.
72    pub retries: crate::account_recovery::AccountRetries,
73}
74
75/// One account of a roster, and whether its reference can be redeemed
76/// right now (kernel D55). Neither the key nor the reference is here.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct RosterEntry {
79    pub account: crate::ServerLabel,
80    pub usable: bool,
81}
82
83/// The model port. Production adapter: gateway::endpoint;
84/// second adapter: citysim scripted model. Implementations never sample
85/// clocks or read global state.
86pub trait Model {
87    /// The non-secret identity recorded before each actual request: the
88    /// roster's provider and current account, answered without asking
89    /// the vault.
90    fn provider_account(&self) -> Option<crate::event::record::ProviderAccountBinding> {
91        None
92    }
93    /// The accounts this adapter can call as; `None` for an adapter with
94    /// no named account. With two accounts or more each entry's
95    /// `usable` asks the vault, so a caller asks once per round.
96    fn account_roster(&self) -> Option<AccountRoster> {
97        None
98    }
99    /// Selects the account every later request of this adapter uses,
100    /// without sending a request; a name outside the roster leaves the
101    /// selection as it was. The credential is redeemed at call time, so
102    /// selecting cannot fail.
103    fn select_account(&mut self, _account: &crate::ServerLabel) {}
104    /// The wait the last call took at its endpoint's gate behind a
105    /// pre-warm of its shared prefix (`crates/kernel/spec/Model.lean`
106    /// D60); `None` when it took none. Asked after every attempt, on
107    /// success and on failure, by the turn that writes the
108    /// `prefix_held` line; an adapter with no gate answers `None`.
109    fn prefix_held(&self) -> Option<crate::event::record::PrefixHeld> {
110        None
111    }
112    fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError>;
113
114    /// The same call, reporting text as it arrives.
115    ///
116    /// The default answers by calling [`Model::call`] and reporting
117    /// nothing, which is the honest behaviour for an adapter that has no
118    /// stream: a caller gets the same `ModelReturn` at the same moment
119    /// and simply sees no increments. An adapter that overrides this owes
120    /// the same `ModelReturn` it would have returned from `call`,
121    /// including the same failures โ€” a stream cut halfway is a read
122    /// error, never a shortened answer.
123    ///
124    /// # Errors
125    /// Whatever [`Model::call`] would fail with.
126    fn call_streaming(
127        &mut self,
128        req: &ModelRequest,
129        _onto: Increments<'_>,
130    ) -> Result<ModelReturn, AxError> {
131        self.call(req)
132    }
133
134    /// The streaming call, also handing over each tool call as soon as it
135    /// is complete.
136    ///
137    /// The default hands nothing over and answers with
138    /// [`Model::call_streaming`], which is honest for an adapter whose
139    /// wire cannot say when a call is complete: the caller simply gets no
140    /// head start. An adapter that overrides this owes the same
141    /// `ModelReturn` and failures as `call_streaming`, and every call it
142    /// hands over equals the one in that `ModelReturn`.
143    ///
144    /// # Errors
145    /// Whatever [`Model::call_streaming`] would fail with.
146    fn call_speculating(
147        &mut self,
148        req: &ModelRequest,
149        onto: Increments<'_>,
150        _early: EarlyCalls<'_>,
151    ) -> Result<ModelReturn, AxError> {
152        self.call_streaming(req, onto)
153    }
154}
155
156#[cfg(feature = "conformance")]
157pub mod conformance;
158
159#[cfg(test)]
160#[allow(
161    clippy::unwrap_used,
162    clippy::expect_used,
163    clippy::panic,
164    clippy::indexing_slicing,
165    reason = "test code"
166)]
167mod tests;