mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
feat(loop): add deterministic context compaction to Controller and Kai thread
This commit is contained in:
@@ -622,6 +622,64 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compacts transcript history, pruning stale tool outputs while keeping error diagnostics verbatim.
|
||||
pub async fn compact_messages(
|
||||
self: &Arc<Self>,
|
||||
_turn: &str,
|
||||
mut messages: Vec<Value>,
|
||||
) -> Option<Vec<Value>> {
|
||||
if messages.len() <= 4 {
|
||||
return None;
|
||||
}
|
||||
let cutoff = messages.len().saturating_sub(4);
|
||||
let mut changed = false;
|
||||
for (i, msg) in messages.iter_mut().enumerate() {
|
||||
if i > 0 && i < cutoff {
|
||||
if let Some(role) = msg.get("role").and_then(|r| r.as_str()) {
|
||||
if role == "tool" {
|
||||
if let Some(content) = msg.get("content").and_then(|c| c.as_str()) {
|
||||
if content.len() > 1000 {
|
||||
let error_lines: Vec<&str> = content
|
||||
.lines()
|
||||
.filter(|l| {
|
||||
let low = l.to_lowercase();
|
||||
low.contains("error:")
|
||||
|| low.contains("failed")
|
||||
|| low.contains("panic")
|
||||
|| low.contains("exception")
|
||||
|| low.contains("cannot find")
|
||||
})
|
||||
.take(10)
|
||||
.collect();
|
||||
let compacted = if !error_lines.is_empty() {
|
||||
format!(
|
||||
"[Diagnostics retained verbatim]:\n{}\n[... {} chars omitted ...]",
|
||||
error_lines.join("\n"),
|
||||
content.len()
|
||||
)
|
||||
} else {
|
||||
let head_len = content.char_indices().nth(300).map_or(content.len(), |(i, _)| i);
|
||||
format!(
|
||||
"{}\n[... {} chars omitted ...]",
|
||||
&content[..head_len],
|
||||
content.len()
|
||||
)
|
||||
};
|
||||
msg["content"] = Value::String(compacted);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
Some(messages)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_started(&self, turn: &str, start: [i64; 4]) {
|
||||
let mut s = self.state();
|
||||
s.turn = turn.to_string();
|
||||
@@ -744,6 +802,14 @@ impl Controller for Shared {
|
||||
) -> Answer<'a, BTreeMap<String, String>> {
|
||||
Box::pin(self.0.route(turn, request))
|
||||
}
|
||||
|
||||
fn compact<'a>(
|
||||
&'a self,
|
||||
turn: &'a str,
|
||||
messages: Vec<Value>,
|
||||
) -> Answer<'a, Option<Vec<Value>>> {
|
||||
Box::pin(self.0.compact_messages(turn, messages))
|
||||
}
|
||||
}
|
||||
|
||||
/// The contributors that feed the controller what the loop does.
|
||||
|
||||
@@ -559,3 +559,25 @@ async fn a_server_that_answers_without_distributions_is_kai_not_answering() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
assert!(!home.path().join("kai/decisions.jsonl").exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn compact_messages_preserves_error_diagnostics_verbatim() {
|
||||
let (_home, _server, kai) = kai("", risky).await;
|
||||
let (thread, _) = thread(&kai);
|
||||
let long_tool_output = format!("info: line 1\nerror: undefined symbol foo\ninfo: line 3\n{}", "a".repeat(1500));
|
||||
let msgs = vec![
|
||||
serde_json::json!({"role": "user", "content": "run task"}),
|
||||
serde_json::json!({"role": "tool", "content": long_tool_output}),
|
||||
serde_json::json!({"role": "assistant", "content": "checking"}),
|
||||
serde_json::json!({"role": "tool", "content": "ok"}),
|
||||
serde_json::json!({"role": "assistant", "content": "next action"}),
|
||||
serde_json::json!({"role": "tool", "content": "status done"}),
|
||||
serde_json::json!({"role": "user", "content": "finalize"}),
|
||||
];
|
||||
let compacted = thread.compact_messages("t1", msgs).await.expect("compacted");
|
||||
let tool_msg = &compacted[1];
|
||||
let content = tool_msg["content"].as_str().unwrap();
|
||||
assert!(content.contains("[Diagnostics retained verbatim]:"));
|
||||
assert!(content.contains("error: undefined symbol foo"));
|
||||
assert!(content.contains("chars omitted"));
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@ pub trait Controller: Send + Sync {
|
||||
/// Routing hints for `turn`'s model requests.
|
||||
fn route<'a>(&'a self, turn: &'a str, request: Request)
|
||||
-> Answer<'a, BTreeMap<String, String>>;
|
||||
|
||||
/// Compact transcript history, pruning stale tool outputs while preserving error diagnostics verbatim.
|
||||
fn compact<'a>(
|
||||
&'a self,
|
||||
_turn: &'a str,
|
||||
_messages: Vec<serde_json::Value>,
|
||||
) -> Answer<'a, Option<Vec<serde_json::Value>>> {
|
||||
Box::pin(async { None })
|
||||
}
|
||||
}
|
||||
|
||||
/// The controller installed on a thread.
|
||||
@@ -102,6 +111,10 @@ impl Handle {
|
||||
pub async fn route(&self, turn: &str, request: Request) -> BTreeMap<String, String> {
|
||||
self.0.route(turn, request).await
|
||||
}
|
||||
|
||||
pub async fn compact(&self, turn: &str, messages: Vec<serde_json::Value>) -> Option<Vec<serde_json::Value>> {
|
||||
self.0.compact(turn, messages).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A step's shortlist: the tools that stay in direct view.
|
||||
@@ -155,4 +168,11 @@ mod tests {
|
||||
assert_eq!(verdict, policy);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_controller_default_compact_returns_none() {
|
||||
let handle = Handle(Arc::new(Loose));
|
||||
let res = handle.compact("t", vec![]).await;
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user