From 9206eb9d739e1d1f90bb9ad1310c1d52c528de00 Mon Sep 17 00:00:00 2001 From: zLeoAlex Date: Tue, 9 Jun 2026 02:36:32 +0800 Subject: [PATCH] refactor(prompts): decouple allow_shell from static system-prompt prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move allow_shell from message[0] (static system prompt) to the per-turn tag alongside mode and approval. This preserves the DeepSeek prefix cache across shell-access toggles and mode switches, which previously forced YOLO entry/exit to rebuild the entire prompt. Changes: - Delete remove_shell_tool_guidance and 3 other dead functions (~60 lines) - Remove allow_shell field from PromptSessionContext and StaticPromptCtx - Remove shell_tools_available dead parameter from compose functions - Add Shell Policy section to Runtime Policy Reference (static text) - Extend tag with allow_shell="true|false" attribute - Update tests: omits→always_keeps, 83/83 prompts tests pass - Drop dead compose_mode_prompt_with_approval_and_model Net: message[0] bytes are now stable regardless of shell-access state. Mode/approval/shell flags all use the same per-turn tag pattern. --- crates/tui/src/core/engine.rs | 23 +- crates/tui/src/prompts.rs | 440 ++++++++++------------------------ crates/tui/src/tui/ui.rs | 2 - 3 files changed, 140 insertions(+), 325 deletions(-) diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 86c146b4..4febdc56 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -681,7 +681,6 @@ impl Engine { goal_objective_for_prompt(config.goal_objective.as_deref(), &config.goal_state); let system_prompt = prompts::system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, &config.workspace, None, Some(&config.skills_dir), @@ -694,7 +693,6 @@ impl Engine { translation_enabled: config.translation_enabled, model_id: &config.model, show_thinking: config.show_thinking, - allow_shell: config.allow_shell, }, ); let stable_prompt = Some(system_prompt); @@ -1456,7 +1454,7 @@ impl Engine { Message { role: "user".to_string(), content: vec![ContentBlock::Text { - text: runtime_prompt_text(mode, approval_mode), + text: runtime_prompt_text(mode, approval_mode, self.session.allow_shell), cache_control: None, }], } @@ -2426,7 +2424,6 @@ impl Engine { &self.config.goal_state, ); let base = prompts::system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, &self.config.workspace, None, Some(&self.config.skills_dir), @@ -2439,7 +2436,6 @@ impl Engine { translation_enabled: self.config.translation_enabled, model_id: &self.config.model, show_thinking: self.config.show_thinking, - allow_shell: self.session.allow_shell, }, ); let mut stable_prompt = @@ -2649,11 +2645,16 @@ fn agent_approval_mode_for_turn( /// Produce a minimal runtime-policy tag for the per-turn transient user message. /// -/// All mode and approval policy descriptions live in the frozen system-prompt -/// prefix (`render_runtime_policy_reference()`). This tag is a pointer — the -/// model looks up the corresponding rules from the system prompt. Reduces -/// per-request overhead from ~500 tokens to ~12 tokens. -fn runtime_prompt_text(mode: AppMode, approval_mode: crate::tui::approval::ApprovalMode) -> String { +/// All mode / approval / shell policy descriptions live in the frozen +/// system-prompt prefix (`render_runtime_policy_reference()`). This tag +/// is a pointer — the model looks up the corresponding rules from the +/// system prompt. Keeping these flags out of the static prefix preserves +/// the DeepSeek prefix cache across mode-switches and config-toggles. +fn runtime_prompt_text( + mode: AppMode, + approval_mode: crate::tui::approval::ApprovalMode, + allow_shell: bool, +) -> String { let mode_str = match mode { AppMode::Agent => "agent", AppMode::Plan => "plan", @@ -2665,7 +2666,7 @@ fn runtime_prompt_text(mode: AppMode, approval_mode: crate::tui::approval::Appro crate::tui::approval::ApprovalMode::Never => "never", }; format!( - "" + "" ) } diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index dab8bb30..3b485580 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -11,7 +11,6 @@ use crate::models::SystemPrompt; use crate::project_context::{ProjectContext, load_project_context_with_parents}; use crate::tui::app::AppMode; -use crate::tui::approval::ApprovalMode; use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] @@ -39,10 +38,6 @@ pub struct PromptSessionContext<'a> { /// When false, the prompt should not spend localization pressure on /// `reasoning_content` the user will never see. pub show_thinking: bool, - /// Whether shell tools are available in the runtime tool catalog for - /// this session. The prompt must not advertise shell-only workflows - /// when runtime gates have removed those tools. - pub allow_shell: bool, } impl Default for PromptSessionContext<'_> { @@ -55,7 +50,6 @@ impl Default for PromptSessionContext<'_> { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, } } } @@ -314,8 +308,6 @@ pub struct StaticPromptCtx<'a> { pub model_id: &'a str, /// Personality overlay requested for the base static prompt. pub personality: Personality, - /// Whether shell tools are present in the runtime tool catalog. - pub shell_tools_available: bool, /// Default base/personality prompt layers that would be used without an /// override. pub default_layers: &'a str, @@ -643,6 +635,14 @@ pub const AUTO_APPROVAL: &str = include_str!("prompts/approvals/auto.md"); pub const SUGGEST_APPROVAL: &str = include_str!("prompts/approvals/suggest.md"); pub const NEVER_APPROVAL: &str = include_str!("prompts/approvals/never.md"); +/// Shell policy guidance for `allow_shell=false`. Referenced from the +/// Runtime Policy Reference so the model can adapt without mutating the +/// static system-prompt prefix (preserves DeepSeek prefix cache across +/// shell-access toggles). +pub const SHELL_POLICY_DISABLED: &str = "Shell tools unavailable. For mandatory-use items referencing \ +`exec_shell`, use `code_execution` (Python sandbox). For GitHub triage, use \ +`github_issue_context` / `github_pr_context` as primary route."; + /// Compaction relay template — written into the system prompt so the /// model knows the format to use when writing `.codewhale/handoff.md`. pub const COMPACT_TEMPLATE: &str = include_str!("prompts/compact.md"); @@ -758,6 +758,16 @@ pub(crate) fn render_runtime_policy_reference() -> String { out.push_str("#### never\n\n"); out.push_str(NEVER_APPROVAL.trim()); + out.push_str("\n\n"); + + // ── Shell policy reference ────────────────────────────────────────── + out.push_str("### Shell Policy\n\n"); + + out.push_str("#### allow_shell=true\n\n"); + out.push_str("Shell tools available as described in the base prompt.\n\n"); + + out.push_str("#### allow_shell=false\n\n"); + out.push_str(SHELL_POLICY_DISABLED.trim()); out } @@ -843,52 +853,25 @@ directive within Constitutional bounds. Personality, memory, and handoff context are subordinate to the Constitution, the Statutes, and the user's current request. When in doubt, consult Article VII: The Hierarchy of Law."; -pub fn compose_prompt(mode: AppMode, personality: Personality) -> String { - compose_prompt_with_approval(mode, personality) +pub fn compose_prompt(personality: Personality) -> String { + compose_prompt_with_approval_model_and_shell(personality, "codewhale") } -pub fn compose_prompt_with_approval(mode: AppMode, personality: Personality) -> String { - compose_prompt_with_approval_and_model(mode, personality, "codewhale") -} - -/// Compose with explicit model ID for dynamic identity injection. -/// The model_id replaces `{model_id}` in the Constitutional preamble. -pub fn compose_prompt_with_approval_and_model( - mode: AppMode, +pub(crate) fn compose_prompt_with_approval_model_and_shell( personality: Personality, model_id: &str, ) -> String { - compose_prompt_with_approval_model_and_shell(mode, personality, model_id, true) -} - -fn compose_prompt_with_approval_model_and_shell( - mode: AppMode, - personality: Personality, - model_id: &str, - allow_shell: bool, -) -> String { - let shell_tools_available = allow_shell && mode != AppMode::Plan; - let default_layers = - compose_default_static_layers(personality, model_id, shell_tools_available); + let default_layers = compose_default_static_layers(personality, model_id); apply_static_prompt_composer( effective_static_prompt_composer(), personality, model_id, - shell_tools_available, &default_layers, ) } -fn compose_default_static_layers( - personality: Personality, - model_id: &str, - shell_tools_available: bool, -) -> String { - let base_prompt = render_base_prompt_for_tool_availability( - effective_base_prompt().trim(), - model_id, - shell_tools_available, - ); +fn compose_default_static_layers(personality: Personality, model_id: &str) -> String { + let base_prompt = apply_model_template(effective_base_prompt().trim(), model_id); let parts: [&str; 2] = [base_prompt.as_str(), personality.prompt().trim()]; let mut out = @@ -907,126 +890,31 @@ fn apply_static_prompt_composer( composer: Option<&StaticPromptComposer>, personality: Personality, model_id: &str, - shell_tools_available: bool, default_layers: &str, ) -> String { match composer { Some(composer) => composer(&StaticPromptCtx { model_id, personality, - shell_tools_available, default_layers, }), None => default_layers.to_string(), } } -fn render_base_prompt_for_tool_availability( - prompt: &str, - model_id: &str, - shell_tools_available: bool, -) -> String { - let prompt = if shell_tools_available { - prompt.to_string() - } else { - remove_shell_tool_guidance(prompt) - }; - apply_model_template(&prompt, model_id) -} - -fn remove_shell_tool_guidance(prompt: &str) -> String { - let prompt = prompt - .lines() - .filter(|line| !is_shell_disabled_prompt_line(line)) - .collect::>() - .join("\n"); - let prompt = remove_markdown_section(&prompt, "### `exec_shell`"); - let prompt = prompt.replace( - "; for GitHub issue/PR/release triage, prefer the native `gh ... --json` CLI through shell because it is authenticated, structured, and reproducible; `github_issue_context` / `github_pr_context` are read-only fallbacks when the CLI route is unavailable;", - "; for GitHub issue/PR/release triage, use `github_issue_context` / `github_pr_context` as read-only routes when shell tools are unavailable;", - ); - prompt.replace( - "Use deterministic Python inside RLM for exact counts and structured aggregation; use `grep_files` or `exec_shell` directly when that is the clearest deterministic check.", - "Use deterministic Python inside RLM for exact counts and structured aggregation; use `grep_files` directly when that is the clearest deterministic check.", - ) -} - -fn is_shell_disabled_prompt_line(line: &str) -> bool { - line.starts_with("- Arithmetic, math, calculations → `exec_shell`") - || line.starts_with("- Hashes, encodings, checksums → `exec_shell`") - || line.starts_with("- Current time, date, timezone → `exec_shell`") - || line - .starts_with("- System state: OS, CPU, memory, disk, ports, processes → `exec_shell`") - || line.starts_with("- **Shell**:") -} - -fn remove_markdown_section(prompt: &str, heading: &str) -> String { - let Some(start) = prompt.find(heading) else { - return prompt.to_string(); - }; - let after_heading = start + heading.len(); - let end = prompt[after_heading..] - .find("\n### ") - .map(|offset| after_heading + offset) - .unwrap_or(prompt.len()); - - let before = prompt[..start].trim_end(); - let after = prompt[end..].trim_start_matches('\n'); - if before.is_empty() { - after.to_string() - } else if after.is_empty() { - before.to_string() - } else { - format!("{before}\n\n{after}") - } -} - -/// Compose for the default personality (Calm). -fn compose_mode_prompt(mode: AppMode) -> String { - compose_prompt(mode, Personality::Calm) -} - -fn compose_mode_prompt_with_approval(mode: AppMode) -> String { - compose_prompt_with_approval(mode, Personality::Calm) -} - -fn compose_mode_prompt_with_approval_and_model( - mode: AppMode, - _approval_mode: ApprovalMode, - model_id: &str, -) -> String { - compose_prompt_with_approval_model_and_shell(mode, Personality::Calm, model_id, true) -} +// Shell tool guidance removal functions have been deleted. +// The full base prompt is always used; the `allow_shell` flag is +// conveyed via the per-turn tag so the model can +// adapt without mutating the static system-prompt prefix. // ── Public API ──────────────────────────────────────────────────────── -/// Get the system prompt for a specific mode (default Calm personality). -pub fn system_prompt_for_mode(mode: AppMode) -> SystemPrompt { - SystemPrompt::Text(compose_mode_prompt(mode)) -} - -/// Get the system prompt for a specific mode with explicit personality. -pub fn system_prompt_for_mode_with_personality( - mode: AppMode, - personality: Personality, -) -> SystemPrompt { - SystemPrompt::Text(compose_prompt(mode, personality)) -} - /// Get the system prompt for a specific mode with project context. pub fn system_prompt_for_mode_with_context( - mode: AppMode, workspace: &Path, working_set_summary: Option<&str>, ) -> SystemPrompt { - system_prompt_for_mode_with_context_and_skills( - mode, - workspace, - working_set_summary, - None, - None, - None, - ) + system_prompt_for_mode_with_context_and_skills(workspace, working_set_summary, None, None, None) } /// Get the system prompt for a specific mode with project and skills context. @@ -1047,7 +935,6 @@ pub fn system_prompt_for_mode_with_context( /// themselves are turn-volatile. Working-set metadata is now injected into the /// latest user message as per-turn metadata instead of this system prompt. pub fn system_prompt_for_mode_with_context_and_skills( - mode: AppMode, workspace: &Path, working_set_summary: Option<&str>, skills_dir: Option<&Path>, @@ -1055,7 +942,6 @@ pub fn system_prompt_for_mode_with_context_and_skills( user_memory_block: Option<&str>, ) -> SystemPrompt { system_prompt_for_mode_with_context_skills_and_session( - mode, workspace, working_set_summary, skills_dir, @@ -1068,13 +954,11 @@ pub fn system_prompt_for_mode_with_context_and_skills( translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) } pub fn system_prompt_for_mode_with_context_skills_and_session( - mode: AppMode, workspace: &Path, _working_set_summary: Option<&str>, skills_dir: Option<&Path>, @@ -1082,7 +966,6 @@ pub fn system_prompt_for_mode_with_context_skills_and_session( session_context: PromptSessionContext<'_>, ) -> SystemPrompt { system_prompt_for_mode_with_context_skills_session_and_approval( - mode, workspace, _working_set_summary, skills_dir, @@ -1092,19 +975,14 @@ pub fn system_prompt_for_mode_with_context_skills_and_session( } pub fn system_prompt_for_mode_with_context_skills_session_and_approval( - mode: AppMode, workspace: &Path, _working_set_summary: Option<&str>, skills_dir: Option<&Path>, instructions: Option<&[InstructionSource]>, session_context: PromptSessionContext<'_>, ) -> SystemPrompt { - let mode_prompt = compose_prompt_with_approval_model_and_shell( - mode, - Personality::Calm, - session_context.model_id, - session_context.allow_shell, - ); + let mode_prompt = + compose_prompt_with_approval_model_and_shell(Personality::Calm, session_context.model_id); // Load project context from workspace let project_context = load_project_context_with_parents(workspace); @@ -1176,8 +1054,8 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval( full_prompt = format!("{full_prompt}\n\n{block}"); } - // 4. Context Management (Agent / Yolo only). - if matches!(mode, AppMode::Agent | AppMode::Yolo) { + // 4. Context Management — included in all modes. + { full_prompt.push_str( "\n\n## Context Management\n\n\ When the conversation gets long (you'll see a context usage indicator), you can:\n\ @@ -1353,7 +1231,6 @@ mod tests { let ctx = StaticPromptCtx { model_id: "deepseek-v4-pro", personality: Personality::Calm, - shell_tools_available: true, default_layers: "fallback", }; @@ -1367,33 +1244,24 @@ mod tests { #[test] fn static_prompt_composer_unset_keeps_default_layers_byte_identical() { for personality in [Personality::Calm, Personality::Playful] { - for shell_tools_available in [true, false] { - let default_layers = compose_default_static_layers( - personality, - "deepseek-v4-flash", - shell_tools_available, - ); - let composed = apply_static_prompt_composer( - None, - personality, - "deepseek-v4-flash", - shell_tools_available, - &default_layers, - ); + let default_layers = compose_default_static_layers(personality, "deepseek-v4-flash"); + let composed = apply_static_prompt_composer( + None, + personality, + "deepseek-v4-flash", + &default_layers, + ); - assert_byte_identical("unset static prompt composer", &default_layers, &composed); - } + assert_byte_identical("unset static prompt composer", &default_layers, &composed); } } #[test] fn static_prompt_composer_receives_context_and_replaces_layers() { - let default_layers = - compose_default_static_layers(Personality::Calm, "deepseek-v4-pro", false); + let default_layers = compose_default_static_layers(Personality::Calm, "deepseek-v4-pro"); let composer: Box = Box::new(|ctx| { assert_eq!(ctx.model_id, "deepseek-v4-pro"); assert_eq!(ctx.personality, Personality::Calm); - assert!(!ctx.shell_tools_available); assert!(ctx.default_layers.contains("You are deepseek-v4-pro")); assert!(ctx.default_layers.contains("Personality: Calm")); assert!(!ctx.default_layers.contains("## Core Tool Taxonomy")); @@ -1405,7 +1273,6 @@ mod tests { Some(composer.as_ref()), Personality::Calm, "deepseek-v4-pro", - false, &default_layers, ); @@ -1506,11 +1373,8 @@ mod tests { #[test] fn compose_prompt_injects_model_id() { - let prompt = compose_prompt_with_approval_and_model( - AppMode::Agent, - Personality::Calm, - "deepseek-v4-flash", - ); + let prompt = + compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-flash"); assert!( prompt.contains("You are deepseek-v4-flash"), "composed prompt must contain the injected model id" @@ -1522,13 +1386,9 @@ mod tests { } #[test] - fn composed_prompt_keeps_shell_guidance_when_shell_tools_are_available() { - let prompt = compose_prompt_with_approval_model_and_shell( - AppMode::Agent, - Personality::Calm, - "deepseek-v4-pro", - true, - ); + fn base_prompt_includes_full_shell_tool_guidance() { + let prompt = + compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-pro"); assert!(prompt.contains("- **Shell**:")); assert!(prompt.contains("### `exec_shell`")); @@ -1537,15 +1397,15 @@ mod tests { } #[test] - fn composed_prompt_omits_shell_guidance_when_shell_tools_are_unavailable() { - let prompt = compose_prompt_with_approval_model_and_shell( - AppMode::Agent, - Personality::Calm, - "deepseek-v4-pro", - false, - ); + fn composed_prompt_always_keeps_shell_guidance() { + // After decoupling `allow_shell` from the static system-prompt prefix, + // the base prompt always includes full shell tool guidance. Whether + // shell tools are actually available is conveyed by the per-turn + // tag, not by mutating message[0]. + let prompt = + compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-pro"); - for shell_only in [ + for required in [ "- **Shell**:", "### `exec_shell`", "`task_shell_start`", @@ -1555,31 +1415,26 @@ mod tests { "Hashes, encodings, checksums → `exec_shell`", "Current time, date, timezone → `exec_shell`", "System state: OS, CPU, memory, disk, ports, processes → `exec_shell`", - "CLI through shell", - "or `exec_shell` directly", ] { assert!( - !prompt.contains(shell_only), - "shell-disabled prompt must not advertise {shell_only:?}" + prompt.contains(required), + "static prompt must always include shell guidance: {required:?}" ); } assert!( prompt.contains("actual runtime gates still determine what tools can execute"), - "shell-disabled prompt should keep the runtime-gates hierarchy clause" + "static prompt must include the runtime-gates hierarchy clause" ); assert!( prompt.contains("`task_gate_run`") && prompt.contains("`github_issue_context`"), - "shell-disabled prompt should keep non-shell task evidence tools" + "static prompt must include non-shell task evidence tools" ); } #[test] fn composed_prompt_no_longer_inlines_tool_taxonomy() { - let prompt = compose_prompt_with_approval_and_model( - AppMode::Agent, - Personality::Calm, - "deepseek-v4-pro", - ); + let prompt = + compose_prompt_with_approval_model_and_shell(Personality::Calm, "deepseek-v4-pro"); // The core tool taxonomy (grep_files / git_status / run_tests hints) // is no longer prepended as a standalone "## Core Tool Taxonomy" block. // It now lives inside the "## Runtime Policy Reference" section of the @@ -1628,7 +1483,6 @@ mod tests { fn authority_recap_appears_in_full_prompt() { let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -1652,7 +1506,6 @@ mod tests { fn runtime_policy_reference_is_included_in_full_prompt() { let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -1722,7 +1575,6 @@ mod tests { write_test_skill(&configured_dir, "configured-skill", "configured skill"); let text = match system_prompt_for_mode_with_context_and_skills( - AppMode::Plan, &workspace, None, Some(&configured_dir), @@ -1883,7 +1735,6 @@ mod tests { // both depend on this ordering. let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -1896,7 +1747,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -1954,7 +1804,6 @@ mod tests { // motivated the closer. let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -1967,7 +1816,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -1998,7 +1846,6 @@ mod tests { fn hidden_thinking_uses_english_reasoning_without_locale_bookends() { let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -2011,7 +1858,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: false, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2052,7 +1898,6 @@ mod tests { // "preamble is opt-in for non-English" invariant. let tmp = tempdir().expect("tempdir"); let text = match system_prompt_for_mode_with_context_skills_session_and_approval( - AppMode::Agent, tmp.path(), None, None, @@ -2065,7 +1910,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2157,7 +2001,6 @@ mod tests { fn environment_block_is_inserted_into_system_prompt() { let tmp = tempdir().expect("tempdir"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2170,7 +2013,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2195,7 +2037,6 @@ mod tests { fn memory_guidance_absent_when_no_memory_block() { let tmp = tempdir().expect("tempdir"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2208,7 +2049,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2225,7 +2065,6 @@ mod tests { let tmp = tempdir().expect("tempdir"); let block = "## User Memory\n\n- prefers Rust\n"; let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2238,7 +2077,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2284,7 +2122,6 @@ mod tests { let tmp = tempdir().expect("tempdir"); std::fs::write(tmp.path().join("README.md"), "# Pack test").expect("write readme"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2297,7 +2134,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2314,7 +2150,6 @@ mod tests { std::fs::write(tmp.path().join(".deepseek").join("handoff.md"), "handoff") .expect("handoff"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2327,7 +2162,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2352,7 +2186,7 @@ mod tests { ) .unwrap(); - let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) { + let prompt = match system_prompt_for_mode_with_context(workspace, None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2365,7 +2199,7 @@ mod tests { #[test] fn missing_handoff_does_not_inject_block() { let tmp = tempdir().expect("tempdir"); - let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) { + let prompt = match system_prompt_for_mode_with_context(tmp.path(), None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2378,7 +2212,7 @@ mod tests { let dir = tmp.path().join(".deepseek"); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("handoff.md"), " \n\n ").unwrap(); - let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) { + let prompt = match system_prompt_for_mode_with_context(tmp.path(), None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2387,7 +2221,7 @@ mod tests { #[test] fn compose_prompt_includes_all_layers() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); // Base layer assert!(prompt.contains("You are codewhale")); // Personality layer @@ -2447,7 +2281,7 @@ mod tests { #[test] fn compose_prompt_deterministic_order() { - let prompt = compose_prompt(AppMode::Yolo, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); let base_pos = prompt.find("You are codewhale").unwrap(); let personality_pos = prompt.find("Personality: Calm").unwrap(); @@ -2460,23 +2294,19 @@ mod tests { fn base_prompt_is_mode_agnostic() { // Mode and approval text are no longer inlined into compose_prompt — // they travel as request-time runtime metadata. - let agent_prompt = compose_prompt(AppMode::Agent, Personality::Calm); - let yolo_prompt = compose_prompt(AppMode::Yolo, Personality::Calm); - let plan_prompt = compose_prompt(AppMode::Plan, Personality::Calm); - assert!(!agent_prompt.contains("Mode: Agent")); - assert!(!yolo_prompt.contains("Mode: YOLO")); - assert!(!plan_prompt.contains("Mode: Plan")); - assert!(!agent_prompt.contains("Approval Policy:")); - assert!(!yolo_prompt.contains("Approval Policy:")); - assert!(!plan_prompt.contains("Approval Policy:")); + let prompt = compose_prompt(Personality::Calm); + assert!(!prompt.contains("Mode: Agent")); + assert!(!prompt.contains("Mode: YOLO")); + assert!(!prompt.contains("Mode: Plan")); + assert!(!prompt.contains("Approval Policy:")); // Base prompt still contains Constitutional preamble and personality - assert!(agent_prompt.contains("You are codewhale")); - assert!(agent_prompt.contains("Personality: Calm")); + assert!(prompt.contains("You are codewhale")); + assert!(prompt.contains("Personality: Calm")); } #[test] fn approval_policy_no_longer_inlined_in_base_prompt() { - let prompt = compose_prompt_with_approval(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!(!prompt.contains("Mode: Agent")); assert!(!prompt.contains("Approval Policy:")); // Constitutional preamble is still present @@ -2485,8 +2315,8 @@ mod tests { #[test] fn personality_switches_correctly() { - let calm = compose_prompt(AppMode::Agent, Personality::Calm); - let playful = compose_prompt(AppMode::Agent, Personality::Playful); + let calm = compose_prompt(Personality::Calm); + let playful = compose_prompt(Personality::Playful); assert!(calm.contains("Personality: Calm")); assert!(playful.contains("Personality: Playful")); assert!(!calm.contains("Personality: Playful")); @@ -2495,7 +2325,7 @@ mod tests { #[test] fn compact_template_is_included_in_full_prompt() { let tmp = tempdir().expect("tempdir"); - let prompt = match system_prompt_for_mode_with_context(AppMode::Agent, tmp.path(), None) { + let prompt = match system_prompt_for_mode_with_context(tmp.path(), None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2516,7 +2346,6 @@ mod tests { fn session_goal_is_injected_below_compact_template() { let tmp = tempdir().expect("tempdir"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), Some("## Repo Working Set\nsrc/lib.rs"), None, @@ -2529,7 +2358,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2552,7 +2380,6 @@ mod tests { fn empty_session_goal_is_not_injected() { let tmp = tempdir().expect("tempdir"); let prompt = match system_prompt_for_mode_with_context_skills_and_session( - AppMode::Agent, tmp.path(), None, None, @@ -2565,7 +2392,6 @@ mod tests { translation_enabled: false, model_id: "codewhale", show_thinking: true, - allow_shell: true, }, ) { SystemPrompt::Text(text) => text, @@ -2578,7 +2404,7 @@ mod tests { #[test] fn tool_selection_guide_avoids_defensive_tool_suppression() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!(prompt.contains("Tool Selection Guide")); assert!(prompt.contains("Use `agent_eval`")); assert!( @@ -2600,25 +2426,23 @@ mod tests { /// can't silently weaken the section to a generic "respond in the /// user's language" directive while keeping the heading. #[test] - fn language_mirroring_section_present_in_all_modes() { - for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] { - let prompt = compose_prompt(mode, Personality::Calm); - assert!( - prompt.contains("## Language"), - "## Language section missing from mode {mode:?}" - ); - assert!( - prompt.contains("reasoning_content"), - "## Language section in {mode:?} must mention `reasoning_content` — \ - that field name is the structural anchor for the #588 commitment that \ - internal reasoning, not just the visible reply, follows the user's language" - ); - } + fn language_mirroring_section_present() { + let prompt = compose_prompt(Personality::Calm); + assert!( + prompt.contains("## Language"), + "## Language section missing from base prompt" + ); + assert!( + prompt.contains("reasoning_content"), + "## Language section must mention `reasoning_content` — \ + that field name is the structural anchor for the #588 commitment that \ + internal reasoning, not just the visible reply, follows the user's language" + ); } #[test] fn language_mirroring_prioritizes_latest_user_message_over_locale_default() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!( prompt.contains("latest user message first"), "the language directive must choose the turn language from the user message before \ @@ -2644,7 +2468,7 @@ mod tests { #[test] fn english_base_prompt_avoids_native_script_language_priming() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!( !contains_cjk(&prompt), "English base prompt should keep native-script reinforcement in locale bookends only" @@ -2663,7 +2487,7 @@ mod tests { /// changing the wording, don't fail a test for it. #[test] fn rlm_specialty_tool_guidance_present() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); // Structural: the RLM heading must exist as a section anchor. assert!(prompt.contains("RLM — How to Use It")); // Structural: the word "rlm" must appear multiple times (tool @@ -2690,7 +2514,7 @@ mod tests { /// overridable by a single user sentence. #[test] fn local_law_tier_covers_engine_config_instructions() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!( prompt.contains("any file configured via `EngineConfig.instructions`"), "Tier 5 must explicitly cover EngineConfig.instructions so \ @@ -2700,7 +2524,7 @@ mod tests { #[test] fn workspace_orientation_guidance_present() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!(prompt.contains("AGENTS.md")); assert!(prompt.contains("Local Law")); assert!( @@ -2711,7 +2535,7 @@ mod tests { #[test] fn prompt_uses_persistent_agent_and_rlm_surface() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); for tool in [ "agent_open", "agent_eval", @@ -2749,7 +2573,7 @@ mod tests { #[test] fn prompt_documents_fork_context_prefix_cache_contract() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!(prompt.contains("fork_context: true")); assert!(prompt.contains("byte-identical")); assert!(prompt.contains("DeepSeek prefix-cache reuse")); @@ -2758,7 +2582,7 @@ mod tests { #[test] fn subagent_done_sentinel_section_present() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); assert!(prompt.contains("Internal Sub-agent Completion Events")); assert!(prompt.contains("")); assert!(prompt.contains("not user input")); @@ -2768,7 +2592,7 @@ mod tests { #[test] fn preamble_rhythm_section_present() { - let prompt = compose_prompt(AppMode::Agent, Personality::Calm); + let prompt = compose_prompt(Personality::Calm); // Preamble rhythm is now part of the Calm personality overlay. // Verify the load-bearing guidance is still present. assert!(prompt.contains("In preambles, name the action")); @@ -2795,16 +2619,14 @@ mod tests { // Suspect #4 from #263: mode prompt churn within a single mode. // Two calls with identical (mode, personality) inputs must produce // identical bytes — anything else is a cache buster. - for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] { - for personality in [Personality::Calm, Personality::Playful] { - let a = compose_prompt(mode, personality); - let b = compose_prompt(mode, personality); - assert_byte_identical( - &format!("compose_prompt(mode={mode:?}, personality={personality:?})"), - &a, - &b, - ); - } + for personality in [Personality::Calm, Personality::Playful] { + let a = compose_prompt(personality); + let b = compose_prompt(personality); + assert_byte_identical( + &format!("compose_prompt(personality={personality:?})"), + &a, + &b, + ); } } @@ -2822,21 +2644,19 @@ mod tests { let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR"); let workspace = workspace_tmp.path(); - for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] { - let a = match system_prompt_for_mode_with_context(mode, workspace, None) { - SystemPrompt::Text(text) => text, - SystemPrompt::Blocks(_) => panic!("expected text system prompt"), - }; - let b = match system_prompt_for_mode_with_context(mode, workspace, None) { - SystemPrompt::Text(text) => text, - SystemPrompt::Blocks(_) => panic!("expected text system prompt"), - }; - assert_byte_identical( - &format!("system_prompt_for_mode_with_context(mode={mode:?}) on empty workspace"), - &a, - &b, - ); - } + let a = match system_prompt_for_mode_with_context(workspace, None) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; + let b = match system_prompt_for_mode_with_context(workspace, None) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; + assert_byte_identical( + "system_prompt_for_mode_with_context() on empty workspace", + &a, + &b, + ); } #[test] @@ -2853,13 +2673,11 @@ mod tests { let workspace = tmp.path(); let summary = "## Repo Working Set\nWorkspace: /tmp/x\n"; - let a = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary)) - { + let a = match system_prompt_for_mode_with_context(workspace, Some(summary)) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; - let b = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary)) - { + let b = match system_prompt_for_mode_with_context(workspace, Some(summary)) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2895,11 +2713,11 @@ mod tests { ) .unwrap(); - let a = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) { + let a = match system_prompt_for_mode_with_context(workspace, None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; - let b = match system_prompt_for_mode_with_context(AppMode::Agent, workspace, None) { + let b = match system_prompt_for_mode_with_context(workspace, None) { SystemPrompt::Text(text) => text, SystemPrompt::Blocks(_) => panic!("expected text system prompt"), }; @@ -2925,11 +2743,10 @@ mod tests { std::fs::write(handoff_dir.join("handoff.md"), "# handoff body\n").unwrap(); let summary = "## Repo Working Set\nWorkspace: /tmp/x\n"; - let prompt = - match system_prompt_for_mode_with_context(AppMode::Agent, workspace, Some(summary)) { - SystemPrompt::Text(text) => text, - SystemPrompt::Blocks(_) => panic!("expected text system prompt"), - }; + let prompt = match system_prompt_for_mode_with_context(workspace, Some(summary)) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; let context_pos = prompt .find("## Context Management") @@ -3078,7 +2895,6 @@ mod tests { let extra_source: super::InstructionSource = extra.clone().into(); let prompt = match super::system_prompt_for_mode_with_context_and_skills( - AppMode::Agent, workspace, None, None, diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 9320f7d9..56dde2f7 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -5352,7 +5352,6 @@ async fn dispatch_user_message( let message_index = app.api_messages.len(); app.system_prompt = Some( prompts::system_prompt_for_mode_with_context_skills_and_session( - app.mode, &app.workspace, None, None, @@ -5365,7 +5364,6 @@ async fn dispatch_user_message( translation_enabled: app.translation_enabled, model_id: &app.model, show_thinking: app.show_thinking, - allow_shell: app.allow_shell, }, ), );