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
|
|
@ -24,7 +24,6 @@ use anyhow::Result;
|
||||||
|
|
||||||
use api::ApiClient;
|
use api::ApiClient;
|
||||||
use context::{AstNode, NodeBody, ContextState, Section, Ast, PendingToolCall, ResponseParser, Role};
|
use context::{AstNode, NodeBody, ContextState, Section, Ast, PendingToolCall, ResponseParser, Role};
|
||||||
use tools::summarize_args;
|
|
||||||
|
|
||||||
use crate::mind::log::ConversationLog;
|
use crate::mind::log::ConversationLog;
|
||||||
|
|
||||||
|
|
@ -416,7 +415,7 @@ impl Agent {
|
||||||
agent.push_node(AstNode::user_msg(
|
agent.push_node(AstNode::user_msg(
|
||||||
"[system] Your previous response was empty. \
|
"[system] Your previous response was empty. \
|
||||||
Please respond with text or use a tool."
|
Please respond with text or use a tool."
|
||||||
));
|
)).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use super::api::{ApiClient, Usage};
|
use super::api::ApiClient;
|
||||||
use super::context::{AstNode, Role};
|
use super::context::AstNode;
|
||||||
use super::tools::{self as agent_tools};
|
use super::tools::{self as agent_tools};
|
||||||
use super::Agent;
|
use super::Agent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -460,8 +460,6 @@ impl Subconscious {
|
||||||
|| outputs.contains_key("reflection")
|
|| outputs.contains_key("reflection")
|
||||||
|| outputs.contains_key("thalamus");
|
|| outputs.contains_key("thalamus");
|
||||||
if has_outputs {
|
if has_outputs {
|
||||||
let mut ag = agent.lock().await;
|
|
||||||
|
|
||||||
if let Some(surface_str) = outputs.get("surface") {
|
if let Some(surface_str) = outputs.get("surface") {
|
||||||
let store = crate::store::Store::cached().await.ok();
|
let store = crate::store::Store::cached().await.ok();
|
||||||
let store_guard = match &store {
|
let store_guard = match &store {
|
||||||
|
|
@ -472,30 +470,30 @@ impl Subconscious {
|
||||||
let rendered = store_guard.as_ref()
|
let rendered = store_guard.as_ref()
|
||||||
.and_then(|s| crate::cli::node::render_node(s, key));
|
.and_then(|s| crate::cli::node::render_node(s, key));
|
||||||
if let Some(rendered) = rendered {
|
if let Some(rendered) = rendered {
|
||||||
ag.push_node(AstNode::memory(
|
agent.push_node(AstNode::memory(
|
||||||
key,
|
key,
|
||||||
format!("--- {} (surfaced) ---\n{}", key, rendered),
|
format!("--- {} (surfaced) ---\n{}", key, rendered),
|
||||||
));
|
)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(reflection) = outputs.get("reflection") {
|
if let Some(reflection) = outputs.get("reflection") {
|
||||||
if !reflection.trim().is_empty() {
|
if !reflection.trim().is_empty() {
|
||||||
ag.push_node(AstNode::dmn(format!(
|
agent.push_node(AstNode::dmn(format!(
|
||||||
"--- subconscious reflection ---\n{}",
|
"--- subconscious reflection ---\n{}",
|
||||||
reflection.trim(),
|
reflection.trim(),
|
||||||
)));
|
))).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(nudge) = outputs.get("thalamus") {
|
if let Some(nudge) = outputs.get("thalamus") {
|
||||||
let nudge = nudge.trim();
|
let nudge = nudge.trim();
|
||||||
if !nudge.is_empty() && nudge != "ok" {
|
if !nudge.is_empty() && nudge != "ok" {
|
||||||
ag.push_node(AstNode::dmn(format!(
|
agent.push_node(AstNode::dmn(format!(
|
||||||
"--- thalamus ---\n{}",
|
"--- thalamus ---\n{}",
|
||||||
nudge,
|
nudge,
|
||||||
)));
|
))).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -513,13 +511,13 @@ impl Subconscious {
|
||||||
/// Trigger subconscious agents that are due to run.
|
/// Trigger subconscious agents that are due to run.
|
||||||
pub async fn trigger(&mut self, agent: &Arc<Agent>) {
|
pub async fn trigger(&mut self, agent: &Arc<Agent>) {
|
||||||
let (conversation_bytes, memory_keys) = {
|
let (conversation_bytes, memory_keys) = {
|
||||||
let ag = agent.lock().await;
|
let ctx = agent.context.lock().await;
|
||||||
let bytes = ag.context.conversation().iter()
|
let bytes = ctx.conversation().iter()
|
||||||
.filter(|node| !matches!(node.leaf().map(|l| l.body()),
|
.filter(|node| !matches!(node.leaf().map(|l| l.body()),
|
||||||
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
|
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
|
||||||
.map(|node| node.render().len() as u64)
|
.map(|node| node.render().len() as u64)
|
||||||
.sum::<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()) {
|
if let Some(NodeBody::Memory { key, .. }) = node.leaf().map(|l| l.body()) {
|
||||||
Some(key.clone())
|
Some(key.clone())
|
||||||
} else { None }
|
} else { None }
|
||||||
|
|
@ -541,23 +539,21 @@ impl Subconscious {
|
||||||
|
|
||||||
if to_run.is_empty() { return; }
|
if to_run.is_empty() { return; }
|
||||||
|
|
||||||
let conscious = agent.lock().await;
|
|
||||||
for (idx, mut auto) in to_run {
|
for (idx, mut auto) in to_run {
|
||||||
dbglog!("[subconscious] triggering {}", auto.name);
|
dbglog!("[subconscious] triggering {}", auto.name);
|
||||||
|
|
||||||
let mut forked = conscious.fork(auto.tools.clone());
|
let forked = agent.fork(auto.tools.clone()).await;
|
||||||
forked.provenance = format!("agent:{}", auto.name);
|
forked.state.lock().await.provenance = format!("agent:{}", auto.name);
|
||||||
let fork_point = forked.context.conversation().len();
|
let fork_point = forked.context.lock().await.conversation().len();
|
||||||
let shared_forked = Arc::new(tokio::sync::Mutex::new(forked));
|
|
||||||
|
|
||||||
self.agents[idx].forked_agent = Some(shared_forked.clone());
|
self.agents[idx].forked_agent = Some(forked.clone());
|
||||||
self.agents[idx].fork_point = fork_point;
|
self.agents[idx].fork_point = fork_point;
|
||||||
|
|
||||||
let keys = memory_keys.clone();
|
let keys = memory_keys.clone();
|
||||||
let st = self.state.clone();
|
let st = self.state.clone();
|
||||||
|
|
||||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
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)
|
(auto, result)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ pub struct Mind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mind {
|
impl Mind {
|
||||||
pub fn new(
|
pub async fn new(
|
||||||
config: SessionConfig,
|
config: SessionConfig,
|
||||||
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -271,7 +271,7 @@ impl Mind {
|
||||||
config.session_dir.join("conversation.jsonl"),
|
config.session_dir.join("conversation.jsonl"),
|
||||||
).ok();
|
).ok();
|
||||||
|
|
||||||
let ag = Agent::new(
|
let agent = Agent::new(
|
||||||
client,
|
client,
|
||||||
config.system_prompt.clone(),
|
config.system_prompt.clone(),
|
||||||
config.context_parts.clone(),
|
config.context_parts.clone(),
|
||||||
|
|
@ -279,8 +279,7 @@ impl Mind {
|
||||||
config.prompt_file.clone(),
|
config.prompt_file.clone(),
|
||||||
conversation_log,
|
conversation_log,
|
||||||
shared_active_tools,
|
shared_active_tools,
|
||||||
);
|
).await;
|
||||||
let agent = Arc::new(tokio::sync::Mutex::new(ag));
|
|
||||||
|
|
||||||
let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns)));
|
let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns)));
|
||||||
let (turn_watch, _) = tokio::sync::watch::channel(false);
|
let (turn_watch, _) = tokio::sync::watch::channel(false);
|
||||||
|
|
@ -314,15 +313,13 @@ impl Mind {
|
||||||
|
|
||||||
pub async fn init(&self) {
|
pub async fn init(&self) {
|
||||||
// Restore conversation
|
// Restore conversation
|
||||||
let mut ag = self.agent.lock().await;
|
self.agent.restore_from_log().await;
|
||||||
ag.restore_from_log();
|
|
||||||
|
|
||||||
// Restore persisted memory scores
|
// Restore persisted memory scores
|
||||||
let scores_path = self.config.session_dir.join("memory-scores.json");
|
let scores_path = self.config.session_dir.join("memory-scores.json");
|
||||||
load_memory_scores(&mut ag.context, &scores_path);
|
load_memory_scores(&mut *self.agent.context.lock().await, &scores_path);
|
||||||
|
|
||||||
ag.changed.notify_one();
|
self.agent.state.lock().await.changed.notify_one();
|
||||||
drop(ag);
|
|
||||||
|
|
||||||
// Load persistent subconscious state
|
// Load persistent subconscious state
|
||||||
let state_path = self.config.session_dir.join("subconscious-state.json");
|
let state_path = self.config.session_dir.join("subconscious-state.json");
|
||||||
|
|
@ -340,10 +337,9 @@ impl Mind {
|
||||||
MindCommand::None => {}
|
MindCommand::None => {}
|
||||||
MindCommand::Compact => {
|
MindCommand::Compact => {
|
||||||
let threshold = compaction_threshold(&self.config.app) as usize;
|
let threshold = compaction_threshold(&self.config.app) as usize;
|
||||||
let mut ag = self.agent.lock().await;
|
if self.agent.context.lock().await.tokens() > threshold {
|
||||||
if ag.context.tokens() > threshold {
|
self.agent.compact().await;
|
||||||
ag.compact();
|
self.agent.state.lock().await.notify("compacted");
|
||||||
ag.notify("compacted");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MindCommand::Score => {
|
MindCommand::Score => {
|
||||||
|
|
@ -356,10 +352,10 @@ impl Mind {
|
||||||
}
|
}
|
||||||
MindCommand::Interrupt => {
|
MindCommand::Interrupt => {
|
||||||
self.shared.lock().unwrap().interrupt();
|
self.shared.lock().unwrap().interrupt();
|
||||||
let ag = self.agent.lock().await;
|
let active_tools = self.agent.state.lock().await.active_tools.clone();
|
||||||
let mut tools = ag.active_tools.lock().unwrap();
|
let mut tools = active_tools.lock().unwrap();
|
||||||
for entry in tools.drain(..) { entry.handle.abort(); }
|
for entry in tools.drain(..) { entry.handle.abort(); }
|
||||||
drop(tools); drop(ag);
|
drop(tools);
|
||||||
if let Some(h) = self.shared.lock().unwrap().turn_handle.take() { h.abort(); }
|
if let Some(h) = self.shared.lock().unwrap().turn_handle.take() { h.abort(); }
|
||||||
self.shared.lock().unwrap().turn_active = false;
|
self.shared.lock().unwrap().turn_active = false;
|
||||||
let _ = self.turn_watch.send(false);
|
let _ = self.turn_watch.send(false);
|
||||||
|
|
@ -373,14 +369,17 @@ impl Mind {
|
||||||
let new_log = log::ConversationLog::new(
|
let new_log = log::ConversationLog::new(
|
||||||
self.config.session_dir.join("conversation.jsonl"),
|
self.config.session_dir.join("conversation.jsonl"),
|
||||||
).ok();
|
).ok();
|
||||||
let mut ag = self.agent.lock().await;
|
{
|
||||||
let shared_tools = ag.active_tools.clone();
|
let mut ctx = self.agent.context.lock().await;
|
||||||
*ag = Agent::new(
|
ctx.clear(Section::Conversation);
|
||||||
ApiClient::new(&self.config.api_base, &self.config.api_key, &self.config.model),
|
}
|
||||||
self.config.system_prompt.clone(), self.config.context_parts.clone(),
|
{
|
||||||
self.config.app.clone(), self.config.prompt_file.clone(),
|
let mut st = self.agent.state.lock().await;
|
||||||
new_log, shared_tools,
|
st.conversation_log = new_log;
|
||||||
);
|
st.generation += 1;
|
||||||
|
st.last_prompt_tokens = 0;
|
||||||
|
}
|
||||||
|
self.agent.compact().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -395,10 +394,12 @@ impl Mind {
|
||||||
let response_window = cfg.scoring_response_window;
|
let response_window = cfg.scoring_response_window;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let (context, client) = {
|
let (context, client) = {
|
||||||
let mut ag = agent.lock().await;
|
let mut st = agent.state.lock().await;
|
||||||
if ag.memory_scoring_in_flight { return; }
|
if st.memory_scoring_in_flight { return; }
|
||||||
ag.memory_scoring_in_flight = true;
|
st.memory_scoring_in_flight = true;
|
||||||
(ag.context.clone(), ag.client_clone())
|
drop(st);
|
||||||
|
let ctx = agent.context.lock().await.clone();
|
||||||
|
(ctx, agent.client.clone())
|
||||||
};
|
};
|
||||||
let _result = learn::score_memories_incremental(
|
let _result = learn::score_memories_incremental(
|
||||||
&context, max_age as i64, response_window, &client, &agent,
|
&context, max_age as i64, response_window, &client, &agent,
|
||||||
|
|
@ -407,27 +408,27 @@ impl Mind {
|
||||||
let path = scores_path.clone();
|
let path = scores_path.clone();
|
||||||
async move {
|
async move {
|
||||||
let scores_snapshot = {
|
let scores_snapshot = {
|
||||||
let mut ag = agent.lock().await;
|
let mut ctx = agent.context.lock().await;
|
||||||
for i in 0..ag.context.conversation().len() {
|
for i in 0..ctx.conversation().len() {
|
||||||
if let AstNode::Leaf(leaf) = &ag.context.conversation()[i] {
|
if let AstNode::Leaf(leaf) = &ctx.conversation()[i] {
|
||||||
if let NodeBody::Memory { key: k, .. } = leaf.body() {
|
if let NodeBody::Memory { key: k, .. } = leaf.body() {
|
||||||
if *k == key {
|
if *k == key {
|
||||||
ag.context.set_score(Section::Conversation, i, Some(score));
|
ctx.set_score(Section::Conversation, i, Some(score));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ag.changed.notify_one();
|
let snapshot = collect_memory_scores(&ctx);
|
||||||
collect_memory_scores(&ag.context)
|
drop(ctx);
|
||||||
|
agent.state.lock().await.changed.notify_one();
|
||||||
|
snapshot
|
||||||
};
|
};
|
||||||
// Write to disk after releasing the lock
|
|
||||||
save_memory_scores(&scores_snapshot, &path);
|
save_memory_scores(&scores_snapshot, &path);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
).await;
|
).await;
|
||||||
{
|
{
|
||||||
let mut ag = agent.lock().await;
|
agent.state.lock().await.memory_scoring_in_flight = false;
|
||||||
ag.memory_scoring_in_flight = false;
|
|
||||||
}
|
}
|
||||||
let _ = bg_tx.send(BgEvent::ScoringDone);
|
let _ = bg_tx.send(BgEvent::ScoringDone);
|
||||||
});
|
});
|
||||||
|
|
@ -435,21 +436,20 @@ impl Mind {
|
||||||
|
|
||||||
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
async fn start_turn(&self, text: &str, target: StreamTarget) {
|
||||||
{
|
{
|
||||||
let mut ag = self.agent.lock().await;
|
|
||||||
match target {
|
match target {
|
||||||
StreamTarget::Conversation => {
|
StreamTarget::Conversation => {
|
||||||
ag.push_node(AstNode::user_msg(text));
|
self.agent.push_node(AstNode::user_msg(text)).await;
|
||||||
}
|
}
|
||||||
StreamTarget::Autonomous => {
|
StreamTarget::Autonomous => {
|
||||||
ag.push_node(AstNode::dmn(text));
|
self.agent.push_node(AstNode::dmn(text)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact if over budget before sending
|
// Compact if over budget before sending
|
||||||
let threshold = compaction_threshold(&self.config.app) as usize;
|
let threshold = compaction_threshold(&self.config.app) as usize;
|
||||||
if ag.context.tokens() > threshold {
|
if self.agent.context.lock().await.tokens() > threshold {
|
||||||
ag.compact();
|
self.agent.compact().await;
|
||||||
ag.notify("compacted");
|
self.agent.state.lock().await.notify("compacted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.shared.lock().unwrap().turn_active = true;
|
self.shared.lock().unwrap().turn_active = true;
|
||||||
|
|
|
||||||
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); } },
|
handler: |s, _| { let _ = s.mind_tx.send(MindCommand::NewSession); } },
|
||||||
SlashCommand { name: "/save", help: "Save session to disk",
|
SlashCommand { name: "/save", help: "Save session to disk",
|
||||||
handler: |s, _| {
|
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>)",
|
SlashCommand { name: "/model", help: "Show/switch model (/model <name>)",
|
||||||
handler: |s, arg| {
|
handler: |s, arg| {
|
||||||
if arg.is_empty() {
|
if arg.is_empty() {
|
||||||
if let Ok(mut ag) = s.agent.try_lock() {
|
if let Ok(mut ag) = s.agent.state.try_lock() {
|
||||||
let names = ag.app_config.model_names();
|
let names = s.agent.app_config.model_names();
|
||||||
let label = if names.is_empty() {
|
let label = if names.is_empty() {
|
||||||
format!("model: {}", ag.model())
|
format!("model: {}", s.agent.model())
|
||||||
} else {
|
} else {
|
||||||
format!("model: {} ({})", ag.model(), names.join(", "))
|
format!("model: {} ({})", s.agent.model(), names.join(", "))
|
||||||
};
|
};
|
||||||
ag.notify(label);
|
ag.notify(label);
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +61,7 @@ fn commands() -> Vec<SlashCommand> { vec![
|
||||||
SlashCommand { name: "/dmn", help: "Show DMN state",
|
SlashCommand { name: "/dmn", help: "Show DMN state",
|
||||||
handler: |s, _| {
|
handler: |s, _| {
|
||||||
let st = s.shared_mind.lock().unwrap();
|
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));
|
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();
|
let mut st = s.shared_mind.lock().unwrap();
|
||||||
st.dmn = crate::mind::dmn::State::Resting { since: std::time::Instant::now() };
|
st.dmn = crate::mind::dmn::State::Resting { since: std::time::Instant::now() };
|
||||||
st.dmn_turns = 0;
|
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",
|
SlashCommand { name: "/wake", help: "Wake DMN to foraging",
|
||||||
handler: |s, _| {
|
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); }
|
if matches!(st.dmn, crate::mind::dmn::State::Off) { crate::mind::dmn::set_off(false); }
|
||||||
st.dmn = crate::mind::dmn::State::Foraging;
|
st.dmn = crate::mind::dmn::State::Foraging;
|
||||||
st.dmn_turns = 0;
|
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)",
|
SlashCommand { name: "/pause", help: "Full stop — no autonomous ticks (Ctrl+P)",
|
||||||
handler: |s, _| {
|
handler: |s, _| {
|
||||||
let mut st = s.shared_mind.lock().unwrap();
|
let mut st = s.shared_mind.lock().unwrap();
|
||||||
st.dmn = crate::mind::dmn::State::Paused;
|
st.dmn = crate::mind::dmn::State::Paused;
|
||||||
st.dmn_turns = 0;
|
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",
|
SlashCommand { name: "/help", help: "Show this help",
|
||||||
handler: |s, _| { notify_help(&s.agent); } },
|
handler: |s, _| { notify_help(&s.agent); } },
|
||||||
|
|
@ -101,36 +101,27 @@ pub async fn cmd_switch_model(
|
||||||
agent: &std::sync::Arc<crate::agent::Agent>,
|
agent: &std::sync::Arc<crate::agent::Agent>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) {
|
) {
|
||||||
let resolved = {
|
let resolved = match agent.app_config.resolve_model(name) {
|
||||||
let ag = agent.lock().await;
|
Ok(r) => r,
|
||||||
match ag.app_config.resolve_model(name) {
|
Err(e) => {
|
||||||
Ok(r) => r,
|
agent.state.lock().await.notify(format!("model error: {}", e));
|
||||||
Err(e) => {
|
return;
|
||||||
agent.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,
|
&resolved.api_base, &resolved.api_key, &resolved.model_id,
|
||||||
);
|
);
|
||||||
let prompt_changed = {
|
let prompt_changed = resolved.prompt_file != agent.prompt_file;
|
||||||
let ag = agent.lock().await;
|
|
||||||
resolved.prompt_file != ag.prompt_file
|
|
||||||
};
|
|
||||||
let mut ag = agent.lock().await;
|
|
||||||
ag.swap_client(new_client);
|
|
||||||
if prompt_changed {
|
if prompt_changed {
|
||||||
ag.prompt_file = resolved.prompt_file.clone();
|
agent.compact().await;
|
||||||
ag.compact();
|
agent.state.lock().await.notify(format!("switched to {} (recompacted)", resolved.model_id));
|
||||||
ag.notify(format!("switched to {} (recompacted)", resolved.model_id));
|
|
||||||
} else {
|
} 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>) {
|
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();
|
let mut help = String::new();
|
||||||
for cmd in &commands() {
|
for cmd in &commands() {
|
||||||
help.push_str(&format!("{:12} {}\n", cmd.name, cmd.help));
|
help.push_str(&format!("{:12} {}\n", cmd.name, cmd.help));
|
||||||
|
|
@ -471,48 +462,57 @@ impl InteractScreen {
|
||||||
}
|
}
|
||||||
self.pending_display_count = 0;
|
self.pending_display_count = 0;
|
||||||
|
|
||||||
if let Ok(agent) = self.agent.try_lock() {
|
let (generation, entries) = {
|
||||||
let generation = agent.generation;
|
let st = match self.agent.state.try_lock() {
|
||||||
let entries = agent.conversation();
|
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() {
|
if generation != self.last_generation || entries.len() < self.last_entries.len() {
|
||||||
self.conversation = PaneState::new(true);
|
self.conversation = PaneState::new(true);
|
||||||
self.autonomous = PaneState::new(true);
|
self.autonomous = PaneState::new(true);
|
||||||
self.tools = PaneState::new(false);
|
self.tools = PaneState::new(false);
|
||||||
self.last_entries.clear();
|
self.last_entries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = self.last_entries.len();
|
let start = self.last_entries.len();
|
||||||
for node in entries.iter().skip(start) {
|
for node in entries.iter().skip(start) {
|
||||||
for (target, text, marker) in Self::route_node(node) {
|
for (target, text, marker) in Self::route_node(node) {
|
||||||
match target {
|
match target {
|
||||||
PaneTarget::Conversation => {
|
PaneTarget::Conversation => {
|
||||||
self.conversation.current_color = Color::Cyan;
|
self.conversation.current_color = Color::Cyan;
|
||||||
self.conversation.append_text(&text);
|
self.conversation.append_text(&text);
|
||||||
self.conversation.pending_marker = marker;
|
self.conversation.pending_marker = marker;
|
||||||
self.conversation.flush_pending();
|
self.conversation.flush_pending();
|
||||||
},
|
},
|
||||||
PaneTarget::ConversationAssistant => {
|
PaneTarget::ConversationAssistant => {
|
||||||
self.conversation.current_color = Color::Reset;
|
self.conversation.current_color = Color::Reset;
|
||||||
self.conversation.append_text(&text);
|
self.conversation.append_text(&text);
|
||||||
self.conversation.pending_marker = marker;
|
self.conversation.pending_marker = marker;
|
||||||
self.conversation.flush_pending();
|
self.conversation.flush_pending();
|
||||||
},
|
},
|
||||||
PaneTarget::Tools =>
|
PaneTarget::Tools =>
|
||||||
self.tools.push_line(text, Color::Yellow),
|
self.tools.push_line(text, Color::Yellow),
|
||||||
PaneTarget::ToolResult => {
|
PaneTarget::ToolResult => {
|
||||||
for line in text.lines().take(20) {
|
for line in text.lines().take(20) {
|
||||||
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.last_entries.push(node.clone());
|
|
||||||
}
|
}
|
||||||
|
self.last_entries.push(node.clone());
|
||||||
self.last_generation = generation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.last_generation = generation;
|
||||||
|
|
||||||
// Display pending input (queued in Mind, not yet accepted)
|
// Display pending input (queued in Mind, not yet accepted)
|
||||||
let mind = self.shared_mind.lock().unwrap();
|
let mind = self.shared_mind.lock().unwrap();
|
||||||
for input in &mind.input {
|
for input in &mind.input {
|
||||||
|
|
@ -537,7 +537,7 @@ impl InteractScreen {
|
||||||
if let Some(cmd) = dispatch_command(input) {
|
if let Some(cmd) = dispatch_command(input) {
|
||||||
(cmd.handler)(self, &input[cmd.name.len()..].trim_start());
|
(cmd.handler)(self, &input[cmd.name.len()..].trim_start());
|
||||||
} else {
|
} 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)));
|
ag.notify(format!("unknown: {}", input.split_whitespace().next().unwrap_or(input)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -833,15 +833,17 @@ impl ScreenView for InteractScreen {
|
||||||
self.sync_from_agent();
|
self.sync_from_agent();
|
||||||
|
|
||||||
// Read status from agent + mind state
|
// Read status from agent + mind state
|
||||||
if let Ok(mut agent) = self.agent.try_lock() {
|
if let Ok(mut st) = self.agent.state.try_lock() {
|
||||||
agent.expire_activities();
|
st.expire_activities();
|
||||||
app.status.prompt_tokens = agent.last_prompt_tokens();
|
app.status.prompt_tokens = st.last_prompt_tokens;
|
||||||
app.status.model = agent.model().to_string();
|
app.status.model = self.agent.model().to_string();
|
||||||
app.status.context_budget = format!("{} tokens", agent.context.tokens());
|
app.activity = st.activities.last()
|
||||||
app.activity = agent.activities.last()
|
|
||||||
.map(|a| a.label.clone())
|
.map(|a| a.label.clone())
|
||||||
.unwrap_or_default();
|
.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();
|
let mind = self.shared_mind.lock().unwrap();
|
||||||
app.status.dmn_state = mind.dmn.label().to_string();
|
app.status.dmn_state = mind.dmn.label().to_string();
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,22 @@ impl ConsciousScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_context_views(&self) -> Vec<SectionView> {
|
fn read_context_views(&self) -> Vec<SectionView> {
|
||||||
let ag = match self.agent.try_lock() {
|
let ctx = match self.agent.context.try_lock() {
|
||||||
Ok(ag) => ag,
|
Ok(ctx) => ctx,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut views: Vec<SectionView> = Vec::new();
|
let mut views: Vec<SectionView> = Vec::new();
|
||||||
|
|
||||||
views.push(section_to_view("System", ag.context.system()));
|
views.push(section_to_view("System", ctx.system()));
|
||||||
views.push(section_to_view("Identity", ag.context.identity()));
|
views.push(section_to_view("Identity", ctx.identity()));
|
||||||
views.push(section_to_view("Journal", ag.context.journal()));
|
views.push(section_to_view("Journal", ctx.journal()));
|
||||||
|
|
||||||
// Memory nodes extracted from conversation
|
// Memory nodes extracted from conversation
|
||||||
let mut mem_children: Vec<SectionView> = Vec::new();
|
let mut mem_children: Vec<SectionView> = Vec::new();
|
||||||
let mut scored = 0usize;
|
let mut scored = 0usize;
|
||||||
let mut unscored = 0usize;
|
let mut unscored = 0usize;
|
||||||
for node in ag.context.conversation() {
|
for node in ctx.conversation() {
|
||||||
if let AstNode::Leaf(leaf) = node {
|
if let AstNode::Leaf(leaf) = node {
|
||||||
if let NodeBody::Memory { key, score, text } = leaf.body() {
|
if let NodeBody::Memory { key, score, text } = leaf.body() {
|
||||||
let status = match score {
|
let status = match score {
|
||||||
|
|
@ -63,7 +63,7 @@ impl ConsciousScreen {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
views.push(section_to_view("Conversation", ag.context.conversation()));
|
views.push(section_to_view("Conversation", ctx.conversation()));
|
||||||
views
|
views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,9 +179,9 @@ async fn start(cli: crate::user::CliArgs) -> Result<()> {
|
||||||
let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1);
|
let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1);
|
||||||
let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
let mind = crate::mind::Mind::new(config, turn_tx);
|
let mind = crate::mind::Mind::new(config, turn_tx).await;
|
||||||
|
|
||||||
let shared_active_tools = mind.agent.lock().await.active_tools.clone();
|
let shared_active_tools = mind.agent.state.lock().await.active_tools.clone();
|
||||||
|
|
||||||
let mut result = Ok(());
|
let mut result = Ok(());
|
||||||
tokio_scoped::scope(|s| {
|
tokio_scoped::scope(|s| {
|
||||||
|
|
@ -203,7 +203,7 @@ async fn start(cli: crate::user::CliArgs) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hotkey_cycle_reasoning(mind: &crate::mind::Mind) {
|
fn hotkey_cycle_reasoning(mind: &crate::mind::Mind) {
|
||||||
if let Ok(mut ag) = mind.agent.try_lock() {
|
if let Ok(mut ag) = mind.agent.state.try_lock() {
|
||||||
let next = match ag.reasoning_effort.as_str() {
|
let next = match ag.reasoning_effort.as_str() {
|
||||||
"none" => "low",
|
"none" => "low",
|
||||||
"low" => "high",
|
"low" => "high",
|
||||||
|
|
@ -221,17 +221,17 @@ fn hotkey_cycle_reasoning(mind: &crate::mind::Mind) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hotkey_kill_processes(mind: &crate::mind::Mind) {
|
async fn hotkey_kill_processes(mind: &crate::mind::Mind) {
|
||||||
let mut ag = mind.agent.lock().await;
|
let mut st = mind.agent.state.lock().await;
|
||||||
let active_tools = ag.active_tools.clone();
|
let active_tools = st.active_tools.clone();
|
||||||
let mut tools = active_tools.lock().unwrap();
|
let mut tools = active_tools.lock().unwrap();
|
||||||
if tools.is_empty() {
|
if tools.is_empty() {
|
||||||
ag.notify("no running tools");
|
st.notify("no running tools");
|
||||||
} else {
|
} else {
|
||||||
let count = tools.len();
|
let count = tools.len();
|
||||||
for entry in tools.drain(..) {
|
for entry in tools.drain(..) {
|
||||||
entry.handle.abort();
|
entry.handle.abort();
|
||||||
}
|
}
|
||||||
ag.notify(format!("killed {} tools", count));
|
st.notify(format!("killed {} tools", count));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,7 +259,7 @@ fn hotkey_cycle_autonomy(mind: &crate::mind::Mind) {
|
||||||
};
|
};
|
||||||
s.dmn_turns = 0;
|
s.dmn_turns = 0;
|
||||||
drop(s);
|
drop(s);
|
||||||
if let Ok(mut ag) = mind.agent.try_lock() {
|
if let Ok(mut ag) = mind.agent.state.try_lock() {
|
||||||
ag.notify(format!("DMN → {}", label));
|
ag.notify(format!("DMN → {}", label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,13 +325,13 @@ async fn run(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let agent_changed = agent.lock().await.changed.clone();
|
let agent_changed = agent.state.lock().await.changed.clone();
|
||||||
let mut turn_watch = mind.turn_watch();
|
let mut turn_watch = mind.turn_watch();
|
||||||
let mut pending: Vec<ratatui::crossterm::event::Event> = Vec::new();
|
let mut pending: Vec<ratatui::crossterm::event::Event> = Vec::new();
|
||||||
|
|
||||||
terminal.hide_cursor()?;
|
terminal.hide_cursor()?;
|
||||||
|
|
||||||
if let Ok(mut ag) = agent.try_lock() { ag.notify("consciousness v0.3"); }
|
if let Ok(mut ag) = agent.state.try_lock() { ag.notify("consciousness v0.3"); }
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
{
|
{
|
||||||
|
|
@ -378,8 +378,8 @@ async fn run(
|
||||||
app.agent_state = mind.subconscious_snapshots().await;
|
app.agent_state = mind.subconscious_snapshots().await;
|
||||||
app.walked_count = mind.subconscious_walked().await.len();
|
app.walked_count = mind.subconscious_walked().await.len();
|
||||||
if !startup_done {
|
if !startup_done {
|
||||||
if let Ok(mut ag) = agent.try_lock() {
|
if let Ok(mut ag) = agent.state.try_lock() {
|
||||||
let model = ag.model().to_string();
|
let model = agent.model().to_string();
|
||||||
ag.notify(format!("model: {}", model));
|
ag.notify(format!("model: {}", model));
|
||||||
startup_done = true;
|
startup_done = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,9 @@ impl SubconsciousScreen {
|
||||||
None => return Vec::new(),
|
None => return Vec::new(),
|
||||||
};
|
};
|
||||||
snap.forked_agent.as_ref()
|
snap.forked_agent.as_ref()
|
||||||
.and_then(|agent| agent.try_lock().ok())
|
.and_then(|agent| agent.context.try_lock().ok())
|
||||||
.map(|ag| {
|
.map(|ctx| {
|
||||||
let conv = ag.context.conversation();
|
let conv = ctx.conversation();
|
||||||
let mut view = section_to_view("Conversation", conv);
|
let mut view = section_to_view("Conversation", conv);
|
||||||
let fork = snap.fork_point.min(view.children.len());
|
let fork = snap.fork_point.min(view.children.len());
|
||||||
view.children = view.children.split_off(fork);
|
view.children = view.children.split_off(fork);
|
||||||
|
|
@ -177,8 +177,8 @@ impl SubconsciousScreen {
|
||||||
.map(|s| format_age(s))
|
.map(|s| format_age(s))
|
||||||
.unwrap_or_else(|| "—".to_string());
|
.unwrap_or_else(|| "—".to_string());
|
||||||
let entries = snap.forked_agent.as_ref()
|
let entries = snap.forked_agent.as_ref()
|
||||||
.and_then(|a| a.try_lock().ok())
|
.and_then(|a| a.context.try_lock().ok())
|
||||||
.map(|ag| ag.context.conversation().len().saturating_sub(snap.fork_point))
|
.map(|ctx| ctx.conversation().len().saturating_sub(snap.fork_point))
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
ListItem::from(Line::from(vec![
|
ListItem::from(Line::from(vec![
|
||||||
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue