runtime/prefix/breakpoint.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 breakpoint plan: where one request's explicit prompt-cache
7//! breakpoints sit.
8//!
9//! **One author.** The system blocks, the conversation and the
10//! `prompt_assembled` record all read their breakpoints from this plan,
11//! so a record cannot name a breakpoint the request does not carry. A
12//! dialect only spells the marks it is handed; it decides none of them.
13
14use kernel::{ChatMessage, MessageBreakpoint};
15
16use super::segment::SegmentSlot;
17
18/// One explicit prompt-cache breakpoint a request carries.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Breakpoint {
21 /// The end of a frozen segment.
22 Edge(SegmentSlot),
23 /// The last message of the conversation, whatever its role.
24 Tail,
25}
26
27impl Breakpoint {
28 /// The spelling `prompt_assembled` records: the slot name for an
29 /// edge, `tail` for the conversation's last message.
30 pub fn as_str(self) -> &'static str {
31 match self {
32 Breakpoint::Edge(slot) => slot.as_str(),
33 Breakpoint::Tail => "tail",
34 }
35 }
36}
37
38/// The breakpoints one request carries: every segment edge but the
39/// last, and the tail of the conversation when there is one.
40///
41/// The run segment's edge carries none because the tail anchor sits
42/// right after it and covers it, and a fifth breakpoint is one the
43/// provider refuses (`CACHE_BREAKPOINTS_MAX`). The tail is the last
44/// message rather than the last assistant message: trailing tool results
45/// ride in the last message, and anchoring before them pays for them
46/// again on every request (`crates/runtime/spec/Turn.lean` ยง8-6).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct BreakpointPlan {
49 tail: Option<usize>,
50}
51
52impl BreakpointPlan {
53 /// The plan for a request carrying `messages`.
54 pub fn for_conversation(messages: &[ChatMessage]) -> BreakpointPlan {
55 BreakpointPlan {
56 tail: messages.len().checked_sub(1),
57 }
58 }
59
60 /// Whether the end of `slot` carries a breakpoint. The edges do not
61 /// depend on the conversation, so a request built without one (a
62 /// probe) asks the same question.
63 pub fn marks_edge(slot: SegmentSlot) -> bool {
64 match slot {
65 SegmentSlot::City | SegmentSlot::Building | SegmentSlot::Resident => true,
66 SegmentSlot::Run => false,
67 }
68 }
69
70 /// The message breakpoint a request carries under this plan, so a
71 /// request carries exactly the message breakpoint the plan names.
72 pub fn message_breakpoint(&self) -> MessageBreakpoint {
73 match self.tail {
74 Some(_) => MessageBreakpoint::Tail,
75 None => MessageBreakpoint::Unmarked,
76 }
77 }
78
79 /// Every breakpoint in request order: the edges, then the tail.
80 pub fn breakpoints(&self) -> Vec<Breakpoint> {
81 SegmentSlot::ALL
82 .into_iter()
83 .filter(|slot| BreakpointPlan::marks_edge(*slot))
84 .map(Breakpoint::Edge)
85 .chain(self.tail.map(|_| Breakpoint::Tail))
86 .collect()
87 }
88}