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;