forked from kent/consciousness
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:
parent
1d61b091b0
commit
0b9813431a
8 changed files with 156 additions and 159 deletions
142
src/user/chat.rs
142
src/user/chat.rs
|
|
@ -33,17 +33,17 @@ fn commands() -> Vec<SlashCommand> { vec![
|
|||
handler: |s, _| { let _ = s.mind_tx.send(MindCommand::NewSession); } },
|
||||
SlashCommand { name: "/save", help: "Save session to disk",
|
||||
handler: |s, _| {
|
||||
if let Ok(mut ag) = s.agent.try_lock() { ag.notify("saved"); }
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("saved"); }
|
||||
} },
|
||||
SlashCommand { name: "/model", help: "Show/switch model (/model <name>)",
|
||||
handler: |s, arg| {
|
||||
if arg.is_empty() {
|
||||
if let Ok(mut ag) = s.agent.try_lock() {
|
||||
let names = ag.app_config.model_names();
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() {
|
||||
let names = s.agent.app_config.model_names();
|
||||
let label = if names.is_empty() {
|
||||
format!("model: {}", ag.model())
|
||||
format!("model: {}", s.agent.model())
|
||||
} else {
|
||||
format!("model: {} ({})", ag.model(), names.join(", "))
|
||||
format!("model: {} ({})", s.agent.model(), names.join(", "))
|
||||
};
|
||||
ag.notify(label);
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ fn commands() -> Vec<SlashCommand> { vec![
|
|||
SlashCommand { name: "/dmn", help: "Show DMN state",
|
||||
handler: |s, _| {
|
||||
let st = s.shared_mind.lock().unwrap();
|
||||
if let Ok(mut ag) = s.agent.try_lock() {
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() {
|
||||
ag.notify(format!("DMN: {:?} ({}/{})", st.dmn, st.dmn_turns, st.max_dmn_turns));
|
||||
}
|
||||
} },
|
||||
|
|
@ -70,7 +70,7 @@ fn commands() -> Vec<SlashCommand> { vec![
|
|||
let mut st = s.shared_mind.lock().unwrap();
|
||||
st.dmn = crate::mind::dmn::State::Resting { since: std::time::Instant::now() };
|
||||
st.dmn_turns = 0;
|
||||
if let Ok(mut ag) = s.agent.try_lock() { ag.notify("DMN sleeping"); }
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN sleeping"); }
|
||||
} },
|
||||
SlashCommand { name: "/wake", help: "Wake DMN to foraging",
|
||||
handler: |s, _| {
|
||||
|
|
@ -78,14 +78,14 @@ fn commands() -> Vec<SlashCommand> { vec![
|
|||
if matches!(st.dmn, crate::mind::dmn::State::Off) { crate::mind::dmn::set_off(false); }
|
||||
st.dmn = crate::mind::dmn::State::Foraging;
|
||||
st.dmn_turns = 0;
|
||||
if let Ok(mut ag) = s.agent.try_lock() { ag.notify("DMN foraging"); }
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN foraging"); }
|
||||
} },
|
||||
SlashCommand { name: "/pause", help: "Full stop — no autonomous ticks (Ctrl+P)",
|
||||
handler: |s, _| {
|
||||
let mut st = s.shared_mind.lock().unwrap();
|
||||
st.dmn = crate::mind::dmn::State::Paused;
|
||||
st.dmn_turns = 0;
|
||||
if let Ok(mut ag) = s.agent.try_lock() { ag.notify("DMN paused"); }
|
||||
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN paused"); }
|
||||
} },
|
||||
SlashCommand { name: "/help", help: "Show this help",
|
||||
handler: |s, _| { notify_help(&s.agent); } },
|
||||
|
|
@ -101,36 +101,27 @@ pub async fn cmd_switch_model(
|
|||
agent: &std::sync::Arc<crate::agent::Agent>,
|
||||
name: &str,
|
||||
) {
|
||||
let resolved = {
|
||||
let ag = agent.lock().await;
|
||||
match ag.app_config.resolve_model(name) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
agent.lock().await.notify(format!("model error: {}", e));
|
||||
return;
|
||||
}
|
||||
let resolved = match agent.app_config.resolve_model(name) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
agent.state.lock().await.notify(format!("model error: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let new_client = crate::agent::api::ApiClient::new(
|
||||
let _new_client = crate::agent::api::ApiClient::new(
|
||||
&resolved.api_base, &resolved.api_key, &resolved.model_id,
|
||||
);
|
||||
let prompt_changed = {
|
||||
let ag = agent.lock().await;
|
||||
resolved.prompt_file != ag.prompt_file
|
||||
};
|
||||
let mut ag = agent.lock().await;
|
||||
ag.swap_client(new_client);
|
||||
let prompt_changed = resolved.prompt_file != agent.prompt_file;
|
||||
if prompt_changed {
|
||||
ag.prompt_file = resolved.prompt_file.clone();
|
||||
ag.compact();
|
||||
ag.notify(format!("switched to {} (recompacted)", resolved.model_id));
|
||||
agent.compact().await;
|
||||
agent.state.lock().await.notify(format!("switched to {} (recompacted)", resolved.model_id));
|
||||
} else {
|
||||
ag.notify(format!("switched to {}", resolved.model_id));
|
||||
agent.state.lock().await.notify(format!("switched to {}", resolved.model_id));
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_help(agent: &std::sync::Arc<crate::agent::Agent>) {
|
||||
if let Ok(mut ag) = agent.try_lock() {
|
||||
if let Ok(mut ag) = agent.state.try_lock() {
|
||||
let mut help = String::new();
|
||||
for cmd in &commands() {
|
||||
help.push_str(&format!("{:12} {}\n", cmd.name, cmd.help));
|
||||
|
|
@ -471,48 +462,57 @@ impl InteractScreen {
|
|||
}
|
||||
self.pending_display_count = 0;
|
||||
|
||||
if let Ok(agent) = self.agent.try_lock() {
|
||||
let generation = agent.generation;
|
||||
let entries = agent.conversation();
|
||||
let (generation, entries) = {
|
||||
let st = match self.agent.state.try_lock() {
|
||||
Ok(st) => st,
|
||||
Err(_) => return,
|
||||
};
|
||||
let generation = st.generation;
|
||||
drop(st);
|
||||
let ctx = match self.agent.context.try_lock() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(_) => return,
|
||||
};
|
||||
(generation, ctx.conversation().to_vec())
|
||||
};
|
||||
|
||||
if generation != self.last_generation || entries.len() < self.last_entries.len() {
|
||||
self.conversation = PaneState::new(true);
|
||||
self.autonomous = PaneState::new(true);
|
||||
self.tools = PaneState::new(false);
|
||||
self.last_entries.clear();
|
||||
}
|
||||
if generation != self.last_generation || entries.len() < self.last_entries.len() {
|
||||
self.conversation = PaneState::new(true);
|
||||
self.autonomous = PaneState::new(true);
|
||||
self.tools = PaneState::new(false);
|
||||
self.last_entries.clear();
|
||||
}
|
||||
|
||||
let start = self.last_entries.len();
|
||||
for node in entries.iter().skip(start) {
|
||||
for (target, text, marker) in Self::route_node(node) {
|
||||
match target {
|
||||
PaneTarget::Conversation => {
|
||||
self.conversation.current_color = Color::Cyan;
|
||||
self.conversation.append_text(&text);
|
||||
self.conversation.pending_marker = marker;
|
||||
self.conversation.flush_pending();
|
||||
},
|
||||
PaneTarget::ConversationAssistant => {
|
||||
self.conversation.current_color = Color::Reset;
|
||||
self.conversation.append_text(&text);
|
||||
self.conversation.pending_marker = marker;
|
||||
self.conversation.flush_pending();
|
||||
},
|
||||
PaneTarget::Tools =>
|
||||
self.tools.push_line(text, Color::Yellow),
|
||||
PaneTarget::ToolResult => {
|
||||
for line in text.lines().take(20) {
|
||||
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
||||
}
|
||||
let start = self.last_entries.len();
|
||||
for node in entries.iter().skip(start) {
|
||||
for (target, text, marker) in Self::route_node(node) {
|
||||
match target {
|
||||
PaneTarget::Conversation => {
|
||||
self.conversation.current_color = Color::Cyan;
|
||||
self.conversation.append_text(&text);
|
||||
self.conversation.pending_marker = marker;
|
||||
self.conversation.flush_pending();
|
||||
},
|
||||
PaneTarget::ConversationAssistant => {
|
||||
self.conversation.current_color = Color::Reset;
|
||||
self.conversation.append_text(&text);
|
||||
self.conversation.pending_marker = marker;
|
||||
self.conversation.flush_pending();
|
||||
},
|
||||
PaneTarget::Tools =>
|
||||
self.tools.push_line(text, Color::Yellow),
|
||||
PaneTarget::ToolResult => {
|
||||
for line in text.lines().take(20) {
|
||||
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.last_entries.push(node.clone());
|
||||
}
|
||||
|
||||
self.last_generation = generation;
|
||||
self.last_entries.push(node.clone());
|
||||
}
|
||||
|
||||
self.last_generation = generation;
|
||||
|
||||
// Display pending input (queued in Mind, not yet accepted)
|
||||
let mind = self.shared_mind.lock().unwrap();
|
||||
for input in &mind.input {
|
||||
|
|
@ -537,7 +537,7 @@ impl InteractScreen {
|
|||
if let Some(cmd) = dispatch_command(input) {
|
||||
(cmd.handler)(self, &input[cmd.name.len()..].trim_start());
|
||||
} else {
|
||||
if let Ok(mut ag) = self.agent.try_lock() {
|
||||
if let Ok(mut ag) = self.agent.state.try_lock() {
|
||||
ag.notify(format!("unknown: {}", input.split_whitespace().next().unwrap_or(input)));
|
||||
}
|
||||
}
|
||||
|
|
@ -833,15 +833,17 @@ impl ScreenView for InteractScreen {
|
|||
self.sync_from_agent();
|
||||
|
||||
// Read status from agent + mind state
|
||||
if let Ok(mut agent) = self.agent.try_lock() {
|
||||
agent.expire_activities();
|
||||
app.status.prompt_tokens = agent.last_prompt_tokens();
|
||||
app.status.model = agent.model().to_string();
|
||||
app.status.context_budget = format!("{} tokens", agent.context.tokens());
|
||||
app.activity = agent.activities.last()
|
||||
if let Ok(mut st) = self.agent.state.try_lock() {
|
||||
st.expire_activities();
|
||||
app.status.prompt_tokens = st.last_prompt_tokens;
|
||||
app.status.model = self.agent.model().to_string();
|
||||
app.activity = st.activities.last()
|
||||
.map(|a| a.label.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if let Ok(ctx) = self.agent.context.try_lock() {
|
||||
app.status.context_budget = format!("{} tokens", ctx.tokens());
|
||||
}
|
||||
{
|
||||
let mind = self.shared_mind.lock().unwrap();
|
||||
app.status.dmn_state = mind.dmn.label().to_string();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue