Agent/AgentState split complete — separate context and state locks

Agent is now Arc<Agent> (immutable config). ContextState and AgentState
have separate tokio::sync::Mutex locks. The parser locks only context,
tool dispatch locks only state. No contention between the two.

All callers migrated: mind/, user/, tools/, oneshot, dmn, learn.
28 tests pass, zero errors.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-08 15:47:21 -04:00
parent 1d61b091b0
commit 0b9813431a
8 changed files with 156 additions and 159 deletions

View file

@ -460,8 +460,6 @@ impl Subconscious {
|| outputs.contains_key("reflection")
|| outputs.contains_key("thalamus");
if has_outputs {
let mut ag = agent.lock().await;
if let Some(surface_str) = outputs.get("surface") {
let store = crate::store::Store::cached().await.ok();
let store_guard = match &store {
@ -472,30 +470,30 @@ impl Subconscious {
let rendered = store_guard.as_ref()
.and_then(|s| crate::cli::node::render_node(s, key));
if let Some(rendered) = rendered {
ag.push_node(AstNode::memory(
agent.push_node(AstNode::memory(
key,
format!("--- {} (surfaced) ---\n{}", key, rendered),
));
)).await;
}
}
}
if let Some(reflection) = outputs.get("reflection") {
if !reflection.trim().is_empty() {
ag.push_node(AstNode::dmn(format!(
agent.push_node(AstNode::dmn(format!(
"--- subconscious reflection ---\n{}",
reflection.trim(),
)));
))).await;
}
}
if let Some(nudge) = outputs.get("thalamus") {
let nudge = nudge.trim();
if !nudge.is_empty() && nudge != "ok" {
ag.push_node(AstNode::dmn(format!(
agent.push_node(AstNode::dmn(format!(
"--- thalamus ---\n{}",
nudge,
)));
))).await;
}
}
}
@ -513,13 +511,13 @@ impl Subconscious {
/// Trigger subconscious agents that are due to run.
pub async fn trigger(&mut self, agent: &Arc<Agent>) {
let (conversation_bytes, memory_keys) = {
let ag = agent.lock().await;
let bytes = ag.context.conversation().iter()
let ctx = agent.context.lock().await;
let bytes = ctx.conversation().iter()
.filter(|node| !matches!(node.leaf().map(|l| l.body()),
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
.map(|node| node.render().len() as u64)
.sum::<u64>();
let keys: Vec<String> = ag.context.conversation().iter().filter_map(|node| {
let keys: Vec<String> = ctx.conversation().iter().filter_map(|node| {
if let Some(NodeBody::Memory { key, .. }) = node.leaf().map(|l| l.body()) {
Some(key.clone())
} else { None }
@ -541,23 +539,21 @@ impl Subconscious {
if to_run.is_empty() { return; }
let conscious = agent.lock().await;
for (idx, mut auto) in to_run {
dbglog!("[subconscious] triggering {}", auto.name);
let mut forked = conscious.fork(auto.tools.clone());
forked.provenance = format!("agent:{}", auto.name);
let fork_point = forked.context.conversation().len();
let shared_forked = Arc::new(tokio::sync::Mutex::new(forked));
let forked = agent.fork(auto.tools.clone()).await;
forked.state.lock().await.provenance = format!("agent:{}", auto.name);
let fork_point = forked.context.lock().await.conversation().len();
self.agents[idx].forked_agent = Some(shared_forked.clone());
self.agents[idx].forked_agent = Some(forked.clone());
self.agents[idx].fork_point = fork_point;
let keys = memory_keys.clone();
let st = self.state.clone();
self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run_forked_shared(&shared_forked, &keys, &st).await;
let result = auto.run_forked_shared(&forked, &keys, &st).await;
(auto, result)
}));
}