From 873f870e233dd0d2c6f0cce935f83fb8ce373505 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 20 Jul 2026 16:38:19 +0700 Subject: [PATCH] feat(agent): add AI summarization for conversation history compacting --- apps/infrastructure/src/agent/mod.rs | 2 +- apps/infrastructure/src/agent/runner.rs | 76 +++++++++++++++++++++++-- apps/interfaces/daemon/src/handler.rs | 22 ++++--- apps/interfaces/tui/src/action.rs | 2 + 4 files changed, 87 insertions(+), 15 deletions(-) diff --git a/apps/infrastructure/src/agent/mod.rs b/apps/infrastructure/src/agent/mod.rs index fac884c..b2e8e4f 100644 --- a/apps/infrastructure/src/agent/mod.rs +++ b/apps/infrastructure/src/agent/mod.rs @@ -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}; diff --git a/apps/infrastructure/src/agent/runner.rs b/apps/infrastructure/src/agent/runner.rs index 998f4c8..49d17b4 100644 --- a/apps/infrastructure/src/agent/runner.rs +++ b/apps/infrastructure/src/agent/runner.rs @@ -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( + ¶ms.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( + ¶ms.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>>, event: TurnEvent) { fn mark_done(flag: &Arc) { 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, 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(()) + } + } +} + diff --git a/apps/interfaces/daemon/src/handler.rs b/apps/interfaces/daemon/src/handler.rs index 31da271..d65044f 100644 --- a/apps/interfaces/daemon/src/handler.rs +++ b/apps/interfaces/daemon/src/handler.rs @@ -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."), )); } } diff --git a/apps/interfaces/tui/src/action.rs b/apps/interfaces/tui/src/action.rs index 03b98a9..5d7d2a2 100644 --- a/apps/interfaces/tui/src/action.rs +++ b/apps/interfaces/tui/src/action.rs @@ -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); } }