1use std::cmp::Ordering;
4use std::sync::Arc;
5
6use crate::llm::provider::ToolDefinition;
7
8use super::hash_utils::{hash_tool_definitions, hash_value};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SegmentBoundaryReason {
13 Compaction,
14 Recovery,
15 ToolCatalogEpoch,
16 PrimaryAgent,
17 Model,
18 Provider,
19 Mode,
20 Instructions,
21}
22
23impl SegmentBoundaryReason {
24 #[must_use]
25 pub const fn as_str(self) -> &'static str {
26 match self {
27 Self::Compaction => "compaction",
28 Self::Recovery => "recovery",
29 Self::ToolCatalogEpoch => "tool_catalog_epoch",
30 Self::PrimaryAgent => "primary_agent",
31 Self::Model => "model",
32 Self::Provider => "provider",
33 Self::Mode => "mode",
34 Self::Instructions => "instructions",
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct SessionRequestEnvelope {
42 segment_id: Arc<str>,
43 system_prompt: Arc<str>,
44 ordered_tools: Arc<Vec<ToolDefinition>>,
45 instruction_digest: u64,
46 prefix_hash: u64,
47 catalog_hash: Option<u64>,
48}
49
50impl SessionRequestEnvelope {
51 #[must_use]
54 pub fn new(
55 segment_id: impl Into<Arc<str>>,
56 system_prompt: impl Into<Arc<str>>,
57 tools: Vec<ToolDefinition>,
58 instruction_digest: u64,
59 ) -> Self {
60 let system_prompt = system_prompt.into();
61 let prefix_hash = hash_value(&system_prompt);
62 Self::with_prefix_hash(segment_id, system_prompt, tools, instruction_digest, prefix_hash)
63 }
64
65 #[must_use]
68 pub fn with_prefix_hash(
69 segment_id: impl Into<Arc<str>>,
70 system_prompt: impl Into<Arc<str>>,
71 mut tools: Vec<ToolDefinition>,
72 instruction_digest: u64,
73 prefix_hash: u64,
74 ) -> Self {
75 tools.sort_by(compare_tools);
76 let system_prompt = system_prompt.into();
77 let catalog_hash = if tools.is_empty() {
78 None
79 } else {
80 hash_tool_definitions(Some(&tools))
81 };
82 Self {
83 segment_id: segment_id.into(),
84 system_prompt,
85 ordered_tools: Arc::new(tools),
86 instruction_digest,
87 prefix_hash,
88 catalog_hash,
89 }
90 }
91
92 #[must_use]
93 pub fn segment_id(&self) -> &str {
94 &self.segment_id
95 }
96
97 #[must_use]
98 pub fn system_prompt(&self) -> Arc<str> {
99 Arc::clone(&self.system_prompt)
100 }
101
102 #[must_use]
103 pub fn ordered_tools(&self) -> Arc<Vec<ToolDefinition>> {
104 Arc::clone(&self.ordered_tools)
105 }
106
107 #[must_use]
108 pub fn instruction_digest(&self) -> u64 {
109 self.instruction_digest
110 }
111
112 #[must_use]
113 pub fn prefix_hash(&self) -> u64 {
114 self.prefix_hash
115 }
116
117 #[must_use]
119 pub fn with_capability_digest(mut self, digest: u64) -> Self {
120 self.prefix_hash = hash_value(&(self.prefix_hash, digest));
121 self
122 }
123
124 #[must_use]
125 pub fn catalog_hash(&self) -> Option<u64> {
126 self.catalog_hash
127 }
128
129 #[must_use]
132 pub fn begin_segment(&self, segment_id: impl Into<Arc<str>>) -> Self {
133 let mut next = self.clone();
134 next.segment_id = segment_id.into();
135 next
136 }
137}
138
139fn core_priority(name: &str) -> u8 {
140 match name {
141 "exec_command" => 0,
142 "write_stdin" => 1,
143 "search_tools" | "mcp_search_tools" => 2,
144 "apply_patch" => 3,
145 _ => 4,
146 }
147}
148
149fn compare_tools(left: &ToolDefinition, right: &ToolDefinition) -> Ordering {
150 let left_name = left.function_name();
151 let right_name = right.function_name();
152 core_priority(left_name)
153 .cmp(&core_priority(right_name))
154 .then_with(|| left_name.cmp(right_name))
155 .then_with(|| left.tool_type.cmp(&right.tool_type))
156}
157
158#[cfg(test)]
159mod tests {
160 use serde_json::json;
161
162 use super::*;
163
164 fn tool(name: &str) -> ToolDefinition {
165 ToolDefinition::function(name.to_string(), name.to_string(), json!({"type": "object"}))
166 }
167
168 #[test]
169 fn equivalent_cold_starts_are_byte_identical() {
170 let left = SessionRequestEnvelope::new(
171 "segment-1",
172 "fixed prompt",
173 vec![tool("zeta"), tool("exec_command"), tool("alpha")],
174 42,
175 );
176 let right = SessionRequestEnvelope::new(
177 "segment-1",
178 "fixed prompt",
179 vec![tool("alpha"), tool("zeta"), tool("exec_command")],
180 42,
181 );
182
183 assert_eq!(left.system_prompt.as_bytes(), right.system_prompt.as_bytes());
184 assert_eq!(
185 serde_json::to_vec(left.ordered_tools.as_ref()).expect("serialize left catalog"),
186 serde_json::to_vec(right.ordered_tools.as_ref()).expect("serialize right catalog")
187 );
188 assert_eq!(left.catalog_hash, right.catalog_hash);
189 }
190
191 #[test]
192 fn core_priority_precedes_name_order() {
193 let envelope = SessionRequestEnvelope::new(
194 "segment-1",
195 "fixed prompt",
196 vec![
197 tool("alpha"),
198 tool("apply_patch"),
199 tool("search_tools"),
200 tool("exec_command"),
201 ],
202 42,
203 );
204 let names = envelope
205 .ordered_tools
206 .iter()
207 .map(ToolDefinition::function_name)
208 .collect::<Vec<_>>();
209
210 assert_eq!(names, ["exec_command", "search_tools", "apply_patch", "alpha"]);
211 }
212
213 #[test]
214 fn constructor_preserves_instruction_digest_and_prefix_override() {
215 let envelope = SessionRequestEnvelope::new("segment-1", "fixed prompt", vec![], 42);
216 assert_eq!(envelope.instruction_digest(), 42);
217 assert_eq!(envelope.prefix_hash(), hash_value(&"fixed prompt"));
218
219 let stable = SessionRequestEnvelope::with_prefix_hash("segment-1", "runtime prompt", vec![], 42, 7);
220 assert_eq!(stable.instruction_digest(), 42);
221 assert_eq!(stable.prefix_hash(), 7);
222 }
223
224 #[test]
225 fn new_segment_preserves_frozen_request_identity() {
226 let previous = SessionRequestEnvelope::new("segment-1", "fixed prompt", vec![tool("alpha")], 42);
227 let next = previous.begin_segment("segment-2");
228
229 assert_eq!(previous.segment_id(), "segment-1");
230 assert_eq!(next.segment_id(), "segment-2");
231 assert_eq!(previous.system_prompt().as_ref(), next.system_prompt().as_ref());
232 assert_eq!(previous.instruction_digest(), next.instruction_digest());
233 assert_eq!(previous.prefix_hash(), next.prefix_hash());
234 assert_eq!(previous.catalog_hash(), next.catalog_hash());
235 }
236
237 #[test]
238 fn begin_segment_shares_prompt_and_catalog_without_deep_clone() {
239 let previous =
243 SessionRequestEnvelope::new("segment-1", "fixed prompt", vec![tool("alpha"), tool("exec_command")], 42);
244 let next = previous.begin_segment("segment-2");
245
246 assert!(Arc::ptr_eq(&previous.system_prompt(), &next.system_prompt()));
247 assert!(Arc::ptr_eq(&previous.ordered_tools(), &next.ordered_tools()));
248 }
249}