unify memory tracking: entries are the single source of truth
Memory tool results (memory_render) are now pushed as ConversationEntry::Memory with the node key, instead of plain Messages. Remove loaded_nodes from ContextState — the debug screen reads memory info from Memory entries in the conversation. Surfaced memories from surface-observe are pushed as separate Memory entries, reflections as separate system-reminder messages. User input is no longer polluted with hook output. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
a21cf31ad2
commit
64dbcbf061
3 changed files with 60 additions and 59 deletions
|
|
@ -102,7 +102,6 @@ impl Agent {
|
|||
personality,
|
||||
journal: Vec::new(),
|
||||
working_stack: Vec::new(),
|
||||
loaded_nodes: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
};
|
||||
let session_id = format!("poc-agent-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
||||
|
|
@ -144,8 +143,8 @@ impl Agent {
|
|||
msgs
|
||||
}
|
||||
|
||||
/// Run agent orchestration cycle and return formatted output to inject.
|
||||
fn run_agent_cycle(&mut self) -> Option<String> {
|
||||
/// Run agent orchestration cycle, returning structured output.
|
||||
fn run_agent_cycle(&mut self) -> crate::subconscious::subconscious::AgentCycleOutput {
|
||||
let transcript_path = self.conversation_log.as_ref()
|
||||
.map(|l| l.path().to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
|
@ -157,12 +156,7 @@ impl Agent {
|
|||
);
|
||||
|
||||
self.agent_cycles.trigger(&session);
|
||||
let text = crate::subconscious::subconscious::format_agent_output(&self.agent_cycles.last_output);
|
||||
if text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(text)
|
||||
}
|
||||
std::mem::take(&mut self.agent_cycles.last_output)
|
||||
}
|
||||
|
||||
/// Push a conversation message — stamped and logged.
|
||||
|
|
@ -201,13 +195,32 @@ impl Agent {
|
|||
target: StreamTarget,
|
||||
) -> Result<TurnResult> {
|
||||
// Run agent orchestration cycle (surface-observe, reflect, journal)
|
||||
if let Some(hook_output) = self.run_agent_cycle() {
|
||||
let enriched = format!("{}\n\n<system-reminder>\n{}\n</system-reminder>",
|
||||
user_input, hook_output);
|
||||
self.push_message(Message::user(enriched));
|
||||
} else {
|
||||
self.push_message(Message::user(user_input));
|
||||
let cycle = self.run_agent_cycle();
|
||||
|
||||
// Surfaced memories — each as a separate Memory entry
|
||||
for key in &cycle.surfaced_keys {
|
||||
if let Some(rendered) = crate::cli::node::render_node(
|
||||
&crate::store::Store::load().unwrap_or_default(), key,
|
||||
) {
|
||||
let mut msg = Message::user(format!(
|
||||
"<system-reminder>\n--- {} (surfaced) ---\n{}\n</system-reminder>",
|
||||
key, rendered,
|
||||
));
|
||||
msg.stamp();
|
||||
self.push_entry(ConversationEntry::Memory { key: key.clone(), message: msg });
|
||||
}
|
||||
}
|
||||
|
||||
// Reflection — separate system reminder
|
||||
if let Some(ref reflection) = cycle.reflection {
|
||||
self.push_message(Message::user(format!(
|
||||
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
|
||||
reflection.trim(),
|
||||
)));
|
||||
}
|
||||
|
||||
// User input — clean, just what was typed
|
||||
self.push_message(Message::user(user_input));
|
||||
let _ = ui_tx.send(UiMessage::AgentUpdate(self.agent_cycles.snapshots()));
|
||||
|
||||
let mut overflow_retries: u32 = 0;
|
||||
|
|
@ -482,37 +495,16 @@ impl Agent {
|
|||
Err(e) => format!("Error: {:#}", e),
|
||||
};
|
||||
|
||||
// Track loaded/updated nodes
|
||||
if result.is_ok() {
|
||||
// Disambiguate memory renders from other tool results
|
||||
let memory_key = if result.is_ok() {
|
||||
match call.function.name.as_str() {
|
||||
"memory_render" | "memory_links" => {
|
||||
if let Some(key) = args.get("key").and_then(|v| v.as_str()) {
|
||||
if let Some(node) = crate::hippocampus::memory::MemoryNode::load(key) {
|
||||
// Replace if already tracked, otherwise add
|
||||
if let Some(existing) = self.context.loaded_nodes.iter_mut()
|
||||
.find(|n| n.key == node.key) {
|
||||
*existing = node;
|
||||
} else {
|
||||
self.context.loaded_nodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"memory_write" => {
|
||||
if let Some(key) = args.get("key").and_then(|v| v.as_str()) {
|
||||
if let Some(node) = crate::hippocampus::memory::MemoryNode::load(key) {
|
||||
// Refresh if already tracked
|
||||
if let Some(existing) = self.context.loaded_nodes.iter_mut()
|
||||
.find(|n| n.key == node.key) {
|
||||
*existing = node;
|
||||
}
|
||||
// Don't auto-add writes — only renders register nodes
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
"memory_render" =>
|
||||
args.get("key").and_then(|v| v.as_str()).map(String::from),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let output = tools::ToolOutput {
|
||||
text,
|
||||
|
|
@ -526,7 +518,13 @@ impl Agent {
|
|||
result: output.text.clone(),
|
||||
});
|
||||
let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.clone() });
|
||||
self.push_message(Message::tool_result(&call.id, &output.text));
|
||||
let mut msg = Message::tool_result(&call.id, &output.text);
|
||||
msg.stamp();
|
||||
if let Some(key) = memory_key {
|
||||
self.push_entry(ConversationEntry::Memory { key, message: msg });
|
||||
} else {
|
||||
self.push_entry(ConversationEntry::Message(msg));
|
||||
}
|
||||
ds.had_tool_calls = true;
|
||||
if output.text.starts_with("Error:") {
|
||||
ds.tool_errors += 1;
|
||||
|
|
@ -654,23 +652,29 @@ impl Agent {
|
|||
children: stack_children,
|
||||
});
|
||||
|
||||
// Loaded memory nodes — tracked by memory tools
|
||||
if !self.context.loaded_nodes.is_empty() {
|
||||
let node_children: Vec<ContextSection> = self.context.loaded_nodes.iter()
|
||||
.map(|node| {
|
||||
let rendered = node.render();
|
||||
// Memory nodes — extracted from Memory entries in the conversation
|
||||
let memory_entries: Vec<&ConversationEntry> = self.context.entries.iter()
|
||||
.filter(|e| e.is_memory())
|
||||
.collect();
|
||||
if !memory_entries.is_empty() {
|
||||
let node_children: Vec<ContextSection> = memory_entries.iter()
|
||||
.map(|entry| {
|
||||
let key = match entry {
|
||||
ConversationEntry::Memory { key, .. } => key.as_str(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let text = entry.message().content_text();
|
||||
ContextSection {
|
||||
name: format!("{} (v{}, w={:.2}, {} links)",
|
||||
node.key, node.version, node.weight, node.links.len()),
|
||||
tokens: count(&rendered),
|
||||
content: String::new(), // don't duplicate in debug view
|
||||
name: key.to_string(),
|
||||
tokens: count(text),
|
||||
content: String::new(),
|
||||
children: Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let node_tokens: usize = node_children.iter().map(|c| c.tokens).sum();
|
||||
sections.push(ContextSection {
|
||||
name: format!("Memory nodes ({} loaded)", self.context.loaded_nodes.len()),
|
||||
name: format!("Memory nodes ({} loaded)", memory_entries.len()),
|
||||
tokens: node_tokens,
|
||||
content: String::new(),
|
||||
children: node_children,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue