#[cfg(feature = "context-compression")]
use uuid::Uuid;
#[cfg(feature = "context-compression")]
use zeph_llm::provider::{Message, MessageMetadata, Role, ToolDefinition};
use crate::config::FocusConfig;
#[cfg(feature = "context-compression")]
pub(crate) fn focus_tool_definitions() -> Vec<ToolDefinition> {
vec![
ToolDefinition {
name: "start_focus".into(),
description: "Start a focused exploration phase. Use before diving into a subtask \
(e.g., reading many files, running experiments). The conversation since \
this checkpoint will be summarized and compressed when you call \
complete_focus.\n\nParameters: scope (string, required) — a concise \
description of what you are about to explore.\nReturns: confirmation \
with a checkpoint marker ID.\nExample: {\"scope\": \"reading auth \
middleware files\"}"
.into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"scope": {
"type": "string",
"description": "Concise label for what you are about to explore."
}
},
"required": ["scope"]
}),
},
ToolDefinition {
name: "complete_focus".into(),
description: "Complete the active focus phase. Summarizes the conversation since the \
last start_focus checkpoint, appends the summary to the pinned Knowledge \
block, and removes the bracketed messages from context.\n\nParameters: \
summary (string, required) — what you learned or accomplished.\nReturns: \
confirmation or error if no focus session is active.\nExample: \
{\"summary\": \"Found that auth.rs uses JWT with RS256. Key file: \
src/auth.rs:42.\"}"
.into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "What you learned or accomplished during this focus phase."
}
},
"required": ["summary"]
}),
},
]
}
#[cfg_attr(not(feature = "context-compression"), allow(dead_code))]
pub(crate) const KNOWLEDGE_BLOCK_PREFIX: &str = "[knowledge]\n";
#[allow(dead_code)]
pub(crate) const FOCUS_REMINDER_PREFIX: &str = "[focus reminder] ";
#[cfg_attr(not(feature = "context-compression"), allow(dead_code))]
pub(crate) struct FocusState {
pub(crate) config: FocusConfig,
pub(crate) knowledge_blocks: Vec<String>,
#[cfg(feature = "context-compression")]
pub(crate) active_marker: Option<Uuid>,
pub(crate) active_scope: Option<String>,
pub(crate) turns_since_focus: usize,
pub(crate) turns_since_reminder: usize,
}
#[cfg_attr(not(feature = "context-compression"), allow(dead_code))]
impl FocusState {
pub(crate) fn new(config: FocusConfig) -> Self {
Self {
config,
knowledge_blocks: Vec::new(),
#[cfg(feature = "context-compression")]
active_marker: None,
active_scope: None,
turns_since_focus: 0,
turns_since_reminder: 0,
}
}
#[cfg_attr(not(feature = "context-compression"), allow(clippy::unused_self))]
pub(crate) fn is_active(&self) -> bool {
#[cfg(feature = "context-compression")]
{
self.active_marker.is_some()
}
#[cfg(not(feature = "context-compression"))]
{
false
}
}
#[allow(dead_code)]
pub(crate) fn should_remind(&self) -> bool {
if !self.config.enabled {
return false;
}
self.turns_since_focus >= self.config.compression_interval
&& self.turns_since_reminder >= self.config.reminder_interval
}
pub(crate) fn tick(&mut self) {
self.turns_since_focus = self.turns_since_focus.saturating_add(1);
self.turns_since_reminder = self.turns_since_reminder.saturating_add(1);
}
pub(crate) fn append_knowledge(&mut self, summary: String) -> bool {
if summary.is_empty() {
return false;
}
self.knowledge_blocks.push(summary);
while self.knowledge_blocks.len() > 1 {
let total_chars: usize = self.knowledge_blocks.iter().map(String::len).sum();
#[allow(clippy::integer_division)]
let approx_tokens = total_chars / 4;
if approx_tokens <= self.config.max_knowledge_tokens {
break;
}
self.knowledge_blocks.remove(0);
}
true
}
#[cfg(feature = "context-compression")]
pub(crate) fn build_knowledge_message(&self) -> Option<Message> {
if self.knowledge_blocks.is_empty() {
return None;
}
let mut body = String::from(KNOWLEDGE_BLOCK_PREFIX);
for (i, block) in self.knowledge_blocks.iter().enumerate() {
if i > 0 {
body.push('\n');
}
body.push_str("## Focus summary ");
body.push_str(&(i + 1).to_string());
body.push('\n');
body.push_str(block);
}
Some(Message {
role: Role::System,
content: body,
parts: vec![],
metadata: MessageMetadata::focus_pinned(),
})
}
#[cfg(feature = "context-compression")]
pub(crate) fn start(&mut self, scope: String) -> Uuid {
let marker = Uuid::new_v4();
self.active_marker = Some(marker);
self.active_scope = Some(scope);
marker
}
#[cfg(feature = "context-compression")]
pub(crate) fn complete(&mut self) {
self.active_marker = None;
self.active_scope = None;
self.turns_since_focus = 0;
self.turns_since_reminder = 0;
}
}
impl Default for FocusState {
fn default() -> Self {
Self::new(FocusConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_state_is_inactive() {
let state = FocusState::new(FocusConfig::default());
assert!(!state.is_active());
assert!(state.knowledge_blocks.is_empty());
}
#[test]
fn should_remind_returns_false_when_disabled() {
let mut config = FocusConfig::default();
config.enabled = false;
let mut state = FocusState::new(config);
state.turns_since_focus = 100;
state.turns_since_reminder = 100;
assert!(!state.should_remind());
}
#[test]
fn should_remind_returns_true_when_thresholds_exceeded() {
let mut config = FocusConfig::default();
config.enabled = true;
config.compression_interval = 5;
config.reminder_interval = 3;
let mut state = FocusState::new(config);
state.turns_since_focus = 6;
state.turns_since_reminder = 4;
assert!(state.should_remind());
}
#[test]
fn append_knowledge_adds_entry() {
let mut state = FocusState::new(FocusConfig::default());
assert!(state.append_knowledge("test summary".to_string()));
assert_eq!(state.knowledge_blocks.len(), 1);
}
#[test]
fn append_knowledge_ignores_empty() {
let mut state = FocusState::new(FocusConfig::default());
assert!(!state.append_knowledge(String::new()));
assert!(state.knowledge_blocks.is_empty());
}
#[cfg(feature = "context-compression")]
#[test]
fn start_sets_marker_and_scope() {
let mut state = FocusState::new(FocusConfig::default());
let marker = state.start("test scope".to_string());
assert!(state.is_active());
assert_eq!(state.active_marker, Some(marker));
assert_eq!(state.active_scope.as_deref(), Some("test scope"));
}
#[cfg(feature = "context-compression")]
#[test]
fn complete_clears_state() {
let mut state = FocusState::new(FocusConfig::default());
state.start("test".to_string());
state.complete();
assert!(!state.is_active());
assert_eq!(state.turns_since_focus, 0);
}
#[cfg(feature = "context-compression")]
#[test]
fn build_knowledge_message_none_when_empty() {
let state = FocusState::new(FocusConfig::default());
assert!(state.build_knowledge_message().is_none());
}
#[cfg(feature = "context-compression")]
#[test]
fn build_knowledge_message_contains_prefix() {
let mut state = FocusState::new(FocusConfig::default());
state.append_knowledge("my summary".to_string());
let msg = state.build_knowledge_message().unwrap();
assert!(msg.content.starts_with(KNOWLEDGE_BLOCK_PREFIX));
assert!(msg.content.contains("my summary"));
assert!(msg.metadata.focus_pinned);
}
#[test]
fn append_knowledge_evicts_oldest_when_over_token_cap() {
let mut config = FocusConfig::default();
config.max_knowledge_tokens = 10; let mut state = FocusState::new(config);
state.append_knowledge("a".repeat(100)); state.append_knowledge("b".repeat(100));
state.append_knowledge("c".repeat(100));
assert!(
!state.knowledge_blocks.is_empty(),
"at least one block must remain"
);
if state.knowledge_blocks.len() > 1 {
assert!(
!state.knowledge_blocks[0].starts_with('a'),
"oldest block must be evicted first"
);
}
}
#[test]
fn append_knowledge_preserves_single_entry_regardless_of_size() {
let mut config = FocusConfig::default();
config.max_knowledge_tokens = 1; let mut state = FocusState::new(config);
state.append_knowledge("very long summary that exceeds any token cap".to_string());
assert_eq!(
state.knowledge_blocks.len(),
1,
"must never evict the last entry"
);
}
#[cfg(feature = "context-compression")]
#[test]
fn is_active_returns_false_before_start() {
let state = FocusState::new(FocusConfig::default());
assert!(!state.is_active());
}
#[cfg(feature = "context-compression")]
#[test]
fn is_active_returns_true_during_session() {
let mut state = FocusState::new(FocusConfig::default());
state.start("scope".to_string());
assert!(state.is_active());
}
#[cfg(feature = "context-compression")]
#[test]
fn is_active_returns_false_after_complete() {
let mut state = FocusState::new(FocusConfig::default());
state.start("scope".to_string());
state.complete();
assert!(!state.is_active());
}
#[test]
fn tick_increments_counters() {
let mut state = FocusState::new(FocusConfig::default());
state.tick();
state.tick();
assert_eq!(state.turns_since_focus, 2);
assert_eq!(state.turns_since_reminder, 2);
}
}