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

@@ -155,6 +155,10 @@ pub struct Cli {
/// Automatically remind LLM to call remember tool after turns with tool calls
#[arg(long)]
pub auto_memory: bool,
/// Enable aggressive context dehydration (save context to disk on compaction)
#[arg(long)]
pub acd: bool,
}
pub async fn run() -> Result<()> {
@@ -390,6 +394,10 @@ pub async fn run() -> Result<()> {
if cli.auto_memory {
agent.set_auto_memory(true);
}
// Apply ACD flag if enabled
if cli.acd {
agent.set_acd_enabled(true);
}
run_with_machine_mode(agent, cli, project).await?;
} else {
@@ -433,6 +441,10 @@ pub async fn run() -> Result<()> {
if cli.auto_memory {
agent.set_auto_memory(true);
}
// Apply ACD flag if enabled
if cli.acd {
agent.set_acd_enabled(true);
}
run_with_console_mode(agent, cli, project, combined_content).await?;
}
@@ -617,6 +629,9 @@ async fn run_agent_mode(
// This prompts the LLM to save discoveries to project memory after each turn
agent.set_auto_memory(true);
// Enable ACD in agent mode for longer sessions
agent.set_acd_enabled(true);
// If resuming a session, restore context and TODO
let initial_task = if let Some(ref incomplete_session) = resuming_session {
// Restore the session context
@@ -1416,7 +1431,10 @@ async fn run_interactive<W: UiWriter>(
output.print(" /thinnify - Trigger context thinning (replaces large tool results with file references)");
output.print(" /skinnify - Trigger full context thinning (like /thinnify but for entire context, not just first third)");
output.print(" /clear - Clear session and start fresh (discards continuation artifacts)");
output.print(" /fragments - List dehydrated context fragments (ACD)");
output.print(" /rehydrate - Restore a dehydrated fragment by ID");
output.print(" /resume - List and switch to a previous session");
output.print(" /dump - Dump entire context window to file for debugging");
output.print(
" /readme - Reload README.md and AGENTS.md from disk",
);
@@ -1454,6 +1472,90 @@ async fn run_interactive<W: UiWriter>(
println!("{}", summary);
continue;
}
"/fragments" => {
if let Some(session_id) = agent.get_session_id() {
match g3_core::acd::list_fragments(session_id) {
Ok(fragments) => {
if fragments.is_empty() {
output.print("No dehydrated fragments found for this session.");
} else {
output.print(&format!("📦 {} dehydrated fragment(s):\n", fragments.len()));
for fragment in &fragments {
output.print(&fragment.generate_stub());
output.print("");
}
}
}
Err(e) => {
output.print(&format!("❌ Error listing fragments: {}", e));
}
}
} else {
output.print("No active session - fragments are session-scoped.");
}
continue;
}
cmd if cmd.starts_with("/rehydrate") => {
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
if parts.len() < 2 || parts[1].trim().is_empty() {
output.print("Usage: /rehydrate <fragment_id>");
output.print("Use /fragments to list available fragment IDs.");
} else {
let fragment_id = parts[1].trim();
if let Some(session_id) = agent.get_session_id() {
match g3_core::acd::Fragment::load(session_id, fragment_id) {
Ok(fragment) => {
output.print(&format!("✅ Fragment '{}' loaded ({} messages, ~{} tokens)",
fragment_id, fragment.message_count, fragment.estimated_tokens));
output.print("");
output.print(&fragment.generate_stub());
}
Err(e) => {
output.print(&format!("❌ Failed to load fragment '{}': {}", fragment_id, e));
}
}
} else {
output.print("No active session - fragments are session-scoped.");
}
}
continue;
}
"/dump" => {
// Dump entire context window to a file for debugging
let dump_dir = std::path::Path::new("tmp");
if !dump_dir.exists() {
if let Err(e) = std::fs::create_dir_all(dump_dir) {
output.print(&format!("❌ Failed to create tmp directory: {}", e));
continue;
}
}
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let dump_path = dump_dir.join(format!("context_dump_{}.txt", timestamp));
let context = agent.get_context_window();
let mut dump_content = String::new();
dump_content.push_str(&format!("# Context Window Dump\n"));
dump_content.push_str(&format!("# Timestamp: {}\n", chrono::Utc::now()));
dump_content.push_str(&format!("# Messages: {}\n", context.conversation_history.len()));
dump_content.push_str(&format!("# Used tokens: {} / {} ({:.1}%)\n\n",
context.used_tokens, context.total_tokens, context.percentage_used()));
for (i, msg) in context.conversation_history.iter().enumerate() {
dump_content.push_str(&format!("=== Message {} ===\n", i));
dump_content.push_str(&format!("Role: {:?}\n", msg.role));
dump_content.push_str(&format!("Kind: {:?}\n", msg.kind));
dump_content.push_str(&format!("Content ({} chars):\n", msg.content.len()));
dump_content.push_str(&msg.content);
dump_content.push_str("\n\n");
}
match std::fs::write(&dump_path, &dump_content) {
Ok(_) => output.print(&format!("📄 Context dumped to: {}", dump_path.display())),
Err(e) => output.print(&format!("❌ Failed to write dump: {}", e)),
}
continue;
}
"/clear" => {
output.print("🧹 Clearing session...");
agent.clear_session();
@@ -1751,6 +1853,71 @@ async fn run_interactive_machine(
println!("{}", summary);
continue;
}
"/fragments" => {
println!("COMMAND: fragments");
if let Some(session_id) = agent.get_session_id() {
match g3_core::acd::list_fragments(session_id) {
Ok(fragments) => {
println!("FRAGMENT_COUNT: {}", fragments.len());
for fragment in &fragments {
println!("FRAGMENT_ID: {}", fragment.fragment_id);
println!("FRAGMENT_MESSAGES: {}", fragment.message_count);
println!("FRAGMENT_TOKENS: {}", fragment.estimated_tokens);
}
}
Err(e) => {
println!("ERROR: {}", e);
}
}
} else {
println!("ERROR: No active session");
}
continue;
}
cmd if cmd.starts_with("/rehydrate") => {
println!("COMMAND: rehydrate");
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
if parts.len() < 2 || parts[1].trim().is_empty() {
println!("ERROR: Usage: /rehydrate <fragment_id>");
} else {
let fragment_id = parts[1].trim();
println!("FRAGMENT_ID: {}", fragment_id);
println!("RESULT: Use the rehydrate tool to restore fragment content");
}
continue;
}
"/dump" => {
println!("COMMAND: dump");
let dump_dir = std::path::Path::new("tmp");
if !dump_dir.exists() {
if let Err(e) = std::fs::create_dir_all(dump_dir) {
println!("ERROR: Failed to create tmp directory: {}", e);
continue;
}
}
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
let dump_path = dump_dir.join(format!("context_dump_{}.txt", timestamp));
let context = agent.get_context_window();
let mut dump_content = String::new();
dump_content.push_str(&format!("# Context Window Dump\n"));
dump_content.push_str(&format!("# Timestamp: {}\n", chrono::Utc::now()));
dump_content.push_str(&format!("# Messages: {}\n", context.conversation_history.len()));
dump_content.push_str(&format!("# Used tokens: {} / {} ({:.1}%)\n\n",
context.used_tokens, context.total_tokens, context.percentage_used()));
for (i, msg) in context.conversation_history.iter().enumerate() {
dump_content.push_str(&format!("=== Message {} ===\nRole: {:?}\nKind: {:?}\nContent ({} chars):\n{}\n\n",
i, msg.role, msg.kind, msg.content.len(), msg.content));
}
match std::fs::write(&dump_path, &dump_content) {
Ok(_) => println!("RESULT: Context dumped to {}", dump_path.display()),
Err(e) => println!("ERROR: Failed to write dump: {}", e),
}
continue;
}
"/clear" => {
println!("COMMAND: clear");
agent.clear_session();
@@ -1779,7 +1946,7 @@ async fn run_interactive_machine(
}
"/help" => {
println!("COMMAND: help");
println!("AVAILABLE_COMMANDS: /compact /thinnify /skinnify /clear /resume /readme /stats /help");
println!("AVAILABLE_COMMANDS: /compact /thinnify /skinnify /clear /dump /fragments /rehydrate /resume /readme /stats /help");
continue;
}
"/resume" => {