fix(approval): harden CLI approval flow and summaries
This commit is contained in:
parent
ab561baa97
commit
bb641d28c2
2 changed files with 54 additions and 33 deletions
|
|
@ -1058,39 +1058,53 @@ pub async fn run(
|
||||||
} else {
|
} else {
|
||||||
println!("🦀 ZeroClaw Interactive Mode");
|
println!("🦀 ZeroClaw Interactive Mode");
|
||||||
println!("Type /quit to exit.\n");
|
println!("Type /quit to exit.\n");
|
||||||
|
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
|
||||||
let cli = crate::channels::CliChannel::new();
|
let cli = crate::channels::CliChannel::new();
|
||||||
|
|
||||||
// Spawn listener
|
|
||||||
let listen_handle = tokio::spawn(async move {
|
|
||||||
let _ = crate::channels::Channel::listen(&cli, tx).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Persistent conversation history across turns
|
// Persistent conversation history across turns
|
||||||
let mut history = vec![ChatMessage::system(&system_prompt)];
|
let mut history = vec![ChatMessage::system(&system_prompt)];
|
||||||
|
|
||||||
while let Some(msg) = rx.recv().await {
|
loop {
|
||||||
|
print!("> ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
match std::io::stdin().read_line(&mut input) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("\nError reading input: {e}\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_input = input.trim().to_string();
|
||||||
|
if user_input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if user_input == "/quit" || user_input == "/exit" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-save conversation turns
|
// Auto-save conversation turns
|
||||||
if config.memory.auto_save {
|
if config.memory.auto_save {
|
||||||
let user_key = autosave_memory_key("user_msg");
|
let user_key = autosave_memory_key("user_msg");
|
||||||
let _ = mem
|
let _ = mem
|
||||||
.store(&user_key, &msg.content, MemoryCategory::Conversation, None)
|
.store(&user_key, &user_input, MemoryCategory::Conversation, None)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject memory + hardware RAG context into user message
|
// Inject memory + hardware RAG context into user message
|
||||||
let mem_context = build_context(mem.as_ref(), &msg.content).await;
|
let mem_context = build_context(mem.as_ref(), &user_input).await;
|
||||||
let rag_limit = if config.agent.compact_context { 2 } else { 5 };
|
let rag_limit = if config.agent.compact_context { 2 } else { 5 };
|
||||||
let hw_context = hardware_rag
|
let hw_context = hardware_rag
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|r| build_hardware_context(r, &msg.content, &board_names, rag_limit))
|
.map(|r| build_hardware_context(r, &user_input, &board_names, rag_limit))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let context = format!("{mem_context}{hw_context}");
|
let context = format!("{mem_context}{hw_context}");
|
||||||
let enriched = if context.is_empty() {
|
let enriched = if context.is_empty() {
|
||||||
msg.content.clone()
|
user_input.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("{context}{}", msg.content)
|
format!("{context}{user_input}")
|
||||||
};
|
};
|
||||||
|
|
||||||
history.push(ChatMessage::user(&enriched));
|
history.push(ChatMessage::user(&enriched));
|
||||||
|
|
@ -1116,7 +1130,11 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
final_output = response.clone();
|
final_output = response.clone();
|
||||||
println!("\n{response}\n");
|
if let Err(e) =
|
||||||
|
crate::channels::Channel::send(&cli, &format!("\n{response}\n"), "user").await
|
||||||
|
{
|
||||||
|
eprintln!("\nError sending CLI response: {e}\n");
|
||||||
|
}
|
||||||
observer.record_event(&ObserverEvent::TurnComplete);
|
observer.record_event(&ObserverEvent::TurnComplete);
|
||||||
|
|
||||||
// Auto-compaction before hard trimming to preserve long-context signal.
|
// Auto-compaction before hard trimming to preserve long-context signal.
|
||||||
|
|
@ -1139,8 +1157,6 @@ pub async fn run(
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listen_handle.abort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
|
||||||
|
|
@ -201,20 +201,10 @@ fn summarize_args(args: &serde_json::Value) -> String {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let val = match v {
|
let val = match v {
|
||||||
serde_json::Value::String(s) => {
|
serde_json::Value::String(s) => truncate_for_summary(s, 80),
|
||||||
if s.len() > 80 {
|
|
||||||
format!("{}…", &s[..77])
|
|
||||||
} else {
|
|
||||||
s.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => {
|
other => {
|
||||||
let s = other.to_string();
|
let s = other.to_string();
|
||||||
if s.len() > 80 {
|
truncate_for_summary(&s, 80)
|
||||||
format!("{}…", &s[..77])
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
format!("{k}: {val}")
|
format!("{k}: {val}")
|
||||||
|
|
@ -224,15 +214,21 @@ fn summarize_args(args: &serde_json::Value) -> String {
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
let s = other.to_string();
|
let s = other.to_string();
|
||||||
if s.len() > 120 {
|
truncate_for_summary(&s, 120)
|
||||||
format!("{}…", &s[..117])
|
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn truncate_for_summary(input: &str, max_chars: usize) -> String {
|
||||||
|
let mut chars = input.chars();
|
||||||
|
let truncated: String = chars.by_ref().take(max_chars).collect();
|
||||||
|
if chars.next().is_some() {
|
||||||
|
format!("{truncated}…")
|
||||||
|
} else {
|
||||||
|
input.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -404,6 +400,15 @@ mod tests {
|
||||||
assert!(summary.len() < 200);
|
assert!(summary.len() < 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarize_args_unicode_safe_truncation() {
|
||||||
|
let long_val = "🦀".repeat(120);
|
||||||
|
let args = serde_json::json!({"content": long_val});
|
||||||
|
let summary = summarize_args(&args);
|
||||||
|
assert!(summary.contains("content:"));
|
||||||
|
assert!(summary.contains('…'));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn summarize_args_non_object() {
|
fn summarize_args_non_object() {
|
||||||
let args = serde_json::json!("just a string");
|
let args = serde_json::json!("just a string");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue