use std::cmp::Ordering;
use std::sync::Arc;
use crate::llm::provider::ToolDefinition;
use super::hash_utils::{hash_tool_definitions, hash_value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentBoundaryReason {
Compaction,
ToolCatalogEpoch,
PrimaryAgent,
Model,
Provider,
Mode,
Instructions,
}
#[derive(Debug, Clone)]
pub struct SessionRequestEnvelope {
segment_id: Arc<str>,
system_prompt: Arc<str>,
ordered_tools: Arc<Vec<ToolDefinition>>,
instruction_digest: u64,
prefix_hash: u64,
catalog_hash: Option<u64>,
}
impl SessionRequestEnvelope {
#[must_use]
pub fn new(
segment_id: impl Into<Arc<str>>,
system_prompt: impl Into<Arc<str>>,
mut tools: Vec<ToolDefinition>,
instruction_digest: u64,
) -> Self {
tools.sort_by(compare_tools);
let system_prompt = system_prompt.into();
let prefix_hash = hash_value(&system_prompt);
let catalog_hash = if tools.is_empty() {
None
} else {
hash_tool_definitions(Some(&tools))
};
Self {
segment_id: segment_id.into(),
system_prompt,
ordered_tools: Arc::new(tools),
instruction_digest,
prefix_hash,
catalog_hash,
}
}
#[must_use]
pub fn segment_id(&self) -> &str {
&self.segment_id
}
#[must_use]
pub fn system_prompt(&self) -> Arc<str> {
Arc::clone(&self.system_prompt)
}
#[must_use]
pub fn ordered_tools(&self) -> Arc<Vec<ToolDefinition>> {
Arc::clone(&self.ordered_tools)
}
#[must_use]
pub fn instruction_digest(&self) -> u64 {
self.instruction_digest
}
#[must_use]
pub fn prefix_hash(&self) -> u64 {
self.prefix_hash
}
#[must_use]
pub fn catalog_hash(&self) -> Option<u64> {
self.catalog_hash
}
}
fn core_priority(name: &str) -> u8 {
match name {
"exec_command" => 0,
"write_stdin" => 1,
"search_tools" | "mcp_search_tools" => 2,
"apply_patch" => 3,
_ => 4,
}
}
fn compare_tools(left: &ToolDefinition, right: &ToolDefinition) -> Ordering {
let left_name = left.function_name();
let right_name = right.function_name();
core_priority(left_name)
.cmp(&core_priority(right_name))
.then_with(|| left_name.cmp(right_name))
.then_with(|| left.tool_type.cmp(&right.tool_type))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn tool(name: &str) -> ToolDefinition {
ToolDefinition::function(name.to_string(), name.to_string(), json!({"type": "object"}))
}
#[test]
fn equivalent_cold_starts_are_byte_identical() {
let left = SessionRequestEnvelope::new(
"segment-1",
"fixed prompt",
vec![tool("zeta"), tool("exec_command"), tool("alpha")],
42,
);
let right = SessionRequestEnvelope::new(
"segment-1",
"fixed prompt",
vec![tool("alpha"), tool("zeta"), tool("exec_command")],
42,
);
assert_eq!(left.system_prompt.as_bytes(), right.system_prompt.as_bytes());
assert_eq!(
serde_json::to_vec(left.ordered_tools.as_ref()).expect("serialize left catalog"),
serde_json::to_vec(right.ordered_tools.as_ref()).expect("serialize right catalog")
);
assert_eq!(left.catalog_hash, right.catalog_hash);
}
#[test]
fn core_priority_precedes_name_order() {
let envelope = SessionRequestEnvelope::new(
"segment-1",
"fixed prompt",
vec![
tool("alpha"),
tool("apply_patch"),
tool("search_tools"),
tool("exec_command"),
],
42,
);
let names = envelope
.ordered_tools
.iter()
.map(ToolDefinition::function_name)
.collect::<Vec<_>>();
assert_eq!(names, ["exec_command", "search_tools", "apply_patch", "alpha"]);
}
}