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;