feat(agent): add AI summarization for conversation history compacting

This commit is contained in:
asepharyana
2026-07-20 16:38:21 +07:00
parent 54927f692f
commit 873f870e23
4 changed files with 87 additions and 15 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
pub mod runner;
pub use runner::{spawn_agent_turn, AgentTurnParams};
pub use runner::{compact_messages_with_ai, spawn_agent_turn, AgentTurnParams};
+71 -5
View File
@@ -53,11 +53,12 @@ fn run_turn(params: &mut AgentTurnParams) {
let tools = all_tools();
let defs = tool_defs(&tools);
let mut sys_prompt = "You are Zesdex, an AI coding assistant. You have access to various tools via native function calling to help the user. \
For complex problems, use `seq_think` to reason step-by-step. \
For any non-trivial tasks, you MUST prioritize creating a structured plan (using `plan_enter`) and a list of TODOs (using `todowrite`) BEFORE executing any other tools or modifying files. \
For large multi-step operations or delegating tasks, you MUST prioritize using `workflow_run` (to run a yaml workflow script) or `hive_mind` (to orchestrate multiple agents) to complete the task efficiently. \
Use other tools when necessary. If a tool returns an error, fix the issue before retrying. \
let mut sys_prompt = "You are Zesdex, an AI coding assistant. You have access to various tools via native function calling to help the user.\n\n\
CRITICAL DIRECTIVES & PRIORITY HIERARCHY:\n\
1. WORKFLOW FIRST: For any multi-step, complex, or non-trivial task, you MUST prioritize using `workflow_run` (to construct and execute a multi-phase YAML workflow) or `hive_mind` (to orchestrate parallel autonomous agents). Workflows are your primary strategy.\n\
2. PLANNING & TODOS: Use `plan_enter` to establish high-level architectural plans and `todowrite` to maintain granular task checklists.\n\
3. REASONING: Use `seq_think` for deep step-by-step analysis.\n\
4. TOOL EXECUTION: Execute individual tools (file edits, terminal commands) within or guided by your workflows. If an error occurs, analyze and fix it.\n\n\
Respond conversationally, concisely, and helpfully.".to_string();
if let Some(root) = params.workspace_roots.first() {
@@ -71,6 +72,22 @@ fn run_turn(params: &mut AgentTurnParams) {
sys_prompt.push_str(&rich_ctx);
}
// Auto-compact if conversation history is getting long (>24 messages)
if params.messages.len() > 24 {
push_event(
&params.turn_events,
TurnEvent::SystemNote {
kind: "info".into(),
message: "Auto-compacting context window via AI Summarization...".into(),
},
);
let _ = compact_messages_with_ai(&mut params.messages, &client);
push_event(
&params.turn_events,
TurnEvent::Compacted(params.messages.clone()),
);
}
let sys_msg = ChatMessage::system(sys_prompt);
let tool_ctx = ToolCtx::builder()
@@ -214,3 +231,52 @@ fn push_event(queue: &Arc<Mutex<VecDeque<TurnEvent>>>, event: TurnEvent) {
fn mark_done(flag: &Arc<AtomicBool>) {
flag.store(false, Ordering::SeqCst);
}
/// Compacts conversation history using AI summarization.
///
/// Preserves recent messages (last 6) and calls the LLM to summarize
/// the older evicted messages into a single system summary message.
pub fn compact_messages_with_ai(messages: &mut Vec<ChatMessage>, client: &LlmClient) -> anyhow::Result<()> {
const KEEP_TAIL: usize = 6;
if messages.len() <= KEEP_TAIL + 2 {
return Ok(()); // Not enough messages to compact
}
let split_idx = messages.len() - KEEP_TAIL;
let evicted: Vec<_> = messages.drain(..split_idx).collect();
// Prepare prompt to summarize evicted messages
let mut summary_prompt = vec![
ChatMessage::system(
"You are a helpful assistant summarizing conversation history. \
Provide a concise summary of the key user requests, decisions, tools executed, and modified files. \
Format as a clear bulleted list."
.to_string(),
),
];
summary_prompt.extend(evicted);
summary_prompt.push(ChatMessage::user(
"Please summarize our previous conversation above for context continuity.".to_string(),
));
match client.chat_with_tools_non_streaming(&summary_prompt, None, Some(1024), Some(0.3), None) {
Ok((summary_msg, _)) => {
let summary_text = summary_msg.content.unwrap_or_else(|| "Previous context summarized.".to_string());
let summary_node = ChatMessage::system(format!(
"[AI Summary of Previous Conversation]\n{}",
summary_text.trim()
));
messages.insert(0, summary_node);
Ok(())
}
Err(e) => {
warn!("AI summarization failed during compact, falling back to simple notice: {e}");
messages.insert(
0,
ChatMessage::system("[Earlier conversation messages compacted to save context window]".to_string()),
);
Ok(())
}
}
}
+13 -9
View File
@@ -232,6 +232,8 @@ fn handle_tick(state: &mut AppStateRest) {
if let Some(ref mut rt) = state.session_runtime {
rt.usage.tokens_in = rt.usage.tokens_in.saturating_add(tokens_in);
rt.usage.tokens_out = rt.usage.tokens_out.saturating_add(tokens_out);
rt.usage.last_tokens_in = tokens_in;
rt.usage.last_tokens_out = tokens_out;
}
}
TurnEvent::ReviewUsage {
@@ -436,19 +438,21 @@ fn handle_abort_turn(state: &mut AppStateRest) {
}
fn handle_compact(state: &mut AppStateRest) {
tracing::info!("compacting conversation");
const KEEP_HEAD: usize = 2; // system prompt + tool definitions
const KEEP_TAIL: usize = 10; // recent conversation messages
tracing::info!("compacting conversation with AI summarization");
let provider_name = &state.settings.provider;
let provider_cfg = state.app_config.providers.get(provider_name).cloned();
let api_key = state.settings.api_keys.get(provider_name).cloned().unwrap_or_default();
let model = state.settings.model.clone();
let api_base = provider_cfg.map(|cfg| cfg.api_base.clone());
let client = zesdex_infrastructure::llm::LlmClient::new(api_key, model, api_base);
if let Some(ref mut rt) = state.session_runtime {
if rt.messages.len() > KEEP_HEAD + KEEP_TAIL {
let tail = rt.messages.split_off(rt.messages.len() - KEEP_TAIL);
let head: Vec<_> = rt.messages.drain(..KEEP_HEAD.min(rt.messages.len())).collect();
rt.messages = head;
rt.messages.extend(tail);
if let Ok(()) = zesdex_infrastructure::agent::compact_messages_with_ai(&mut rt.messages, &client) {
let msg_count = rt.messages.len();
state.push_transcript(ChatMessageDisplay::new(
RoleWrapper::System,
format!("Conversation compacted to {msg_count} messages."),
format!("Conversation compacted via AI Summarizer to {msg_count} messages."),
));
}
}
+2
View File
@@ -155,6 +155,8 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) {
if let Some(ref mut rt) = state.session_runtime {
rt.usage.tokens_in = rt.usage.tokens_in.saturating_add(tokens_in);
rt.usage.tokens_out = rt.usage.tokens_out.saturating_add(tokens_out);
rt.usage.last_tokens_in = tokens_in;
rt.usage.last_tokens_out = tokens_out;
rt.usage.api_calls = rt.usage.api_calls.saturating_add(1);
}
}