Skip to main content

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}