sprawling-kernel 0.0.12

Every decision of a sprawling city - addresses, events, gates, plans and errors - with nothing that touches a disk.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! The canonical conversation vocabulary: what a request and a response
//! are made of, in the city dialect every adapter translates from.
//! The part `crates/kernel/spec/Model.lean` specifies this module with the
//! rest of `kernel::model`.

use std::borrow::Cow;
use std::num::NonZeroU64;

use serde::{Deserialize, Serialize};

use crate::event::Payload;
use crate::model::image::ImageRef;
use crate::model::usage::ModelUsage;
use crate::tool::ToolName;
/// Building-level constraints riding along the call. It carries the one
/// load-bearing bit; further fields only grow, and each one is
/// added at every construction site the compiler names.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildingPolicy {
    /// Confidential buildings lock the call to the local model pool;
    /// content never leaves the machine (11.4).
    pub confidential: bool,
}

impl BuildingPolicy {
    pub fn new(confidential: bool) -> Self {
        BuildingPolicy { confidential }
    }
}

/// Message author on the canonical (Anthropic-shaped) conversation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Role {
    User,
    Assistant,
}

/// Why the provider stopped. Wire-borne and closed: a fourth reason has
/// to be answered by every dialect that maps one (14.3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
    EndTurn,
    ToolUse,
    MaxTokens,
}

/// The most a model may emit in one response, thinking included.
///
/// **Zero is unrepresentable, and absence is not zero.** A provider
/// answers a zero ceiling with nothing at all, and a reply with nothing
/// in it reads downstream as work that finished — so a figure nobody
/// registered has to be carried as a figure nobody registered, and the
/// dialect that cannot write the request without one refuses the call
/// instead of inventing a number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Ceiling(NonZeroU64);

impl Ceiling {
    /// `None` for zero, which is not a ceiling.
    pub const fn new(tokens: u64) -> Option<Ceiling> {
        match NonZeroU64::new(tokens) {
            Some(tokens) => Some(Ceiling(tokens)),
            None => None,
        }
    }

    pub const fn get(self) -> u64 {
        self.0.get()
    }
}

impl std::fmt::Display for Ceiling {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0.get())
    }
}

/// One frozen-prefix block on the wire. `cache` marks an explicit prompt
/// cache breakpoint (breakpoints sit on segment edges).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemBlock {
    pub text: String,
    pub cache: bool,
}

/// Which wire the far side speaks. Closed: a fourth dialect is a
/// compile error at every entrance of `gateway::dialect`, which is how
/// it is kept from being approximated with the nearest of the three
/// already written.
///
/// Lives here rather than in `gateway` for the same reason
/// [`BuildingPolicy`] does: two outer crates must name it (the gateway
/// translates it, the wire carries it) and neither may name the other.
/// `gateway::dialect` is its evaluator, not its definition.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum DialectKind {
    Anthropic,
    /// OpenAI's chat completions face, which most relays, most local
    /// servers and most other laboratories also serve.
    OpenAi,
    /// OpenAI's responses face: a different request body, a different
    /// reply shape and a different set of stream event names.
    ///
    /// Registered apart from [`DialectKind::OpenAi`] because a person
    /// who pasted a responses URL said which face they meant, and a
    /// registration that folded the two would send chat bodies to a
    /// path that answers 404.
    OpenAiResponses,
}

/// What a chosen model is for. Exhaustive rather than a free label: a
/// tag exists because some code asks for a model by it, so a tag with no
/// asker is a setting the person can fill in and never see used. It
/// grows when a caller appears, not when a name is imagined.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum ModelTag {
    /// The model a resident thinks with.
    Main,
    /// The small model that reads long documents so the main one does
    /// not have to: summaries, structure trees, search results.
    Digest,
    /// The model that turns a recording into text.
    ///
    /// A tag rather than a store of its own, because "which endpoint and
    /// which model answer this kind of job" already has a mechanism: a
    /// person attaches an endpoint and chooses a model for a tag. A
    /// second form and a second file would be a second answer to one
    /// question, and the credential would need a second way into the
    /// vault.
    Transcribe,
    /// The model that reads text out of a picture: one a person
    /// registered as able to read images. The city's OCR tool asks for
    /// it, and with none chosen that tool says so rather than handing a
    /// picture to a model that only reads text (`crates/kernel/spec/Model.lean` §8-80).
    Ocr,
}

