1use std::collections::BTreeSet;
12
13use kernel::consts_policy::{BYTES_PER_TOKEN, PREFIX_SLOTS, STARTUP_BUDGET_TOKENS};
14use kernel::event::record::{PromptAssembled, PromptSegment, PromptSkip, SkipReason};
15use kernel::{Address, AxCode, AxError, B3Hash, Payload, SystemBlock};
16
17use crate::elision::{self, Elided};
18
19mod breakpoint;
20mod segment;
21
22pub(crate) mod shape;
23pub mod warmth;
24
25pub use breakpoint::{Breakpoint, BreakpointPlan};
26
27pub(crate) use segment::ANOTHER_ADDRESS;
28pub use segment::{FrozenSegment, SegmentSlot, SegmentSource};
29
30pub(crate) const DOC_JOIN: &str = "\n\n";
33
34#[derive(Debug, Clone)]
37pub struct SourceDoc {
38 pub addr: Address,
39 pub bytes: Option<Vec<u8>>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct SegmentCaps {
47 pub city: u64,
48 pub building: u64,
49 pub resident: u64,
50 pub run: u64,
51}
52
53impl SegmentCaps {
54 pub fn startup_default() -> SegmentCaps {
55 let per_slot = STARTUP_BUDGET_TOKENS.saturating_mul(BYTES_PER_TOKEN) / PREFIX_SLOTS;
56 SegmentCaps {
57 city: per_slot,
58 building: per_slot,
59 resident: per_slot,
60 run: per_slot,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct PrefixPlan {
68 pub city: Vec<SourceDoc>,
69 pub building: Vec<SourceDoc>,
70 pub resident: Vec<SourceDoc>,
71 pub run: Vec<SourceDoc>,
72 pub caps: SegmentCaps,
73}
74
75pub fn build_prefix(plan: PrefixPlan) -> Result<FrozenPrefix, AxError> {
81 let mut seen: BTreeSet<String> = BTreeSet::new();
82 let mut skipped = Vec::new();
83 let mut built = Vec::new();
84 let slots = [
85 (SegmentSlot::City, &plan.city, plan.caps.city),
86 (SegmentSlot::Building, &plan.building, plan.caps.building),
87 (SegmentSlot::Resident, &plan.resident, plan.caps.resident),
88 (SegmentSlot::Run, &plan.run, plan.caps.run),
89 ];
90 for (slot, docs, cap) in slots {
91 let (segment, note) = build_segment(slot, docs, cap, &mut seen)?;
92 skipped.push(note);
93 built.push(segment);
94 }
95 let mut iter = built.into_iter();
96 let (Some(city), Some(building), Some(resident), Some(run)) =
97 (iter.next(), iter.next(), iter.next(), iter.next())
98 else {
99 return Err(AxError::failure(
100 AxCode::InvalidArgs,
101 "build prefix",
102 "segment construction lost a slot",
103 )
104 .with_recovery(
105 "report this against runtime::prefix: a prefix is four segments, city, \
106 building, resident and run, and fewer than four were built",
107 ));
108 };
109 let mut prefix = FrozenPrefix::assemble(city, building, resident, run)?;
110 prefix.skipped = Some(skipped);
111 Ok(prefix)
112}
113
114#[expect(
115 clippy::arithmetic_side_effects,
116 reason = "budget arithmetic on usize values bounded by document sizes already held \
117 in memory; every subtraction is guarded by a preceding comparison"
118)]
119fn build_segment(
120 slot: SegmentSlot,
121 docs: &[SourceDoc],
122 cap: u64,
123 seen: &mut BTreeSet<String>,
124) -> Result<(FrozenSegment, Vec<PromptSkip>), AxError> {
125 let cap = usize::try_from(cap).map_err(|_| {
126 AxError::failure(
127 AxCode::InvalidArgs,
128 "build prefix segment",
129 "cap exceeds usize",
130 )
131 .with_recovery(
132 "lower this slot's byte cap in `[prefix]`: it is larger than this machine \
133 can address",
134 )
135 })?;
136 let mut text = String::new();
137 let mut sources = Vec::new();
138 let mut skipped = Vec::new();
139 for doc in docs {
140 let addr = doc.addr.as_str().to_owned();
141 let left_out = |reason: SkipReason| PromptSkip {
142 addr: doc.addr.clone(),
143 reason,
144 };
145 if seen.contains(&addr) {
146 skipped.push(left_out(SkipReason::Duplicate));
147 continue;
148 }
149 let Some(bytes) = &doc.bytes else {
150 skipped.push(left_out(SkipReason::Unreadable));
151 continue;
152 };
153 let Ok(body) = std::str::from_utf8(bytes) else {
154 skipped.push(left_out(SkipReason::NotUtf8));
155 continue;
156 };
157 let joiner = if text.is_empty() { 0 } else { DOC_JOIN.len() };
158 let used = text.len() + joiner;
159 let remaining = cap.saturating_sub(used);
160 if body.len() <= remaining {
161 if joiner > 0 {
162 text.push_str(DOC_JOIN);
163 }
164 text.push_str(body);
165 seen.insert(addr.clone());
166 sources.push(SegmentSource::whole(
167 doc.addr.clone(),
168 u64::try_from(body.len()).unwrap_or(u64::MAX),
169 ));
170 continue;
171 }
172 let worst_marker = elision::marker_room(body.len());
173 if remaining <= worst_marker {
174 skipped.push(left_out(SkipReason::NoBudget));
175 continue;
176 }
177 let kept = elision::boundary_before(body, remaining.saturating_sub(worst_marker));
178 let cut = elision::splice(body, kept, body.len(), Elided::Tail);
182 if joiner > 0 {
183 text.push_str(DOC_JOIN);
184 }
185 text.push_str(&cut.text);
186 seen.insert(addr.clone());
187 sources.push(SegmentSource {
188 addr: doc.addr.clone(),
189 kept: u64::try_from(kept).unwrap_or(u64::MAX),
190 dropped: cut.dropped.get(),
191 });
192 }
193 Ok((
194 FrozenSegment::assembled(slot, text.into_bytes(), sources),
195 skipped,
196 ))
197}
198
199pub use segment::verified_system_hashes;
207
208#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct FrozenPrefix {
212 city: FrozenSegment,
213 building: FrozenSegment,
214 resident: FrozenSegment,
215 run: FrozenSegment,
216 skipped: Option<Vec<Vec<PromptSkip>>>,
221}
222
223impl FrozenPrefix {
224 pub fn assemble(
227 city: FrozenSegment,
228 building: FrozenSegment,
229 resident: FrozenSegment,
230 run: FrozenSegment,
231 ) -> Result<FrozenPrefix, AxError> {
232 let expected = [
233 (SegmentSlot::City, city.slot()),
234 (SegmentSlot::Building, building.slot()),
235 (SegmentSlot::Resident, resident.slot()),
236 (SegmentSlot::Run, run.slot()),
237 ];
238 for (want, got) in expected {
239 if want != got {
240 return Err(AxError::failure(
241 AxCode::InvalidArgs,
242 "assemble frozen prefix",
243 format!("slot {} holds a {} segment", want.as_str(), got.as_str()),
244 )
245 .with_recovery("pass segments in slot order: city, building, resident, run"));
246 }
247 }
248 Ok(FrozenPrefix {
249 city,
250 building,
251 resident,
252 run,
253 skipped: None,
254 })
255 }
256
257 pub fn system_blocks(&self) -> Result<Vec<SystemBlock>, AxError> {
263 self.segments()
264 .iter()
265 .map(|segment| {
266 let text = std::str::from_utf8(segment.bytes())
267 .map_err(|_| {
268 AxError::failure(
269 AxCode::InvalidArgs,
270 "render system blocks",
271 format!("{} segment is not utf-8", segment.slot().as_str()),
272 )
273 .with_recovery(format!(
274 "save the documents feeding the {} slot as UTF-8; a model \
275 prompt carries text and nothing else",
276 segment.slot().as_str()
277 ))
278 })?
279 .to_owned();
280 Ok(SystemBlock {
281 text,
282 cache: BreakpointPlan::marks_edge(segment.slot()),
283 })
284 })
285 .collect()
286 }
287
288 pub fn segments(&self) -> [&FrozenSegment; 4] {
289 [&self.city, &self.building, &self.resident, &self.run]
290 }
291
292 pub fn segment_hashes(&self) -> [B3Hash; 4] {
293 [
294 *self.city.hash(),
295 *self.building.hash(),
296 *self.resident.hash(),
297 *self.run.hash(),
298 ]
299 }
300
301 pub fn verified_segment_hashes(&self) -> Result<[B3Hash; 4], AxError> {
312 let [city, building, resident, run] = self.segments();
313 Ok([
314 city.verified_hash()?,
315 building.verified_hash()?,
316 resident.verified_hash()?,
317 run.verified_hash()?,
318 ])
319 }
320
321 pub fn prompt_payload(&self, plan: &BreakpointPlan) -> Result<Payload, AxError> {
336 let mut segments = Vec::new();
337 for (index, segment) in self.segments().into_iter().enumerate() {
338 let len = u64::try_from(segment.bytes().len()).map_err(|_| {
339 AxError::failure(
340 AxCode::InvalidArgs,
341 "encode prompt payload",
342 "segment length exceeds u64",
343 )
344 .with_recovery(
345 "lower the slot byte caps in `[prefix]`: one frozen segment is \
346 longer than a byte count this city can hold",
347 )
348 })?;
349 segments.push(PromptSegment {
350 slot: segment.slot().as_str().to_owned(),
351 hash: *segment.hash(),
352 len,
353 sources: segment.sources().iter().map(SegmentSource::row).collect(),
354 skipped: self
355 .skipped
356 .as_ref()
357 .and_then(|all| all.get(index))
358 .cloned()
359 .unwrap_or_default(),
360 });
361 }
362 Payload::of(&PromptAssembled {
363 segments,
364 breakpoints: plan
365 .breakpoints()
366 .into_iter()
367 .map(|breakpoint| breakpoint.as_str().to_owned())
368 .collect(),
369 })
370 }
371}
372
373#[cfg(test)]
374#[allow(
375 clippy::unwrap_used,
376 clippy::expect_used,
377 clippy::panic,
378 clippy::indexing_slicing,
379 reason = "test code"
380)]
381mod tests;