feat(agent): add AI summarization for conversation history compacting
This commit is contained in:
@@ -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};
|
||||
|
||||
@@ -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<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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user