impl ModelTag {
    /// Every tag, in the order a settings page should offer them.
    pub const ALL: [ModelTag; 4] = [
        ModelTag::Main,
        ModelTag::Digest,
        ModelTag::Transcribe,
        ModelTag::Ocr,
    ];

    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            ModelTag::Main => "main",
            ModelTag::Digest => "digest",
            ModelTag::Transcribe => "transcribe",
            ModelTag::Ocr => "ocr",
        }
    }
}

impl std::fmt::Display for ModelTag {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// How hard the provider should think, least to most, in the upstreams' words. Which levels one
/// (Endpoint, model) offers is the upstream's statement, answered by the gateway's thinking
/// ladder and never by a table here (`crates/kernel/spec/Model.lean` D56); a level outside the
/// offer is refused, never moved to a neighbour. Check the vendors before changing the ladder:
/// <https://platform.claude.com/docs/en/build-with-claude/effort>,
/// <https://developers.openai.com/api/docs/guides/reasoning>.
///
/// Absence (`Option::None`) is not `Effort::None`: absence means nobody stated a level and the
/// default rule resolves one ([`crate::consts_policy::DEFAULT_EFFORT`]). `Effort::None` survives
/// only so ledgers that asked a provider not to think read back; no picker offers it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Effort {
    None,
    Minimal,
    Low,
    Medium,
    High,
    #[serde(rename = "xhigh")]
    XHigh,
    Max,
}

impl Effort {
    /// Every setting, in the order a person is offered them.
    ///
    /// The one list. A refusal that spells the allowed values, a form
    /// that offers them and a document that names them all read this,
    /// so an eighth setting cannot appear in one of the three and be
    /// missing from the other two.
    pub const ALL: [Effort; 7] = [
        Effort::None,
        Effort::Minimal,
        Effort::Low,
        Effort::Medium,
        Effort::High,
        Effort::XHigh,
        Effort::Max,
    ];

    /// The word the wire and every configuration file spell it with.
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Effort::None => "none",
            Effort::Minimal => "minimal",
            Effort::Low => "low",
            Effort::Medium => "medium",
            Effort::High => "high",
            Effort::XHigh => "xhigh",
            Effort::Max => "max",
        }
    }
}

/// One piece of what a model is producing, before the call it belongs to
/// has settled.
///
/// **Reasoning and prose are two streams, not one.** A model that spends
/// most of a call reasoning sends almost nothing on the prose stream,
/// and a page that appended both to one buffer would either show a
/// person their own model's scratch work as its answer or show them an
/// empty thread for three minutes. Which stream a piece came from is
/// therefore part of the piece, and no reader has to guess.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Increment {
    /// Prose, which is the answer as it arrives.
    Said(String),
    /// Reasoning, which is how the answer was arrived at.
    Thought(String),
}

/// Canonical content block. Tool inputs are [`Payload`] — the float ban
/// holds here because these bytes become ledger payloads verbatim.
///
/// `Thinking` and `RedactedThinking` are carried unchanged end to end:
/// the provider verifies `signature` against the reasoning it issued, and
/// altering either block earns a 400 that names them as unmodifiable.
/// The block shapes are the city dialect's, so
/// <https://platform.claude.com/docs/en/build-with-claude/thinking> is
/// what a change to them has to agree with.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ContentBlock {
    Text {
        text: String,
    },
    Thinking {
        thinking: String,
        signature: String,
    },
    RedactedThinking {
        data: String,
    },
    ToolUse {
        id: String,
        name: ToolName,
        input: Payload,
    },
    /// A picture the model can see. Carries the reference and the
    /// dimensions; the bytes are fetched at the wire by whoever is about
    /// to send them (`gateway::endpoint`), so this block stays a thing a
    /// ledger can hold.
    Image(ImageRef),
    ToolResult {
        tool_use_id: String,
        content: String,
        is_error: bool,
        /// Pictures the tool produced. Absent from every tool result
        /// written before this field existed, which is why it reads as
        /// an empty list rather than as a broken record: an old history
        /// has to replay.
        #[serde(default)]
        attachments: Vec<ImageRef>,
    },
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatMessage {
    pub role: Role,
    pub content: Vec<ContentBlock>,
}

/// Which message of a request carries an explicit prompt-cache
/// breakpoint.
///
/// A request-side annotation rather than a mark on a message, so a
/// request can borrow the conversation it carries: marking a message
/// would need a writable copy of it. `runtime::prefix::BreakpointPlan`
/// is the only producer, and it anchors the tail - so everything a
/// request carries, trailing tool results included, sits inside the
/// region a later request can hit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MessageBreakpoint {
    #[default]
    Unmarked,
    Tail,
}

