use serde::{Deserialize, Serialize};
use crate::plugins::{Plugin, PluginDescriptor, PluginRegistrar};
use crate::state::{MutationBatch, StateKey, StateKeyOptions};
pub const CONTEXT_COMPACTION_PLUGIN_ID: &str = "context_compaction";
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CompactionConfig {
#[serde(default, rename = "mode")]
pub execution_mode: CompactionExecutionMode,
pub summarizer_system_prompt: String,
pub summarizer_user_prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_model: Option<String>,
pub min_savings_ratio: f64,
#[serde(default)]
pub raw_retention: CompactionRawRetention,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum CompactionExecutionMode {
Off,
#[default]
Background,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum CompactionRawRetention {
#[default]
PreserveDurable,
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
execution_mode: CompactionExecutionMode::Background,
summarizer_system_prompt: "You are a conversation summarizer. Preserve all key facts, decisions, tool results, and action items. Be concise but complete.".into(),
summarizer_user_prompt: "Update the cumulative conversation summary.\n\n<existing-summary>\n{previous_summary}\n</existing-summary>\n\n<new-conversation>\n{messages}\n</new-conversation>".into(),
summary_max_tokens: None,
summary_model: None,
min_savings_ratio: 0.3,
raw_retention: CompactionRawRetention::PreserveDurable,
}
}
}
pub struct CompactionConfigKey;
impl awaken_runtime_contract::registry_spec::PluginConfigKey for CompactionConfigKey {
const KEY: &'static str = "compaction";
type Config = CompactionConfig;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompactionBoundary {
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boundary_message_id: Option<String>,
pub pre_tokens: usize,
pub post_tokens: usize,
pub timestamp_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompactionFailure {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
pub boundary_message_id: String,
pub error: String,
pub timestamp_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompactionSkipped {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
pub boundary_message_id: String,
pub reason: String,
pub pre_tokens: usize,
pub post_tokens: usize,
pub savings_ratio_ppm: u32,
pub min_savings_ratio_ppm: u32,
pub timestamp_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompactionInFlight {
pub task_id: String,
pub boundary_message_id: String,
pub started_at_ms: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CompactionState {
pub boundaries: Vec<CompactionBoundary>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub failures: Vec<CompactionFailure>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skipped: Vec<CompactionSkipped>,
pub total_compactions: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub in_flight: Option<CompactionInFlight>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompactionAction {
RecordBoundary(CompactionBoundary),
RecordFailure(CompactionFailure),
RecordSkipped(CompactionSkipped),
SetInFlight(CompactionInFlight),
ClearInFlight,
Clear,
}
impl CompactionState {
fn reduce(&mut self, action: CompactionAction) {
match action {
CompactionAction::RecordBoundary(boundary) => {
self.boundaries.push(boundary);
self.total_compactions += 1;
}
CompactionAction::RecordFailure(failure) => {
self.failures.push(failure);
}
CompactionAction::RecordSkipped(skipped) => {
self.skipped.push(skipped);
}
CompactionAction::SetInFlight(in_flight) => {
self.in_flight = Some(in_flight);
}
CompactionAction::ClearInFlight => {
self.in_flight = None;
}
CompactionAction::Clear => {
self.boundaries.clear();
self.failures.clear();
self.skipped.clear();
self.total_compactions = 0;
self.in_flight = None;
}
}
}
pub fn latest_boundary(&self) -> Option<&CompactionBoundary> {
self.boundaries.last()
}
pub fn is_compacting(&self) -> bool {
self.in_flight.is_some()
}
}
pub struct CompactionStateKey;
impl StateKey for CompactionStateKey {
const KEY: &'static str = "__context_compaction";
type Value = CompactionState;
type Update = CompactionAction;
fn apply(value: &mut Self::Value, update: Self::Update) {
value.reduce(update);
}
}
#[derive(Debug, Clone, Default)]
pub struct CompactionPlugin {
pub config: CompactionConfig,
}
impl CompactionPlugin {
pub fn new(config: CompactionConfig) -> Self {
Self { config }
}
}
impl Plugin for CompactionPlugin {
fn descriptor(&self) -> PluginDescriptor {
PluginDescriptor {
name: CONTEXT_COMPACTION_PLUGIN_ID,
}
}
fn register(
&self,
registrar: &mut PluginRegistrar,
) -> Result<(), awaken_runtime_contract::StateError> {
registrar.register_key::<CompactionStateKey>(StateKeyOptions::default())?;
Ok(())
}
fn on_activate(
&self,
_agent_spec: &awaken_runtime_contract::registry_spec::AgentSpec,
_patch: &mut MutationBatch,
) -> Result<(), awaken_runtime_contract::StateError> {
Ok(())
}
}
#[cfg(test)]
#[path = "compaction_tests.rs"]
mod tests;