use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub use codewhale_core::fragments::{
DEFAULT_FRAGMENT_MAX_BYTES, MAX_FRAGMENT_BYTES, MAX_FRAGMENT_TOKENS, MAX_FRAGMENTS_PER_CONTEXT,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FragmentId {
Workspace,
Permissions,
Route,
AgentTopology,
SkillsTools,
TokenBudget,
ProjectInstructions,
Constitution,
}
impl FragmentId {
#[must_use]
#[allow(dead_code)] pub fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Permissions => "permissions",
Self::Route => "route",
Self::AgentTopology => "agent_topology",
Self::SkillsTools => "skills_tools",
Self::TokenBudget => "token_budget",
Self::ProjectInstructions => "project_instructions",
Self::Constitution => "constitution",
}
}
#[must_use]
pub fn marker(self) -> &'static str {
match self {
Self::Workspace => "<!-- cw:ctx:workspace -->",
Self::Permissions => "<!-- cw:ctx:permissions -->",
Self::Route => "<!-- cw:ctx:route -->",
Self::AgentTopology => "<!-- cw:ctx:agent_topology -->",
Self::SkillsTools => "<!-- cw:ctx:skills_tools -->",
Self::TokenBudget => "<!-- cw:ctx:token_budget -->",
Self::ProjectInstructions => "<!-- cw:ctx:project_instructions -->",
Self::Constitution => "<!-- cw:ctx:constitution -->",
}
}
#[must_use]
#[allow(dead_code)] pub fn role(self) -> FragmentRole {
match self {
Self::Workspace => FragmentRole::Workspace,
Self::Permissions => FragmentRole::Permissions,
Self::Route => FragmentRole::Route,
Self::AgentTopology => FragmentRole::AgentTopology,
Self::SkillsTools => FragmentRole::SkillsTools,
Self::TokenBudget => FragmentRole::TokenBudget,
Self::ProjectInstructions => FragmentRole::ProjectInstructions,
Self::Constitution => FragmentRole::Constitution,
}
}
#[must_use]
#[allow(dead_code)] pub fn all() -> &'static [FragmentId] {
&[
Self::Workspace,
Self::Permissions,
Self::Route,
Self::AgentTopology,
Self::SkillsTools,
Self::TokenBudget,
Self::ProjectInstructions,
Self::Constitution,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FragmentRole {
Workspace,
Permissions,
Route,
AgentTopology,
SkillsTools,
TokenBudget,
ProjectInstructions,
Constitution,
}
impl FragmentRole {
#[must_use]
#[allow(dead_code)] pub fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Permissions => "permissions",
Self::Route => "route",
Self::AgentTopology => "agent_topology",
Self::SkillsTools => "skills_tools",
Self::TokenBudget => "token_budget",
Self::ProjectInstructions => "project_instructions",
Self::Constitution => "constitution",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FragmentRender {
Unchanged { marker: String, content_hash: u64 },
Updated { fragment: ModelContextFragment },
#[allow(dead_code)] Cleared { marker: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelContextFragment {
pub id: FragmentId,
pub role: FragmentRole,
pub marker: &'static str,
pub max_bytes: usize,
pub content: String,
pub content_hash: u64,
}
impl ModelContextFragment {
#[must_use]
pub fn new(id: FragmentId, role: FragmentRole, raw: impl Into<String>) -> Self {
Self::with_max_bytes(id, role, raw, DEFAULT_FRAGMENT_MAX_BYTES)
}
#[must_use]
pub fn with_max_bytes(
id: FragmentId,
role: FragmentRole,
raw: impl Into<String>,
max_bytes: usize,
) -> Self {
let clamped_max = max_bytes.min(MAX_FRAGMENT_BYTES);
let mut content = enforce_byte_cap(raw.into(), clamped_max);
if content.len().div_ceil(4) > MAX_FRAGMENT_TOKENS {
content = enforce_byte_cap(content, MAX_FRAGMENT_BYTES);
}
let content_hash = hash_content(&content);
Self {
id,
role,
marker: id.marker(),
max_bytes: clamped_max,
content,
content_hash,
}
}
#[must_use]
pub fn matches_text(&self, haystack: &str) -> bool {
haystack.contains(self.marker)
}
#[must_use]
pub fn render_diff(&self, previous: Option<&Self>) -> FragmentRender {
match previous {
Some(prev) if prev.content_hash == self.content_hash && prev.marker == self.marker => {
FragmentRender::Unchanged {
marker: self.marker.to_string(),
content_hash: self.content_hash,
}
}
_ => FragmentRender::Updated {
fragment: self.clone(),
},
}
}
#[must_use]
pub fn render_marked(&self) -> String {
let rendered = format!("{}\n{}", self.marker, self.content.trim_end());
debug_assert!(self.matches_text(&rendered));
rendered
}
}
fn hash_content(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
fn enforce_byte_cap(raw: String, max_bytes: usize) -> String {
if max_bytes == 0 {
return String::new();
}
if raw.len() <= max_bytes {
return raw;
}
let omitted = raw.len().saturating_sub(max_bytes);
let marker = format!("\n[…truncated: {omitted} bytes omitted]");
if marker.len() >= max_bytes {
return marker.chars().take(max_bytes).collect();
}
let keep = max_bytes.saturating_sub(marker.len());
let mut end = keep;
while end > 0 && !raw.is_char_boundary(end) {
end -= 1;
}
let mut out = raw[..end].to_string();
out.push_str(&marker);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_diff_detects_change_and_retain() {
let a = ModelContextFragment::new(FragmentId::Route, FragmentRole::Route, "m=a");
let b = ModelContextFragment::new(FragmentId::Route, FragmentRole::Route, "m=a");
let c = ModelContextFragment::new(FragmentId::Route, FragmentRole::Route, "m=b");
assert!(matches!(
b.render_diff(Some(&a)),
FragmentRender::Unchanged { .. }
));
assert!(matches!(
c.render_diff(Some(&a)),
FragmentRender::Updated { .. }
));
assert!(matches!(
a.render_diff(None),
FragmentRender::Updated { .. }
));
}
}