/// How many of a request's leading system blocks every resident of one
/// building carries byte for byte (`crates/kernel/spec/Model.lean` D60).
///
/// A request-side annotation like [`MessageBreakpoint`]: `runtime` sets
/// it from the frozen prefix, and the gateway's prefix hold reads it
/// through [`ChatRequest::shared_system`] to build the one pre-warm a
/// cold wave of calls waits behind. Zero is not a count here: a request
/// that shares nothing says [`SharedPrefix::Unshared`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SharedPrefix {
    #[default]
    Unshared,
    Leading(std::num::NonZeroUsize),
}

/// A tool as the provider sees it; sourced from catalog `tool_defs` only.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolDef {
    pub name: ToolName,
    pub description: String,
    pub input_schema: Payload,
}

/// The canonical request conversation (city dialect = Anthropic Messages). Both production dialects and the scripted model
/// consume this one shape — the seam carries it so replay, citysim and
/// the real gateway argue about the same object.
///
/// `messages` and `tools` are borrowed on the turn's path, because a
/// request lives for one call; a holder that keeps a request past the
/// call (the keep-warm renewal) owns a `ChatRequest<'static>`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatRequest<'a> {
    pub model: String,
    /// Absent when no catalogue row states this model's ceiling. The
    /// OpenAI wire then omits the field and takes the provider's own
    /// default; the Anthropic wire, which requires it, refuses.
    pub max_tokens: Option<Ceiling>,
    pub system: Vec<SystemBlock>,
    pub messages: Cow<'a, [ChatMessage]>,
    pub tools: Cow<'a, [ToolDef]>,
    /// Outside serde, like every request-side annotation: the bytes a
    /// ledger or a transcript records are the same with and without it.
    #[serde(skip)]
    pub breakpoint: MessageBreakpoint,
    /// Outside serde for the same reason as `breakpoint`.
    #[serde(skip)]
    pub shared: SharedPrefix,
    /// Frozen at run start: changing it mid-run would start a new cached
    /// prompt prefix, so the value rides in from [`crate::FrozenConfig`].
    pub effort: Option<Effort>,
}

/// The canonical response: dialect output, endpoint input.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatResponse {
    pub content: Vec<ContentBlock>,
    pub stop: StopReason,
    pub usage: ModelUsage,
}

impl ChatRequest<'_> {
    /// An empty conversation shell for adapters and tests that argue
    /// about hashes, not content.
    pub fn empty(model: &str, max_tokens: Ceiling) -> ChatRequest<'static> {
        ChatRequest {
            model: model.to_owned(),
            max_tokens: Some(max_tokens),
            system: Vec::new(),
            messages: Cow::Borrowed(&[]),
            tools: Cow::Borrowed(&[]),
            breakpoint: MessageBreakpoint::Unmarked,
            shared: SharedPrefix::Unshared,
            effort: None,
        }
    }

    /// Whether the message at `index` carries this request's breakpoint.
    /// The one reading of [`MessageBreakpoint`]; a dialect only spells
    /// the answer on its wire.
    pub fn carries_breakpoint(&self, index: usize) -> bool {
        match self.breakpoint {
            MessageBreakpoint::Unmarked => false,
            MessageBreakpoint::Tail => index.checked_add(1) == Some(self.messages.len()),
        }
    }

    /// The system blocks every resident of one building carries: the
    /// one reading of [`SharedPrefix`]. `None` when the request shares
    /// nothing, or names more blocks than it carries.
    pub fn shared_system(&self) -> Option<&[SystemBlock]> {
        match self.shared {
            SharedPrefix::Unshared => None,
            SharedPrefix::Leading(count) => self.system.get(..count.get()),
        }
    }

    /// The same request, owning what it borrowed: for a holder that
    /// keeps it past the call it was assembled for.
    pub fn into_owned(self) -> ChatRequest<'static> {
        ChatRequest {
            model: self.model,
            max_tokens: self.max_tokens,
            system: self.system,
            messages: Cow::Owned(self.messages.into_owned()),
            tools: Cow::Owned(self.tools.into_owned()),
            breakpoint: self.breakpoint,
            shared: self.shared,
            effort: self.effort,
        }
    }
}