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

@ -151,7 +151,6 @@ pub struct AgentState {
pub pending_model_switch: Option<String>,
pub pending_dmn_pause: bool,
pub provenance: String,
pub conversation_log: Option<ConversationLog>,
pub generation: u64,
pub memory_scoring_in_flight: bool,
pub active_tools: tools::ActiveTools,
@ -172,6 +171,7 @@ impl Agent {
active_tools: tools::ActiveTools,
) -> Arc<Self> {
let mut context = ContextState::new();
context.conversation_log = conversation_log;
context.push(Section::System, AstNode::system_msg(&system_prompt));
let tool_defs: Vec<String> = tools::tools().iter()
@ -213,7 +213,6 @@ impl Agent {
pending_model_switch: None,
pending_dmn_pause: false,
provenance: "manual".to_string(),
conversation_log,
generation: 0,
memory_scoring_in_flight: false,
active_tools,
@ -249,7 +248,6 @@ impl Agent {
pending_model_switch: None,
pending_dmn_pause: false,
provenance: st.provenance.clone(),
conversation_log: None,
generation: 0,
memory_scoring_in_flight: false,
active_tools: tools::ActiveTools::new(),
@ -269,15 +267,8 @@ impl Agent {
pub async fn push_node(&self, node: AstNode) {
let node = node.with_timestamp(chrono::Utc::now());
let st = self.state.lock().await;
if let Some(ref log) = st.conversation_log {
if let Err(e) = log.append_node(&node) {
eprintln!("warning: failed to log entry: {:#}", e);
}
}
st.changed.notify_one();
drop(st);
self.context.lock().await.push(Section::Conversation, node);
self.state.lock().await.changed.notify_one();
}
/// Run the agent turn loop: assemble prompt, stream response,
@ -375,11 +366,13 @@ impl Agent {
}
Err(e) => return Err(anyhow::anyhow!("parser task panicked: {}", e)),
Ok(Ok(())) => {
let node = agent.context.lock().await.conversation()[branch_idx].clone();
let st = agent.state.lock().await;
if let Some(ref log) = st.conversation_log {
if let Err(e) = log.append_node(&node) {
eprintln!("warning: failed to log assistant response: {:#}", e);
// Assistant response was pushed to context by the parser;
// log it now that parsing is complete.
let ctx = agent.context.lock().await;
if let Some(ref log) = ctx.conversation_log {
let node = &ctx.conversation()[branch_idx];
if let Err(e) = log.append_node(node) {
eprintln!("warning: log: {:#}", e);
}
}
}
@ -469,19 +462,11 @@ impl Agent {
nodes.push(Self::make_tool_result_node(call, output));
}
// Single lock: remove from active, log, push to context
{
let mut st = agent.state.lock().await;
for (call, _) in &results {
st.active_tools.remove(&call.id);
}
for node in &nodes {
if let Some(ref log) = st.conversation_log {
if let Err(e) = log.append_node(node) {
eprintln!("warning: failed to log entry: {:#}", e);
}
}
}
}
{
let mut ctx = agent.context.lock().await;
@ -494,8 +479,8 @@ impl Agent {
async fn load_startup_journal(&self) {
let oldest_msg_ts = {
let st = self.state.lock().await;
st.conversation_log.as_ref().and_then(|log| log.oldest_timestamp())
let ctx = self.context.lock().await;
ctx.conversation_log.as_ref().and_then(|log| log.oldest_timestamp())
};
let store = match crate::store::Store::load() {
@ -574,8 +559,8 @@ impl Agent {
pub async fn restore_from_log(&self) -> bool {
let nodes = {
let st = self.state.lock().await;
match &st.conversation_log {
let ctx = self.context.lock().await;
match &ctx.conversation_log {
Some(log) => match log.read_nodes(64 * 1024 * 1024) {
Ok(nodes) if !nodes.is_empty() => nodes,
_ => return false,