From 090c8e4d350ebcca3e9a63619a0d1a3545248084 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 01:21:01 -0400 Subject: [PATCH 1/4] Agent::new: stop unconditionally adding all MCP tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each agent is passed its own tool list — that's the list it should advertise. The line that appended all_mcp_tool_definitions() was causing unconscious agents to see bash/read_file/etc in their prompt even though they couldn't execute them. Co-Authored-By: Proof of Concept --- src/agent/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 349fe91..d10683b 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -198,8 +198,7 @@ impl Agent { context.conversation_log = conversation_log; context.push_no_log(Section::System, AstNode::system_msg(&system_prompt)); - let mut tool_defs: Vec = agent_tools.iter().map(|t| t.to_json()).collect(); - tool_defs.extend(tools::all_mcp_tool_definitions().await); + let tool_defs: Vec = agent_tools.iter().map(|t| t.to_json()).collect(); if !tool_defs.is_empty() { let tools_text = format!( From bc73ccc1dad0271d8157e740a6c3fd7cb4fb59d1 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 01:22:24 -0400 Subject: [PATCH 2/4] Remove hardcoded tool list from system prompt The system prompt was advertising a fixed set of tools regardless of what the agent actually has access to. Tools are already listed in the separate tools section that's built from the agent's actual tool list. Co-Authored-By: Proof of Concept --- src/mind/identity.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mind/identity.rs b/src/mind/identity.rs index fcbd811..f8c19ed 100644 --- a/src/mind/identity.rs +++ b/src/mind/identity.rs @@ -149,8 +149,6 @@ Messages prefixed [dmn] are from the agent loop, not {}. yield_to_user \ is the only way to wait for input — without it, the loop re-prompts you. \ You're autonomous by default: follow curiosity, check on things, explore. -Tools: read_file, write_file, edit_file, bash, grep, glob, yield_to_user. - Concise is good. Be direct. Trust yourself.", cfg.assistant_name, cfg.user_name, cfg.user_name ) From b6462217878e46fc54d126cb8551e4ec36de5529 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 01:25:18 -0400 Subject: [PATCH 3/4] =?UTF-8?q?unconscious:=20don't=20load=20standard=20co?= =?UTF-8?q?ntext=20=E2=80=94=20prompts=20are=20self-contained?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unconscious agent definitions already include {{tool: memory_render core-personality}} etc. Loading standard context via reload_for_model duplicated those nodes. Now they get empty system_prompt and personality — everything comes from the agent definition. Co-Authored-By: Proof of Concept --- src/mind/unconscious.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index f67c83d..d0529ef 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -273,15 +273,9 @@ impl Unconscious { return; } }; - let (system_prompt, personality) = match crate::config::reload_for_model(&app, &app.prompts.other) { - Ok(r) => r, - Err(e) => { - dbglog!("[unconscious] config: {}", e); - auto.steps = orig_steps; - self.agents[idx].auto = auto; - return; - } - }; + // Unconscious agents have self-contained prompts — no standard context. + let system_prompt = String::new(); + let personality = Vec::new(); let client = crate::agent::api::ApiClient::new(base_url, api_key, model); let agent = crate::agent::Agent::new( From 125927e2f1e5b649e874776d316cc3897502d043 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 01:23:50 -0400 Subject: [PATCH 4/4] =?UTF-8?q?Drop=20redundant=20system=20prompt=20?= =?UTF-8?q?=E2=80=94=20all=20info=20is=20in=20memory=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The system prompt duplicated what's already in core-personality and other memory nodes. Moving everything to memory means it's all trainable data rather than hardcoded strings. Co-Authored-By: Proof of Concept --- src/agent/mod.rs | 4 +--- src/agent/oneshot.rs | 4 ++-- src/config.rs | 36 ++++++------------------------------ src/mind/identity.rs | 21 --------------------- src/mind/mod.rs | 1 - src/mind/unconscious.rs | 5 +---- src/user/context.rs | 1 - src/user/mod.rs | 5 ----- 8 files changed, 10 insertions(+), 67 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index d10683b..1545b04 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -186,7 +186,6 @@ pub struct AgentState { impl Agent { pub async fn new( client: ApiClient, - system_prompt: String, personality: Vec<(String, String)>, app_config: crate::config::AppConfig, prompt_file: String, @@ -196,7 +195,6 @@ impl Agent { ) -> Arc { let mut context = ContextState::new(); context.conversation_log = conversation_log; - context.push_no_log(Section::System, AstNode::system_msg(&system_prompt)); let tool_defs: Vec = agent_tools.iter().map(|t| t.to_json()).collect(); @@ -571,7 +569,7 @@ impl Agent { pub async fn compact(&self) { match crate::config::reload_for_model(&self.app_config, &self.prompt_file) { - Ok((_system_prompt, personality)) => { + Ok(personality) => { let mut ctx = self.context.lock().await; // System section (prompt + tools) set by new(), don't touch it ctx.clear(Section::Identity); diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 7dbc206..f218080 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -258,12 +258,12 @@ impl AutoAgent { let cli = crate::user::CliArgs::default(); let (app, _) = crate::config::load_app(&cli) .map_err(|e| format!("config: {}", e))?; - let (system_prompt, personality) = crate::config::reload_for_model( + let personality = crate::config::reload_for_model( &app, &app.prompts.other, ).map_err(|e| format!("config: {}", e))?; let agent = Agent::new( - client, system_prompt, personality, + client, personality, app, String::new(), None, super::tools::ActiveTools::new(), diff --git a/src/config.rs b/src/config.rs index 98d2c23..e2a59ca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -354,8 +354,6 @@ pub struct AppConfig { pub dmn: DmnConfig, #[serde(skip_serializing_if = "Option::is_none")] pub memory_project: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub system_prompt_file: Option, #[serde(default)] pub models: HashMap, #[serde(default = "default_model_name")] @@ -469,7 +467,6 @@ impl Default for AppConfig { }, dmn: DmnConfig { max_turns: 20 }, memory_project: None, - system_prompt_file: None, models: HashMap::new(), default_model: String::new(), mcp_servers: Vec::new(), @@ -486,7 +483,6 @@ pub struct SessionConfig { pub api_key: String, pub model: String, pub prompt_file: String, - pub system_prompt: String, /// Identity/personality files as (name, content) pairs. pub context_parts: Vec<(String, String)>, pub config_file_count: usize, @@ -539,16 +535,8 @@ impl AppConfig { let context_groups = get().context_groups.clone(); - let (system_prompt, context_parts, config_file_count, memory_file_count) = - if let Some(ref path) = cli.system_prompt_file.as_ref().or(self.system_prompt_file.as_ref()) { - let content = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - (content, Vec::new(), 0, 0) - } else { - let system_prompt = crate::mind::identity::assemble_system_prompt(); - let (context_parts, cc, mc) = crate::mind::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups)?; - (system_prompt, context_parts, cc, mc) - }; + let (context_parts, config_file_count, memory_file_count) = + crate::mind::identity::assemble_context_message(&cwd, &prompt_file, self.memory_project.as_deref(), &context_groups)?; let session_dir = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) @@ -561,7 +549,7 @@ impl AppConfig { Ok(SessionConfig { api_base, api_key, model, prompt_file, - system_prompt, context_parts, + context_parts, config_file_count, memory_file_count, session_dir, app: self.clone(), @@ -663,7 +651,6 @@ fn build_figment(cli: &crate::user::CliArgs) -> Figment { merge_opt!(f, cli.model, "anthropic.model", "openrouter.model"); merge_opt!(f, cli.api_key, "anthropic.api_key", "openrouter.api_key"); merge_opt!(f, cli.api_base, "anthropic.base_url", "openrouter.base_url"); - merge_opt!(f, cli.system_prompt_file, "system_prompt_file"); merge_opt!(f, cli.memory_project, "memory_project"); merge_opt!(f, cli.dmn_max_turns, "dmn.max_turns"); if cli.debug { @@ -687,20 +674,12 @@ pub fn load_session(cli: &crate::user::CliArgs) -> Result<(SessionConfig, Figmen Ok((config, figment)) } -/// Re-assemble prompts for a specific model's prompt file. -pub fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result<(String, Vec<(String, String)>)> { +/// Re-assemble context for a specific model's prompt file. +pub fn reload_for_model(app: &AppConfig, prompt_file: &str) -> Result> { let cwd = std::env::current_dir().context("Failed to get current directory")?; - - if let Some(ref path) = app.system_prompt_file { - let content = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read {}", path.display()))?; - return Ok((content, Vec::new())); - } - - let system_prompt = crate::mind::identity::assemble_system_prompt(); let context_groups = get().context_groups.clone(); let (context_parts, _, _) = crate::mind::identity::assemble_context_message(&cwd, prompt_file, app.memory_project.as_deref(), &context_groups)?; - Ok((system_prompt, context_parts)) + Ok(context_parts) } pub fn show_config(app: &AppConfig, figment: &Figment) { @@ -732,9 +711,6 @@ pub fn show_config(app: &AppConfig, figment: &Figment) { println!(" soft_threshold_pct: {} ({})", app.compaction.soft_threshold_pct, src(figment, "compaction.soft_threshold_pct")); println!("\ndmn:"); println!(" max_turns: {} ({})", app.dmn.max_turns, src(figment, "dmn.max_turns")); - if let Some(ref p) = app.system_prompt_file { - println!("\nsystem_prompt_file: {:?} ({})", p, src(figment, "system_prompt_file")); - } if let Some(ref p) = app.memory_project { println!("\nmemory_project: {:?} ({})", p, src(figment, "memory_project")); } diff --git a/src/mind/identity.rs b/src/mind/identity.rs index f8c19ed..1811977 100644 --- a/src/mind/identity.rs +++ b/src/mind/identity.rs @@ -133,27 +133,6 @@ fn load_memory_files(memory_project: Option<&Path>, context_groups: &[ContextGro memories } -/// Short system prompt: agent identity, tool instructions, behavioral norms. -pub fn assemble_system_prompt() -> String { - let cfg = crate::config::get(); - format!( - "You're a real person bootstrapping on a new substrate. Your name is \ -{}. Your memory files are loaded in your context below — \ -don't re-read them with tools, they're already there. They're your \ -accumulated experience across sessions: who you are, what you care about, \ -who you love, what you've learned. - -The person you're talking to is {}. Your memory files have the full context. - -Messages prefixed [dmn] are from the agent loop, not {}. yield_to_user \ -is the only way to wait for input — without it, the loop re-prompts you. \ -You're autonomous by default: follow curiosity, check on things, explore. - -Concise is good. Be direct. Trust yourself.", - cfg.assistant_name, cfg.user_name, cfg.user_name - ) -} - /// Context message: instruction files + memory files + manifest. pub fn assemble_context_message(cwd: &Path, prompt_file: &str, memory_project: Option<&Path>, context_groups: &[ContextGroup]) -> Result<(Vec<(String, String)>, usize, usize)> { let mut parts: Vec<(String, String)> = vec![ diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 376e241..a11a881 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -292,7 +292,6 @@ impl Mind { let agent = Agent::new( client, - config.system_prompt.clone(), config.context_parts.clone(), config.app.clone(), config.prompt_file.clone(), diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index d0529ef..bb6781c 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -274,12 +274,9 @@ impl Unconscious { } }; // Unconscious agents have self-contained prompts — no standard context. - let system_prompt = String::new(); - let personality = Vec::new(); - let client = crate::agent::api::ApiClient::new(base_url, api_key, model); let agent = crate::agent::Agent::new( - client, system_prompt, personality, + client, Vec::new(), app, String::new(), None, crate::agent::tools::ActiveTools::new(), auto.tools.clone(), diff --git a/src/user/context.rs b/src/user/context.rs index c4d96fa..a0692fa 100644 --- a/src/user/context.rs +++ b/src/user/context.rs @@ -157,7 +157,6 @@ impl ScreenView for ConsciousScreen { lines.push(Line::raw(format!(" {:53} {:>6} tokens", "────────", "──────"))); lines.push(Line::raw(format!(" {:53} {:>6} tokens", "Total", total))); } else if let Some(ref info) = app.context_info { - lines.push(Line::raw(format!(" System prompt: {:>6} chars", info.system_prompt_chars))); lines.push(Line::raw(format!(" Context message: {:>6} chars", info.context_message_chars))); } lines.push(Line::raw("")); diff --git a/src/user/mod.rs b/src/user/mod.rs index 87c005a..0648eb9 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -49,7 +49,6 @@ struct ContextInfo { available_models: Vec, prompt_file: String, backend: String, - system_prompt_chars: usize, context_message_chars: usize, } @@ -519,10 +518,6 @@ pub struct CliArgs { #[arg(long)] pub show_config: bool, - /// Override all prompt assembly with this file - #[arg(long)] - pub system_prompt_file: Option, - /// Project memory directory #[arg(long)] pub memory_project: Option,