Fix ACD turn summary loss and add /dump command

ACD (Aggressive Context Dehydration) fixes:
- Fixed dehydrate_context() to extract turn summary from context window
  instead of using the passed-in final_response (which contained only
  the timing footer, not the actual LLM response)
- Removed final_response parameter from dehydrate_context() since it
  now self-extracts the last assistant message as the summary
- This ensures the actual turn summary is preserved after dehydration,
  not just the timing footer

New /dump command:
- Added /dump command to dump entire context window to tmp/ for debugging
- Shows message index, role, kind, content length, and full content
- Available in both console and machine modes

UTF-8 safety:
- Fixed truncate_to_word_boundary() to use character indices instead of
  byte indices, preventing panics on multi-byte UTF-8 characters
- Added UTF-8 string slicing guidance to AGENTS.md

Agent: g3
This commit is contained in:
Dhanji R. Prasanna
2026-01-12 05:13:02 +05:30
parent ac17b95b24
commit f415dbb84b
14 changed files with 1771 additions and 27 deletions

View File

@@ -271,6 +271,75 @@ Format this as a detailed but concise summary that can be used to resume the con
old_chars.saturating_sub(new_chars)
}
/// Reset context window with a summary and optional ACD stub
/// Preserves the original system prompt as the first message
/// If stub is provided, it's added as a system message before the summary
pub fn reset_with_summary_and_stub(
&mut self,
summary: String,
latest_user_message: Option<String>,
stub: Option<String>,
) -> usize {
// Calculate chars saved (old history minus new summary)
let old_chars: usize = self
.conversation_history
.iter()
.map(|m| m.content.len())
.sum();
// Preserve the original system prompt (first message) and optionally the README (second message)
let original_system_prompt = self.conversation_history.first().cloned();
let readme_message = self.conversation_history.get(1).and_then(|msg| {
if matches!(msg.role, MessageRole::System)
&& (msg.content.contains("Project README")
|| msg.content.contains("Agent Configuration"))
{
Some(msg.clone())
} else {
None
}
});
// Clear the conversation history
self.conversation_history.clear();
self.used_tokens = 0;
// Re-add the original system prompt first (critical invariant)
if let Some(system_prompt) = original_system_prompt {
self.add_message(system_prompt);
}
// Re-add the README message if it existed
if let Some(readme) = readme_message {
self.add_message(readme);
}
// Add the ACD stub if provided (before summary so LLM knows about dehydrated context)
if let Some(stub_content) = stub {
let stub_message = Message::new(MessageRole::System, stub_content);
self.add_message(stub_message);
}
// Add the summary as a system message
let summary_message = Message::new(
MessageRole::System,
format!("Previous conversation summary:\n\n{}", summary),
);
self.add_message(summary_message);
// Add the latest user message if provided
if let Some(user_msg) = latest_user_message {
self.add_message(Message::new(MessageRole::User, user_msg));
}
let new_chars: usize = self
.conversation_history
.iter()
.map(|m| m.content.len())
.sum();
old_chars.saturating_sub(new_chars)
}
/// Check if we should trigger context thinning
/// Triggers at 50%, 60%, 70%, and 80% thresholds
pub fn should_thin(&self) -> bool {
@@ -676,7 +745,8 @@ Format this as a detailed but concise summary that can be used to resume the con
}
/// Recalculate token usage based on current conversation history
fn recalculate_tokens(&mut self) {
/// Recalculate the token count based on current conversation history.
pub fn recalculate_tokens(&mut self) {
let mut total = 0;
for message in &self.conversation_history {
total += Self::estimate_tokens(&message.content);