use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{ContextSource, MemoryStatus, SCHEMA_VERSION, hash_json};
pub const COMPACTION_ALGORITHM_VERSION: &str = "proofborne.compaction.v1";
pub const COMPACTION_TOKEN_ESTIMATOR: &str = "utf8_ascii_div_3_plus_non_ascii_scalars.v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompactionBudget {
pub context_tokens: u64,
pub reserved_output_tokens: u64,
pub max_input_tokens: u64,
pub max_input_bytes: u64,
}
impl CompactionBudget {
pub fn new(
context_tokens: u64,
reserved_output_tokens: u64,
max_input_bytes: u64,
) -> Result<Self, CompactionError> {
let max_input_tokens = context_tokens
.checked_sub(reserved_output_tokens)
.filter(|available| *available > 0)
.ok_or(CompactionError::InvalidBudget)?;
let budget = Self {
context_tokens,
reserved_output_tokens,
max_input_tokens,
max_input_bytes,
};
budget.validate()?;
Ok(budget)
}
pub fn validate(&self) -> Result<(), CompactionError> {
if self.context_tokens == 0
|| self.reserved_output_tokens == 0
|| self.max_input_tokens == 0
|| self.max_input_bytes == 0
|| self
.reserved_output_tokens
.checked_add(self.max_input_tokens)
!= Some(self.context_tokens)
{
return Err(CompactionError::InvalidBudget);
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompactionUsage {
pub token_estimator: String,
pub instruction_tokens: u64,
pub tool_tokens: u64,
pub contract_tokens: u64,
pub evidence_tokens: u64,
pub memory_tokens: u64,
pub history_tokens: u64,
pub input_tokens: u64,
pub input_bytes: u64,
}
impl Default for CompactionUsage {
fn default() -> Self {
Self {
token_estimator: COMPACTION_TOKEN_ESTIMATOR.to_owned(),
instruction_tokens: 0,
tool_tokens: 0,
contract_tokens: 0,
evidence_tokens: 0,
memory_tokens: 0,
history_tokens: 0,
input_tokens: 0,
input_bytes: 0,
}
}
}
impl CompactionUsage {
pub fn category_total(&self) -> Option<u64> {
self.instruction_tokens
.checked_add(self.tool_tokens)?
.checked_add(self.contract_tokens)?
.checked_add(self.evidence_tokens)?
.checked_add(self.memory_tokens)?
.checked_add(self.history_tokens)
}
pub fn validate(&self, budget: &CompactionBudget) -> Result<(), CompactionError> {
if self.token_estimator != COMPACTION_TOKEN_ESTIMATOR {
return Err(CompactionError::UnsupportedTokenEstimator(
self.token_estimator.clone(),
));
}
budget.validate()?;
if self.category_total() != Some(self.input_tokens) {
return Err(CompactionError::UsageAccounting);
}
if self.input_tokens > budget.max_input_tokens || self.input_bytes > budget.max_input_bytes
{
return Err(CompactionError::BudgetExceeded);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum CompactionContextKind {
Contract,
Criterion,
Evidence,
Memory,
History,
Workspace,
SideEffect,
Recovery,
Authority,
SecretTaint,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum CompactionPriority {
Pinned,
Proof,
Retrieved,
Recent,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CompactionDecision {
Retained,
Omitted,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum CompactionSelectionReason {
TaskContract,
ClaimScope,
RequiredCriterion,
OpenObligation,
UnresolvedCounterevidence,
ActiveEvidence,
Waiver,
Supersession,
WorkspaceBinding,
SideEffectBinding,
RecoveryBinding,
AuthorityBinding,
SecretTaint,
TaskScopeMatch,
PathScopeMatch,
SymbolScopeMatch,
ProducerMatch,
RetrievalMatch,
RecentHistory,
BudgetExhausted,
HistoricalMemoryExcluded,
Superseded,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContextSelection {
pub id: String,
pub kind: CompactionContextKind,
pub content_hash: String,
pub decision: CompactionDecision,
pub reason: CompactionSelectionReason,
pub priority: CompactionPriority,
pub required: bool,
pub rank: u64,
pub estimated_tokens: u64,
pub byte_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<ContextSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_status: Option<MemoryStatus>,
}
impl ContextSelection {
fn validate(&self) -> Result<(), CompactionError> {
if self.id.trim().is_empty() {
return Err(CompactionError::EmptySelectionId);
}
if !valid_digest(&self.content_hash) {
return Err(CompactionError::InvalidDigest(self.content_hash.clone()));
}
if self.estimated_tokens == 0 || self.byte_count == 0 {
return Err(CompactionError::EmptySelection(self.id.clone()));
}
if self.required && self.decision != CompactionDecision::Retained {
return Err(CompactionError::RequiredSelectionOmitted(self.id.clone()));
}
if self.kind == CompactionContextKind::Memory && self.memory_status.is_none() {
return Err(CompactionError::MemoryLifecycleMissing(self.id.clone()));
}
if self.kind != CompactionContextKind::Memory && self.memory_status.is_some() {
return Err(CompactionError::UnexpectedMemoryLifecycle(self.id.clone()));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompactionPlan {
pub schema_version: String,
pub algorithm_version: String,
pub input_hash: String,
pub contract_hash: String,
pub workspace_generation: u64,
pub state_binding: String,
pub side_effect_watermark: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub authority_hash: Option<String>,
pub budget: CompactionBudget,
pub selections: Vec<ContextSelection>,
}
impl CompactionPlan {
pub fn digest(&self) -> Result<String, CompactionError> {
let value = serde_json::to_value(self).map_err(|_| CompactionError::Serialization)?;
Ok(hash_json(&value))
}
pub fn validate(&self) -> Result<(), CompactionError> {
if self.schema_version != SCHEMA_VERSION {
return Err(CompactionError::UnsupportedSchema(
self.schema_version.clone(),
));
}
if self.algorithm_version != COMPACTION_ALGORITHM_VERSION {
return Err(CompactionError::UnsupportedAlgorithm(
self.algorithm_version.clone(),
));
}
for digest in [&self.input_hash, &self.contract_hash, &self.state_binding] {
if !valid_digest(digest) {
return Err(CompactionError::InvalidDigest((*digest).clone()));
}
}
if self
.authority_hash
.as_deref()
.is_some_and(|digest| !valid_digest(digest))
{
return Err(CompactionError::InvalidDigest(
self.authority_hash.clone().unwrap_or_default(),
));
}
self.budget.validate()?;
if self.selections.is_empty() {
return Err(CompactionError::EmptyPlan);
}
let mut identifiers = BTreeSet::new();
let mut previous = None;
for selection in &self.selections {
selection.validate()?;
if !identifiers.insert(selection.id.as_str()) {
return Err(CompactionError::DuplicateSelection(selection.id.clone()));
}
let key = (selection.priority, selection.rank, selection.id.as_str());
if previous.is_some_and(|previous| previous > key) {
return Err(CompactionError::SelectionOrder);
}
previous = Some(key);
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompactionReceipt {
pub schema_version: String,
pub algorithm_version: String,
pub plan_hash: String,
pub input_hash: String,
pub output_hash: String,
pub output_history_hash: String,
pub output_history_items: u64,
pub workspace_generation: u64,
pub state_binding: String,
pub plan: CompactionPlan,
pub usage: CompactionUsage,
pub retained_ids: Vec<String>,
pub omitted_ids: Vec<String>,
}
impl CompactionReceipt {
pub fn validate(&self) -> Result<(), CompactionError> {
if self.schema_version != SCHEMA_VERSION {
return Err(CompactionError::UnsupportedSchema(
self.schema_version.clone(),
));
}
if self.algorithm_version != COMPACTION_ALGORITHM_VERSION {
return Err(CompactionError::UnsupportedAlgorithm(
self.algorithm_version.clone(),
));
}
self.plan.validate()?;
if self.plan_hash != self.plan.digest()? {
return Err(CompactionError::PlanDigest);
}
if self.input_hash != self.plan.input_hash
|| self.workspace_generation != self.plan.workspace_generation
|| self.state_binding != self.plan.state_binding
{
return Err(CompactionError::PlanBinding);
}
for digest in [&self.output_hash, &self.output_history_hash] {
if !valid_digest(digest) {
return Err(CompactionError::InvalidDigest((*digest).clone()));
}
}
if self.output_history_items == 0 {
return Err(CompactionError::PlanBinding);
}
self.usage.validate(&self.plan.budget)?;
let mut retained = self
.plan
.selections
.iter()
.filter(|selection| selection.decision == CompactionDecision::Retained)
.map(|selection| selection.id.clone())
.collect::<Vec<_>>();
let mut omitted = self
.plan
.selections
.iter()
.filter(|selection| selection.decision == CompactionDecision::Omitted)
.map(|selection| selection.id.clone())
.collect::<Vec<_>>();
retained.sort();
omitted.sort();
if retained != self.retained_ids || omitted != self.omitted_ids {
return Err(CompactionError::SelectionSets);
}
Ok(())
}
}
fn valid_digest(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum CompactionError {
#[error("unsupported compaction schema: {0}")]
UnsupportedSchema(String),
#[error("unsupported compaction algorithm: {0}")]
UnsupportedAlgorithm(String),
#[error("compaction budget is invalid")]
InvalidBudget,
#[error("unsupported compaction token estimator: {0}")]
UnsupportedTokenEstimator(String),
#[error("compaction usage accounting is inconsistent")]
UsageAccounting,
#[error("compacted context exceeds its hard budget")]
BudgetExceeded,
#[error("compaction contract serialization failed")]
Serialization,
#[error("invalid compaction digest: {0}")]
InvalidDigest(String),
#[error("compaction plan must contain at least one selection")]
EmptyPlan,
#[error("compaction selection identifier must not be empty")]
EmptySelectionId,
#[error("compaction selection has empty content: {0}")]
EmptySelection(String),
#[error("duplicate compaction selection: {0}")]
DuplicateSelection(String),
#[error("compaction selections are not deterministically ordered")]
SelectionOrder,
#[error("required compaction selection was omitted: {0}")]
RequiredSelectionOmitted(String),
#[error("memory selection lacks lifecycle state: {0}")]
MemoryLifecycleMissing(String),
#[error("non-memory selection carries a memory lifecycle state: {0}")]
UnexpectedMemoryLifecycle(String),
#[error("compaction receipt plan digest is invalid")]
PlanDigest,
#[error("compaction receipt does not match its plan bindings")]
PlanBinding,
#[error("compaction receipt selection sets are inconsistent")]
SelectionSets,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::hash_bytes;
fn valid_plan() -> CompactionPlan {
CompactionPlan {
schema_version: SCHEMA_VERSION.to_owned(),
algorithm_version: COMPACTION_ALGORITHM_VERSION.to_owned(),
input_hash: hash_bytes(b"input"),
contract_hash: hash_bytes(b"contract"),
workspace_generation: 4,
state_binding: hash_bytes(b"workspace"),
side_effect_watermark: 0,
authority_hash: Some(hash_bytes(b"authority")),
budget: CompactionBudget::new(100, 10, 1_000).unwrap(),
selections: vec![ContextSelection {
id: "contract:task".to_owned(),
kind: CompactionContextKind::Contract,
content_hash: hash_bytes(b"selection"),
decision: CompactionDecision::Retained,
reason: CompactionSelectionReason::TaskContract,
priority: CompactionPriority::Pinned,
required: true,
rank: 0,
estimated_tokens: 4,
byte_count: 9,
source: None,
memory_status: None,
}],
}
}
fn valid_receipt() -> CompactionReceipt {
let plan = valid_plan();
CompactionReceipt {
schema_version: SCHEMA_VERSION.to_owned(),
algorithm_version: COMPACTION_ALGORITHM_VERSION.to_owned(),
plan_hash: plan.digest().unwrap(),
input_hash: plan.input_hash.clone(),
output_hash: hash_bytes(b"provider-input"),
output_history_hash: hash_bytes(b"history"),
output_history_items: 1,
workspace_generation: plan.workspace_generation,
state_binding: plan.state_binding.clone(),
plan,
usage: CompactionUsage {
token_estimator: COMPACTION_TOKEN_ESTIMATOR.to_owned(),
instruction_tokens: 1,
tool_tokens: 1,
contract_tokens: 1,
evidence_tokens: 1,
memory_tokens: 1,
history_tokens: 1,
input_tokens: 6,
input_bytes: 18,
},
retained_ids: vec!["contract:task".to_owned()],
omitted_ids: Vec::new(),
}
}
#[test]
fn required_selection_cannot_be_omitted() {
let mut plan = valid_plan();
plan.selections[0].decision = CompactionDecision::Omitted;
assert_eq!(
plan.validate(),
Err(CompactionError::RequiredSelectionOmitted(
"contract:task".to_owned()
))
);
}
#[test]
fn receipt_rejects_plan_tampering_and_commits_history_digest() {
let mut plan_tampered = valid_receipt();
plan_tampered.plan.side_effect_watermark = 1;
assert_eq!(plan_tampered.validate(), Err(CompactionError::PlanDigest));
let mut history_tampered = valid_receipt();
history_tampered.output_history_hash = "0".repeat(64);
assert!(history_tampered.validate().is_ok());
assert_ne!(
history_tampered.output_history_hash,
hash_bytes(b"history"),
"checkpoint recovery must compare this digest to the persisted history"
);
}
#[test]
fn default_usage_names_the_versioned_estimator() {
let usage = CompactionUsage::default();
assert_eq!(usage.token_estimator, COMPACTION_TOKEN_ESTIMATOR);
usage.validate(&valid_plan().budget).unwrap();
}
#[test]
fn public_receipt_shape_is_camel_case_and_versioned() {
let receipt = valid_receipt();
receipt.validate().unwrap();
let value = serde_json::to_value(receipt).unwrap();
assert_eq!(value["schemaVersion"], json!(SCHEMA_VERSION));
assert_eq!(
value["algorithmVersion"],
json!(COMPACTION_ALGORITHM_VERSION)
);
assert_eq!(
value["usage"]["tokenEstimator"],
json!(COMPACTION_TOKEN_ESTIMATOR)
);
assert!(value.get("outputHistoryHash").is_some());
assert!(value.get("retainedIds").is_some());
assert!(value.get("output_history_hash").is_none());
}
}