ConversationEntry enum: typed memory vs conversation messages
Replace untyped message list with ConversationEntry enum:
- Message(Message) — regular conversation turn
- Memory { key, message } — memory content with preserved message
for KV cache round-tripping
Budget counts memory vs conversation by matching on enum variant.
Debug screen labels memory entries with [memory: key]. No heuristic
tool-name scanning.
Custom serde: Memory serializes with a memory_key field alongside
the message fields, deserializes by checking for the field.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
eb4dae04cb
commit
b9e3568385
3 changed files with 153 additions and 93 deletions
|
|
@ -103,7 +103,7 @@ impl Agent {
|
|||
journal: Vec::new(),
|
||||
working_stack: Vec::new(),
|
||||
loaded_nodes: Vec::new(),
|
||||
messages: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
};
|
||||
let session_id = format!("poc-agent-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
||||
let agent_cycles = crate::subconscious::subconscious::AgentCycleState::new(&session_id);
|
||||
|
|
@ -140,7 +140,7 @@ impl Agent {
|
|||
if !jnl.is_empty() {
|
||||
msgs.push(Message::user(jnl));
|
||||
}
|
||||
msgs.extend(self.context.messages.iter().cloned());
|
||||
msgs.extend(self.context.entries.iter().map(|e| e.api_message().clone()));
|
||||
msgs
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ impl Agent {
|
|||
eprintln!("warning: failed to log message: {:#}", e);
|
||||
}
|
||||
}
|
||||
self.context.messages.push(msg);
|
||||
self.context.entries.push(ConversationEntry::Message(msg));
|
||||
}
|
||||
|
||||
/// Push a context-only message (system prompt, identity context,
|
||||
|
|
@ -673,32 +673,41 @@ impl Agent {
|
|||
}
|
||||
|
||||
// Conversation — each message as a child
|
||||
let conv_messages = &self.context.messages;
|
||||
let conv_messages = &self.context.entries;
|
||||
let conv_children: Vec<ContextSection> = conv_messages.iter().enumerate()
|
||||
.map(|(i, msg)| {
|
||||
let text = msg.content.as_ref()
|
||||
.map(|(i, entry)| {
|
||||
let m = entry.message();
|
||||
let text = m.content.as_ref()
|
||||
.map(|c| c.as_text().to_string())
|
||||
.unwrap_or_default();
|
||||
let tool_info = msg.tool_calls.as_ref().map(|tc| {
|
||||
let tool_info = m.tool_calls.as_ref().map(|tc| {
|
||||
tc.iter()
|
||||
.map(|c| c.function.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
});
|
||||
let label = match (&msg.role, &tool_info) {
|
||||
(_, Some(tools)) => format!("[tool_call: {}]", tools),
|
||||
_ => {
|
||||
let preview: String = text.chars().take(60).collect();
|
||||
let preview = preview.replace('\n', " ");
|
||||
if text.len() > 60 { format!("{}...", preview) } else { preview }
|
||||
let label = if entry.is_memory() {
|
||||
if let ConversationEntry::Memory { key, .. } = entry {
|
||||
format!("[memory: {}]", key)
|
||||
} else { unreachable!() }
|
||||
} else {
|
||||
match &tool_info {
|
||||
Some(tools) => format!("[tool_call: {}]", tools),
|
||||
None => {
|
||||
let preview: String = text.chars().take(60).collect();
|
||||
let preview = preview.replace('\n', " ");
|
||||
if text.len() > 60 { format!("{}...", preview) } else { preview }
|
||||
}
|
||||
}
|
||||
};
|
||||
let tokens = count(&text);
|
||||
let role_name = match msg.role {
|
||||
Role::Assistant => "PoC",
|
||||
Role::User => "Kent",
|
||||
Role::Tool => "tool",
|
||||
Role::System => "system",
|
||||
let role_name = if entry.is_memory() { "mem" } else {
|
||||
match m.role {
|
||||
Role::Assistant => "PoC",
|
||||
Role::User => "Kent",
|
||||
Role::Tool => "tool",
|
||||
Role::System => "system",
|
||||
}
|
||||
};
|
||||
ContextSection {
|
||||
name: format!("[{}] {}: {}", i, role_name, label),
|
||||
|
|
@ -846,7 +855,8 @@ impl Agent {
|
|||
/// all previous ones. The tool result message (right before each image
|
||||
/// message) already records what was loaded, so no info is lost.
|
||||
fn age_out_images(&mut self) {
|
||||
for msg in &mut self.context.messages {
|
||||
for entry in &mut self.context.entries {
|
||||
let msg = entry.message_mut();
|
||||
if let Some(MessageContent::Parts(parts)) = &msg.content {
|
||||
let has_images = parts.iter().any(|p| matches!(p, ContentPart::ImageUrl { .. }));
|
||||
if !has_images {
|
||||
|
|
@ -891,7 +901,8 @@ impl Agent {
|
|||
let mut strip_ids: Vec<String> = Vec::new();
|
||||
let mut strip_msg_indices: Vec<usize> = Vec::new();
|
||||
|
||||
for (i, msg) in self.context.messages.iter().enumerate() {
|
||||
for (i, entry) in self.context.entries.iter().enumerate() {
|
||||
let msg = entry.message();
|
||||
if msg.role != Role::Assistant {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -917,8 +928,8 @@ impl Agent {
|
|||
}
|
||||
|
||||
// Remove in reverse order to preserve indices
|
||||
self.context.messages.retain(|msg| {
|
||||
// Strip the assistant messages we identified
|
||||
self.context.entries.retain(|entry| {
|
||||
let msg = entry.message();
|
||||
if msg.role == Role::Assistant {
|
||||
if let Some(calls) = &msg.tool_calls {
|
||||
if calls.iter().all(|c| strip_ids.contains(&c.id)) {
|
||||
|
|
@ -926,7 +937,6 @@ impl Agent {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Strip matching tool results
|
||||
if msg.role == Role::Tool {
|
||||
if let Some(ref id) = msg.tool_call_id {
|
||||
if strip_ids.contains(id) {
|
||||
|
|
@ -955,7 +965,8 @@ impl Agent {
|
|||
|
||||
/// Internal compaction — rebuilds context window from current messages.
|
||||
fn do_compact(&mut self) {
|
||||
let conversation: Vec<Message> = self.context.messages.clone();
|
||||
let conversation: Vec<Message> = self.context.entries.iter()
|
||||
.map(|e| e.api_message().clone()).collect();
|
||||
let (messages, journal) = crate::agent::context::build_context_window(
|
||||
&self.context,
|
||||
&conversation,
|
||||
|
|
@ -963,7 +974,8 @@ impl Agent {
|
|||
&self.tokenizer,
|
||||
);
|
||||
self.context.journal = journal::parse_journal_text(&journal);
|
||||
self.context.messages = messages;
|
||||
self.context.entries = messages.into_iter()
|
||||
.map(ConversationEntry::Message).collect();
|
||||
self.last_prompt_tokens = 0;
|
||||
|
||||
self.publish_context_state();
|
||||
|
|
@ -1025,8 +1037,9 @@ impl Agent {
|
|||
dbglog!("[restore] journal text: {} chars, {} lines",
|
||||
journal.len(), journal.lines().count());
|
||||
self.context.journal = journal::parse_journal_text(&journal);
|
||||
self.context.messages = messages;
|
||||
dbglog!("[restore] built context window: {} messages", self.context.messages.len());
|
||||
self.context.entries = messages.into_iter()
|
||||
.map(ConversationEntry::Message).collect();
|
||||
dbglog!("[restore] built context window: {} entries", self.context.entries.len());
|
||||
self.last_prompt_tokens = 0;
|
||||
|
||||
self.publish_context_state();
|
||||
|
|
@ -1043,19 +1056,19 @@ impl Agent {
|
|||
&self.client.model
|
||||
}
|
||||
|
||||
/// Get the conversation history for persistence.
|
||||
pub fn messages(&self) -> &[Message] {
|
||||
&self.context.messages
|
||||
/// Get the conversation entries for persistence.
|
||||
pub fn entries(&self) -> &[ConversationEntry] {
|
||||
&self.context.entries
|
||||
}
|
||||
|
||||
/// Mutable access to conversation history (for /retry).
|
||||
pub fn messages_mut(&mut self) -> &mut Vec<Message> {
|
||||
&mut self.context.messages
|
||||
/// Mutable access to conversation entries (for /retry).
|
||||
pub fn entries_mut(&mut self) -> &mut Vec<ConversationEntry> {
|
||||
&mut self.context.entries
|
||||
}
|
||||
|
||||
/// Restore from a saved conversation.
|
||||
pub fn restore(&mut self, messages: Vec<Message>) {
|
||||
self.context.messages = messages;
|
||||
/// Restore from saved conversation entries.
|
||||
pub fn restore(&mut self, entries: Vec<ConversationEntry>) {
|
||||
self.context.entries = entries;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue