diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index a031ef07..9fa110a9 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -139,6 +139,7 @@ pub struct EngineConfig { /// Path to the user memory file (#489). Always populated; only /// consulted when `memory_enabled` is `true`. pub memory_path: PathBuf, + pub goal_objective: Option, } impl Default for EngineConfig { @@ -168,6 +169,7 @@ impl Default for EngineConfig { subagent_model_overrides: HashMap::new(), memory_enabled: false, memory_path: PathBuf::from("./memory.md"), + goal_objective: None, } } } @@ -355,13 +357,16 @@ impl Engine { let working_set_summary = session.working_set.summary_block(&config.workspace); let user_memory_block = crate::memory::compose_block(config.memory_enabled, &config.memory_path); - let system_prompt = prompts::system_prompt_for_mode_with_context_and_skills( + let system_prompt = prompts::system_prompt_for_mode_with_context_skills_and_session( AppMode::Agent, &config.workspace, None, Some(&config.skills_dir), Some(&config.instructions), - user_memory_block.as_deref(), + prompts::PromptSessionContext { + user_memory_block: user_memory_block.as_deref(), + goal_objective: config.goal_objective.as_deref(), + }, ); session.system_prompt = append_working_set_summary(Some(system_prompt), working_set_summary.as_deref()); @@ -461,6 +466,7 @@ impl Engine { content, mode, model, + goal_objective, reasoning_effort, allow_shell, trust_mode, @@ -470,6 +476,7 @@ impl Engine { content, mode, model, + goal_objective, reasoning_effort, allow_shell, trust_mode, @@ -653,6 +660,7 @@ impl Engine { new_message, mode, self.session.model.clone(), + self.config.goal_objective.clone(), self.session.reasoning_effort.clone(), self.session.allow_shell, self.session.trust_mode, @@ -691,6 +699,7 @@ impl Engine { content: String, mode: AppMode, model: String, + goal_objective: Option, reasoning_effort: Option, allow_shell: bool, trust_mode: bool, @@ -770,6 +779,7 @@ impl Engine { self.session.model = model; self.config.model.clone_from(&self.session.model); + self.config.goal_objective = goal_objective; self.session.reasoning_effort = reasoning_effort; self.session.allow_shell = allow_shell; self.config.allow_shell = allow_shell; @@ -1631,13 +1641,16 @@ impl Engine { .summary_block(&self.config.workspace); let user_memory_block = crate::memory::compose_block(self.config.memory_enabled, &self.config.memory_path); - let base = prompts::system_prompt_for_mode_with_context_and_skills( + let base = prompts::system_prompt_for_mode_with_context_skills_and_session( mode, &self.config.workspace, None, Some(&self.config.skills_dir), Some(&self.config.instructions), - user_memory_block.as_deref(), + prompts::PromptSessionContext { + user_memory_block: user_memory_block.as_deref(), + goal_objective: self.config.goal_objective.as_deref(), + }, ); let stable_prompt = merge_system_prompts(Some(&base), self.session.compaction_summary_prompt.clone()); diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index a84cc926..1e5e0349 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -66,6 +66,27 @@ fn engine_handle_cancel_tracks_latest_turn_token() { assert!(!stale_token.is_cancelled()); } +#[test] +fn engine_initial_prompt_includes_configured_goal() { + let config = EngineConfig { + goal_objective: Some("Fix goal handoff".to_string()), + ..Default::default() + }; + let (engine, _handle) = Engine::new(config, &Config::default()); + let prompt = match engine.session.system_prompt { + Some(SystemPrompt::Text(text)) => text, + Some(SystemPrompt::Blocks(blocks)) => blocks + .into_iter() + .map(|block| block.text) + .collect::>() + .join("\n"), + None => panic!("expected system prompt"), + }; + + assert!(prompt.contains("")); + assert!(prompt.contains("Fix goal handoff")); +} + #[test] fn parallel_batch_requires_read_only_parallel_tools() { let plans = vec![make_plan(true, true, false, false)]; diff --git a/crates/tui/src/core/ops.rs b/crates/tui/src/core/ops.rs index 1b55e11c..1fed70b2 100644 --- a/crates/tui/src/core/ops.rs +++ b/crates/tui/src/core/ops.rs @@ -16,6 +16,7 @@ pub enum Op { content: String, mode: AppMode, model: String, + goal_objective: Option, /// Reasoning-effort tier: `"off" | "low" | "medium" | "high" | "max"`. /// `None` lets the provider apply its default. reasoning_effort: Option, @@ -104,6 +105,7 @@ impl Op { content: content.into(), mode, model: model.into(), + goal_objective: None, reasoning_effort, allow_shell, trust_mode, diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index c12a9868..3a85dfbc 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -3631,6 +3631,7 @@ async fn run_exec_agent( subagent_model_overrides: config.subagent_model_overrides(), memory_enabled: config.memory_enabled(), memory_path: config.memory_path(), + goal_objective: None, }; let engine_handle = spawn_engine(engine_config, config); diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 68441434..26913357 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -12,6 +12,12 @@ use crate::project_context::{ProjectContext, load_project_context_with_parents}; use crate::tui::app::AppMode; use std::path::{Path, PathBuf}; +#[derive(Debug, Clone, Copy, Default)] +pub struct PromptSessionContext<'a> { + pub user_memory_block: Option<&'a str>, + pub goal_objective: Option<&'a str>, +} + /// Conventional location for the structured session-handoff artifact (#32). /// A previous session writes it on exit / `/compact`; the next session reads /// it back on startup and prepends it to the system prompt so a fresh agent @@ -260,6 +266,27 @@ pub fn system_prompt_for_mode_with_context_and_skills( skills_dir: Option<&Path>, instructions: Option<&[PathBuf]>, user_memory_block: Option<&str>, +) -> SystemPrompt { + system_prompt_for_mode_with_context_skills_and_session( + mode, + workspace, + working_set_summary, + skills_dir, + instructions, + PromptSessionContext { + user_memory_block, + goal_objective: None, + }, + ) +} + +pub fn system_prompt_for_mode_with_context_skills_and_session( + mode: AppMode, + workspace: &Path, + working_set_summary: Option<&str>, + skills_dir: Option<&Path>, + instructions: Option<&[PathBuf]>, + session_context: PromptSessionContext<'_>, ) -> SystemPrompt { let mode_prompt = compose_mode_prompt(mode); @@ -293,12 +320,21 @@ pub fn system_prompt_for_mode_with_context_and_skills( // 2.5b. User memory block (#489). Goes above skills/context-management // because it's session-stable: the memory file changes when the user // edits it via `/memory` or `# foo` quick-add, but not turn-over-turn. - if let Some(memory_block) = user_memory_block + if let Some(memory_block) = session_context.user_memory_block && !memory_block.trim().is_empty() { full_prompt = format!("{full_prompt}\n\n{memory_block}"); } + if let Some(goal_objective) = session_context.goal_objective + && !goal_objective.trim().is_empty() + { + full_prompt = format!( + "{full_prompt}\n\n## Current Session Goal\n\n\n{}\n", + goal_objective.trim() + ); + } + // 3. Skills block. #432: walks every candidate workspace // skills directory (`.agents/skills`, `skills`, // `.opencode/skills`, `.claude/skills`) plus the global @@ -503,6 +539,55 @@ mod tests { assert!(prompt.contains("### Next step")); } + #[test] + fn session_goal_is_injected_above_volatile_prompt_tail() { + 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, + None, + PromptSessionContext { + user_memory_block: None, + goal_objective: Some("Fix transcript corruption"), + }, + ) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; + + let goal_pos = prompt.find("").expect("goal block"); + let compact_pos = prompt.find("## Compaction Handoff").expect("compact block"); + let working_set_pos = prompt.find("## Repo Working Set").expect("working set"); + + assert!(prompt.contains("Fix transcript corruption")); + assert!(goal_pos < compact_pos); + assert!(goal_pos < working_set_pos); + } + + #[test] + 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, + None, + PromptSessionContext { + user_memory_block: None, + goal_objective: Some(" "), + }, + ) { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(_) => panic!("expected text system prompt"), + }; + + assert!(!prompt.contains("")); + assert!(!prompt.contains("## Current Session Goal")); + } + #[test] fn when_not_to_use_sections_present() { let prompt = compose_prompt(AppMode::Agent, Personality::Calm); diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 58829c09..5f4bcfca 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -1566,6 +1566,7 @@ impl RuntimeThreadManager { subagent_model_overrides: self.config.subagent_model_overrides(), memory_enabled: self.config.memory_enabled(), memory_path: self.config.memory_path(), + goal_objective: None, }; let engine = spawn_engine(engine_cfg, &self.config); diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 3c729df2..240ce5d5 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -534,6 +534,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { subagent_model_overrides: config.subagent_model_overrides(), memory_enabled: config.memory_enabled(), memory_path: config.memory_path(), + goal_objective: app.goal.goal_objective.clone(), } } @@ -3017,11 +3018,19 @@ async fn dispatch_user_message( ); let content = queued_message_content_for_app(app, &message, cwd); let message_index = app.api_messages.len(); - app.system_prompt = Some(prompts::system_prompt_for_mode_with_context( - app.mode, - &app.workspace, - None, - )); + app.system_prompt = Some( + prompts::system_prompt_for_mode_with_context_skills_and_session( + app.mode, + &app.workspace, + None, + None, + None, + prompts::PromptSessionContext { + user_memory_block: None, + goal_objective: app.goal.goal_objective.as_deref(), + }, + ), + ); app.add_message(HistoryCell::User { content: message.display.clone(), }); @@ -3065,6 +3074,7 @@ async fn dispatch_user_message( content, mode: app.mode, model: effective_model, + goal_objective: app.goal.goal_objective.clone(), reasoning_effort: app.reasoning_effort.api_value().map(str::to_string), allow_shell: app.allow_shell, trust_mode: app.trust_mode,