Move conversation_log from AgentState to ContextState

The log records what goes into context, so it belongs under the context
lock. push() now auto-logs conversation entries, eliminating all the
manual lock-state-for-log, drop, lock-context-for-push dances.

- ContextState: new conversation_log field, Clone impl drops it
  (forked contexts don't log)
- push(): auto-logs Section::Conversation entries
- push_node, apply_tool_results, collect_results: all simplified
- collect_results: batch nodes under single context lock
- Assistant response logged under context lock after parse completes

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-09 00:32:32 -04:00
parent d82a2ae90d
commit ddfdbe6cb1
4 changed files with 112 additions and 109 deletions

View file

@ -99,12 +99,24 @@ pub enum AstNode {
/// The context window: four sections as Vec<AstNode>.
/// All mutation goes through ContextState methods to maintain the invariant
/// that token_ids on every leaf matches its rendered text.
#[derive(Clone)]
pub struct ContextState {
system: Vec<AstNode>,
identity: Vec<AstNode>,
journal: Vec<AstNode>,
conversation: Vec<AstNode>,
pub conversation_log: Option<crate::mind::log::ConversationLog>,
}
impl Clone for ContextState {
fn clone(&self) -> Self {
Self {
system: self.system.clone(),
identity: self.identity.clone(),
journal: self.journal.clone(),
conversation: self.conversation.clone(),
conversation_log: None, // forked contexts don't log
}
}
}
/// Identifies a section for mutation methods.
@ -698,6 +710,7 @@ impl ContextState {
identity: Vec::new(),
journal: Vec::new(),
conversation: Vec::new(),
conversation_log: None,
}
}
@ -753,6 +766,13 @@ impl ContextState {
}
pub fn push(&mut self, section: Section, node: AstNode) {
if section == Section::Conversation {
if let Some(ref log) = self.conversation_log {
if let Err(e) = log.append_node(&node) {
eprintln!("warning: log: {:#}", e);
}
}
}
self.section_mut(section).push(node);
}