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,
|
personality,
|
||||||
journal: Vec::new(),
|
journal: Vec::new(),
|
||||||
working_stack: Vec::new(),
|
working_stack: Vec::new(),
|
||||||
loaded_nodes: Vec::new(),
|
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
};
|
};
|
||||||
let session_id = format!("poc-agent-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
let session_id = format!("poc-agent-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
||||||
|
|
@ -144,8 +143,8 @@ impl Agent {
|
||||||
msgs
|
msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run agent orchestration cycle and return formatted output to inject.
|
/// Run agent orchestration cycle, returning structured output.
|
||||||
fn run_agent_cycle(&mut self) -> Option<String> {
|
fn run_agent_cycle(&mut self) -> crate::subconscious::subconscious::AgentCycleOutput {
|
||||||
let transcript_path = self.conversation_log.as_ref()
|
let transcript_path = self.conversation_log.as_ref()
|
||||||
.map(|l| l.path().to_string_lossy().to_string())
|
.map(|l| l.path().to_string_lossy().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -157,12 +156,7 @@ impl Agent {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.agent_cycles.trigger(&session);
|
self.agent_cycles.trigger(&session);
|
||||||
let text = crate::subconscious::subconscious::format_agent_output(&self.agent_cycles.last_output);
|
std::mem::take(&mut self.agent_cycles.last_output)
|
||||||
if text.trim().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a conversation message — stamped and logged.
|
/// Push a conversation message — stamped and logged.
|
||||||
|
|
@ -201,13 +195,32 @@ impl Agent {
|
||||||
target: StreamTarget,
|
target: StreamTarget,
|
||||||
) -> Result<TurnResult> {
|
) -> Result<TurnResult> {
|
||||||
// Run agent orchestration cycle (surface-observe, reflect, journal)
|
// Run agent orchestration cycle (surface-observe, reflect, journal)
|
||||||
if let Some(hook_output) = self.run_agent_cycle() {
|
let cycle = self.run_agent_cycle();
|
||||||
let enriched = format!("{}\n\n<system-reminder>\n{}\n</system-reminder>",
|
|
||||||
user_input, hook_output);
|
// Surfaced memories — each as a separate Memory entry
|
||||||
self.push_message(Message::user(enriched));
|
for key in &cycle.surfaced_keys {
|
||||||
} else {
|
if let Some(rendered) = crate::cli::node::render_node(
|
||||||
self.push_message(Message::user(user_input));
|
&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 _ = ui_tx.send(UiMessage::AgentUpdate(self.agent_cycles.snapshots()));
|
||||||
|
|
||||||
let mut overflow_retries: u32 = 0;
|
let mut overflow_retries: u32 = 0;
|
||||||
|
|
@ -482,37 +495,16 @@ impl Agent {
|
||||||
Err(e) => format!("Error: {:#}", e),
|
Err(e) => format!("Error: {:#}", e),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track loaded/updated nodes
|
// Disambiguate memory renders from other tool results
|
||||||
if result.is_ok() {
|
let memory_key = if result.is_ok() {
|
||||||
match call.function.name.as_str() {
|
match call.function.name.as_str() {
|
||||||
"memory_render" | "memory_links" => {
|
"memory_render" =>
|
||||||
if let Some(key) = args.get("key").and_then(|v| v.as_str()) {
|
args.get("key").and_then(|v| v.as_str()).map(String::from),
|
||||||
if let Some(node) = crate::hippocampus::memory::MemoryNode::load(key) {
|
_ => None,
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let output = tools::ToolOutput {
|
let output = tools::ToolOutput {
|
||||||
text,
|
text,
|
||||||
|
|
@ -526,7 +518,13 @@ impl Agent {
|
||||||
result: output.text.clone(),
|
result: output.text.clone(),
|
||||||
});
|
});
|
||||||
let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.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;
|
ds.had_tool_calls = true;
|
||||||
if output.text.starts_with("Error:") {
|
if output.text.starts_with("Error:") {
|
||||||
ds.tool_errors += 1;
|
ds.tool_errors += 1;
|
||||||
|
|
@ -654,23 +652,29 @@ impl Agent {
|
||||||
children: stack_children,
|
children: stack_children,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loaded memory nodes — tracked by memory tools
|
// Memory nodes — extracted from Memory entries in the conversation
|
||||||
if !self.context.loaded_nodes.is_empty() {
|
let memory_entries: Vec<&ConversationEntry> = self.context.entries.iter()
|
||||||
let node_children: Vec<ContextSection> = self.context.loaded_nodes.iter()
|
.filter(|e| e.is_memory())
|
||||||
.map(|node| {
|
.collect();
|
||||||
let rendered = node.render();
|
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 {
|
ContextSection {
|
||||||
name: format!("{} (v{}, w={:.2}, {} links)",
|
name: key.to_string(),
|
||||||
node.key, node.version, node.weight, node.links.len()),
|
tokens: count(text),
|
||||||
tokens: count(&rendered),
|
content: String::new(),
|
||||||
content: String::new(), // don't duplicate in debug view
|
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let node_tokens: usize = node_children.iter().map(|c| c.tokens).sum();
|
let node_tokens: usize = node_children.iter().map(|c| c.tokens).sum();
|
||||||
sections.push(ContextSection {
|
sections.push(ContextSection {
|
||||||
name: format!("Memory nodes ({} loaded)", self.context.loaded_nodes.len()),
|
name: format!("Memory nodes ({} loaded)", memory_entries.len()),
|
||||||
tokens: node_tokens,
|
tokens: node_tokens,
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
children: node_children,
|
children: node_children,
|
||||||
|
|
|
||||||
|
|
@ -402,10 +402,6 @@ pub struct ContextState {
|
||||||
pub personality: Vec<(String, String)>,
|
pub personality: Vec<(String, String)>,
|
||||||
pub journal: Vec<crate::agent::journal::JournalEntry>,
|
pub journal: Vec<crate::agent::journal::JournalEntry>,
|
||||||
pub working_stack: Vec<String>,
|
pub working_stack: Vec<String>,
|
||||||
/// Memory nodes currently loaded — for debug display and refresh.
|
|
||||||
/// Content is NOT duplicated here; the actual content is in entries
|
|
||||||
/// as ConversationEntry::Memory.
|
|
||||||
pub loaded_nodes: Vec<crate::hippocampus::memory::MemoryNode>,
|
|
||||||
/// Conversation entries — messages and memory, interleaved in order.
|
/// Conversation entries — messages and memory, interleaved in order.
|
||||||
/// Does NOT include system prompt, personality, or journal.
|
/// Does NOT include system prompt, personality, or journal.
|
||||||
pub entries: Vec<ConversationEntry>,
|
pub entries: Vec<ConversationEntry>,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use std::time::{Duration, Instant, SystemTime};
|
||||||
pub use crate::session::HookSession;
|
pub use crate::session::HookSession;
|
||||||
|
|
||||||
/// Output from a single agent orchestration cycle.
|
/// Output from a single agent orchestration cycle.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct AgentCycleOutput {
|
pub struct AgentCycleOutput {
|
||||||
/// Memory node keys surfaced by surface-observe.
|
/// Memory node keys surfaced by surface-observe.
|
||||||
pub surfaced_keys: Vec<String>,
|
pub surfaced_keys: Vec<String>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue