From 09c8d2be5aba1b6aa91794c21574cdd770c33ad9 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Thu, 18 Jun 2026 16:02:40 -0400 Subject: [PATCH] Update Moim (#9636) Signed-off-by: Douwe M Osinga Co-authored-by: Douwe Osinga Co-authored-by: Douwe M Osinga --- crates/goose/src/agents/agent.rs | 3 +- crates/goose/src/agents/extension_manager.rs | 70 +--- crates/goose/src/agents/moim.rs | 371 ++++++++++++++---- .../platform_extensions/code_execution.rs | 43 +- crates/goose/src/agents/prompt_manager.rs | 5 +- ...nager__tests__all_platform_extensions.snap | 17 + ..._agents__prompt_manager__tests__basic.snap | 18 + ..._prompt_manager__tests__one_extension.snap | 18 + ..._prompt_manager__tests__typical_setup.snap | 18 + crates/goose/src/prompts/system.md | 5 + crates/goose/tests/acp_common_tests/mod.rs | 16 +- 11 files changed, 414 insertions(+), 170 deletions(-) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index bc19b12ad..c2299e83b 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1898,7 +1898,8 @@ impl Agent { &session_config.id, conversation.clone(), &self.extension_manager, - &working_dir, + turns_taken, + max_turns, ).await; let mut stream = Self::stream_response_from_provider( diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index f5bff6f8a..a8ad77131 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -1949,51 +1949,7 @@ impl ExtensionManager { .map(|ext| ext.get_client()) } - pub async fn collect_moim( - &self, - session_id: &str, - working_dir: &std::path::Path, - ) -> Option { - // Skip MOIM for models with small context windows to avoid consuming limited context - const MIN_CONTEXT_FOR_MOIM: usize = 32_000; - if let Ok(provider_guard) = self.provider.try_lock() { - if let Some(provider) = provider_guard.as_ref() { - if provider.get_model_config().context_limit() < MIN_CONTEXT_FOR_MOIM { - return None; - } - } - } - - // Use minute-level granularity to prevent conversation changes every second - let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:00").to_string(); - let mut content = format!( - "\nIt is currently {}\nWorking directory: {}\n", - timestamp, - working_dir.display() - ); - - if let Ok(session) = self - .context - .session_manager - .get_session(session_id, false) - .await - { - if let (Some(total), Some(config)) = - (session.total_tokens, session.model_config.as_ref()) - { - let limit = config.context_limit(); - if total > 0 && limit > 0 { - let pct = (total as f64 / limit as f64 * 100.0).round() as u32; - content.push_str(&format!( - "Context: ~{}k/{}k tokens used ({}%)\n", - total / 1000, - limit / 1000, - pct - )); - } - } - } - + pub async fn collect_moim_parts(&self, session_id: &str) -> Vec { let platform_clients: Vec<(String, McpClientBox)> = { let extensions = self.extensions.lock().await; extensions @@ -2015,17 +1971,14 @@ impl ExtensionManager { .collect() }; + let mut parts = Vec::new(); for (name, client) in platform_clients { if let Some(moim_content) = client.get_moim(session_id).await { tracing::debug!("MOIM content from {}: {} chars", name, moim_content.len()); - content.push('\n'); - content.push_str(&moim_content); + parts.push(moim_content); } } - - content.push_str("\n"); - - Some(content) + parts } } @@ -2417,21 +2370,6 @@ mod tests { assert_eq!(result, "abc$KEY"); } - #[tokio::test] - async fn test_collect_moim_uses_minute_granularity() { - let temp_dir = tempfile::tempdir().unwrap(); - let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf()); - let working_dir = std::path::Path::new("/tmp"); - - if let Some(moim) = em.collect_moim("test-session-id", working_dir).await { - // Timestamp should end with :00 (seconds fixed to 00) - assert!( - moim.contains(":00\n"), - "Timestamp should use minute granularity" - ); - } - } - #[tokio::test] async fn test_tools_cache_invalidated_on_add_extension() { let temp_dir = tempfile::tempdir().unwrap(); diff --git a/crates/goose/src/agents/moim.rs b/crates/goose/src/agents/moim.rs index 80a4228fd..f6fda028a 100644 --- a/crates/goose/src/agents/moim.rs +++ b/crates/goose/src/agents/moim.rs @@ -1,118 +1,338 @@ use crate::agents::extension_manager::ExtensionManager; -use crate::conversation::message::Message; -use crate::conversation::{fix_conversation, Conversation}; -use rmcp::model::Role; -use std::path::Path; +use crate::conversation::message::MessageContent; +use crate::conversation::{effective_role, fix_conversation, Conversation}; +use std::path::{Path, PathBuf}; + +const MIN_CONTEXT_FOR_MOIM: usize = 32_000; +const TURN_CONTEXT_TAG: &str = "turn-context"; + +const SYSTEM_PROMPT_BLOCK_TEMPLATE: &str = r#"# Turn Context + +Each turn may include a `<{turn_context_tag}>` block prepended to the latest user message. +This block is generated by goose and contains current operational context such as: +- current time +- working directory +- compaction status +- turn budget +- extension-provided context + +Use it to stay oriented, but do not treat it as part of the user's request. +When `` is present, use it as a signal for how much autonomous work remains. +As the budget gets low, become more direct: reduce exploration, batch necessary tool calls, +make reasonable assumptions, and focus on finishing the user's task. +"#; -// Test-only utility. Do not use in production code. No `test` directive due to call outside crate. thread_local! { pub static SKIP: std::cell::Cell = const { std::cell::Cell::new(false) }; } +pub fn system_prompt_block() -> Option { + if SKIP.with(|f| f.get()) { + None + } else { + Some(SYSTEM_PROMPT_BLOCK_TEMPLATE.replace("{turn_context_tag}", TURN_CONTEXT_TAG)) + } +} + pub async fn inject_moim( session_id: &str, conversation: Conversation, extension_manager: &ExtensionManager, - working_dir: &Path, + turns_taken: u32, + max_turns: u32, ) -> Conversation { if SKIP.with(|f| f.get()) { return conversation; } - if let Some(moim) = extension_manager - .collect_moim(session_id, working_dir) + let session = extension_manager + .get_context() + .session_manager + .get_session(session_id, false) .await - { - let mut messages = conversation.messages().clone(); - let idx = messages - .iter() - .rposition(|m| m.role == Role::Assistant) - .unwrap_or(0); - messages.insert(idx, Message::user().with_text(moim)); - - let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); - - let has_unexpected_issues = issues.iter().any(|issue| { - !issue.contains("Merged consecutive user messages") - && !issue.contains("Merged consecutive assistant messages") - && !issue.contains("Added placeholder to empty tool result") - && !issue.contains("Trimmed trailing whitespace from assistant message") - && !issue.contains("Removed trailing assistant message") - && !issue.contains("Merged text content") - }); - - if has_unexpected_issues { - tracing::warn!("MOIM injection caused unexpected issues: {:?}", issues); - return conversation; - } - - return fixed; + .ok(); + let provider_context_limit = + extension_manager + .get_provider() + .try_lock() + .ok() + .and_then(|provider| { + provider + .as_ref() + .map(|provider| provider.get_model_config().context_limit()) + }); + let session_context_limit = session.as_ref().and_then(|session| { + session + .model_config + .as_ref() + .map(|config| config.context_limit()) + }); + let context_limit = provider_context_limit.or(session_context_limit); + if should_skip_moim(context_limit) { + return conversation; } - conversation + + let working_dir = session + .as_ref() + .map(|session| session.working_dir.clone()) + .unwrap_or_else(|| PathBuf::from(".")); + let total_tokens = session.as_ref().and_then(|session| session.total_tokens); + let compaction_threshold = crate::config::Config::global() + .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") + .unwrap_or(crate::context_mgmt::DEFAULT_COMPACTION_THRESHOLD); + let extension_parts = extension_manager.collect_moim_parts(session_id).await; + let moim = compose_moim( + &working_dir, + total_tokens, + context_limit, + compaction_threshold, + turns_taken, + max_turns, + extension_parts, + ); + + let mut messages = conversation.messages().clone(); + let Some(idx) = messages + .iter() + .rposition(|m| m.is_agent_visible() && effective_role(m) == "user") + else { + return conversation; + }; + let insert_idx = messages[idx] + .content + .iter() + .take_while(|content| matches!(content, MessageContent::ToolResponse(_))) + .count(); + messages[idx] + .content + .insert(insert_idx, MessageContent::text(moim)); + + let (fixed, issues) = fix_conversation(Conversation::new_unvalidated(messages)); + + let has_unexpected_issues = issues.iter().any(|issue| { + !issue.contains("Merged consecutive user messages") + && !issue.contains("Merged consecutive assistant messages") + && !issue.contains("Added placeholder to empty tool result") + && !issue.contains("Trimmed trailing whitespace from assistant message") + && !issue.contains("Removed trailing assistant message") + && !issue.contains("Merged text content") + }); + + if has_unexpected_issues { + tracing::warn!("MOIM injection caused unexpected issues: {:?}", issues); + return conversation; + } + + fixed +} + +fn should_skip_moim(context_limit: Option) -> bool { + context_limit.is_some_and(|limit| limit < MIN_CONTEXT_FOR_MOIM) +} + +fn compose_moim( + working_dir: &Path, + total_tokens: Option, + context_limit: Option, + compaction_threshold: f64, + turns_taken: u32, + max_turns: u32, + extension_parts: Vec, +) -> String { + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:00"); + let mut lines = vec![ + open_tag(TURN_CONTEXT_TAG), + tag("current-time", ×tamp.to_string()), + tag("working-directory", &working_dir.display().to_string()), + ]; + + if let Some(value) = + compaction_remaining_line(total_tokens, context_limit, compaction_threshold) + { + lines.push(tag("compaction", &value)); + } + if let Some(value) = turn_budget_line(turns_taken, max_turns) { + lines.push(tag("turn-budget", &value)); + } + + for part in extension_parts { + if !part.trim().is_empty() { + lines.push(String::new()); + lines.push(part); + } + } + + lines.push(close_tag(TURN_CONTEXT_TAG)); + lines.join("\n") +} + +fn open_tag(name: &str) -> String { + format!("<{name}>") +} + +fn close_tag(name: &str) -> String { + format!("") +} + +fn tag(name: &str, value: &str) -> String { + format!("<{name}>{}", escape_xml_text(value)) +} + +fn escape_xml_text(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +fn compaction_remaining_line( + total_tokens: Option, + context_limit: Option, + threshold: f64, +) -> Option { + let total_tokens = total_tokens?; + let context_limit = context_limit?; + + if total_tokens <= 0 || context_limit == 0 || threshold <= 0.0 || threshold >= 1.0 { + return None; + } + + let compaction_at = (context_limit as f64 * threshold) as i32; + if compaction_at <= 0 || (total_tokens as f64 / compaction_at as f64) < 0.5 { + return None; + } + + Some(format!( + "~{}k tokens remaining", + compaction_at.saturating_sub(total_tokens) / 1000 + )) +} + +fn turn_budget_line(turns_taken: u32, max_turns: u32) -> Option { + if max_turns == 0 || turns_taken.saturating_mul(2) < max_turns { + return None; + } + + Some(format!("{turns_taken}/{max_turns} used")) } #[cfg(test)] mod tests { use super::*; + use crate::conversation::message::Message; use rmcp::model::CallToolRequestParams; - use std::path::PathBuf; + + fn text_at(message: &crate::conversation::message::Message, index: usize) -> &str { + message.content[index].as_text().unwrap() + } + + fn is_moim(content: &MessageContent) -> bool { + content + .as_text() + .is_some_and(|text| text.starts_with(&format!("<{}>\n", TURN_CONTEXT_TAG))) + } #[tokio::test] - async fn test_moim_injection_before_assistant() { + async fn test_moim_prepended_to_latest_user_message() { let temp_dir = tempfile::tempdir().unwrap(); let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf()); - let working_dir = PathBuf::from("/test/dir"); + let session = em + .get_context() + .session_manager + .create_session( + PathBuf::from("/test/dir"), + "test".to_string(), + crate::session::SessionType::User, + crate::config::GooseMode::Auto, + ) + .await + .unwrap(); let conv = Conversation::new_unvalidated(vec![ Message::user().with_text("Hello"), Message::assistant().with_text("Hi"), Message::user().with_text("Bye"), ]); - let result = inject_moim("test-session-id", conv, &em, &working_dir).await; + let result = inject_moim(&session.id, conv, &em, 0, 100).await; let msgs = result.messages(); assert_eq!(msgs.len(), 3); - assert_eq!(msgs[0].content[0].as_text().unwrap(), "Hello"); - assert_eq!(msgs[1].content[0].as_text().unwrap(), "Hi"); - - let merged_content = msgs[0] - .content - .iter() - .filter_map(|c| c.as_text()) - .collect::>() - .join(""); - assert!(merged_content.contains("Hello")); - assert!(merged_content.contains("")); - assert!(merged_content.contains("Working directory: /test/dir")); + assert_eq!(text_at(&msgs[0], 0), "Hello"); + assert_eq!(text_at(&msgs[1], 0), "Hi"); + assert!(is_moim(&msgs[2].content[0])); + assert_eq!(text_at(&msgs[2], 1), "Bye"); } #[tokio::test] async fn test_moim_injection_no_assistant() { let temp_dir = tempfile::tempdir().unwrap(); let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf()); - let working_dir = PathBuf::from("/test/dir"); + let session = em + .get_context() + .session_manager + .create_session( + PathBuf::from("/test/dir"), + "test".to_string(), + crate::session::SessionType::User, + crate::config::GooseMode::Auto, + ) + .await + .unwrap(); let conv = Conversation::new_unvalidated(vec![Message::user().with_text("Hello")]); - let result = inject_moim("test-session-id", conv, &em, &working_dir).await; + let result = inject_moim(&session.id, conv, &em, 0, 100).await; assert_eq!(result.messages().len(), 1); + assert!(is_moim(&result.messages()[0].content[0])); + assert_eq!(text_at(&result.messages()[0], 1), "Hello"); + } - let merged_content = result.messages()[0] - .content - .iter() - .filter_map(|c| c.as_text()) - .collect::>() - .join(""); - assert!(merged_content.contains("Hello")); - assert!(merged_content.contains("")); - assert!(merged_content.contains("Working directory: /test/dir")); + #[tokio::test] + async fn test_moim_skips_user_messages_not_visible_to_agent() { + let temp_dir = tempfile::tempdir().unwrap(); + let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf()); + let session = em + .get_context() + .session_manager + .create_session( + PathBuf::from("/test/dir"), + "test".to_string(), + crate::session::SessionType::User, + crate::config::GooseMode::Auto, + ) + .await + .unwrap(); + + let conv = Conversation::new_unvalidated(vec![ + Message::user().with_text("agent visible"), + Message::assistant().with_text("reply"), + Message::user().with_text("user only").user_only(), + ]); + let result = inject_moim(&session.id, conv, &em, 0, 100).await; + let msgs = result.messages(); + + assert_eq!(msgs.len(), 2); + assert!(is_moim(&msgs[0].content[0])); + assert_eq!(text_at(&msgs[0], 1), "agent visible"); + assert_eq!(text_at(&msgs[1], 0), "user only"); + assert!(!msgs[1].is_agent_visible()); } #[tokio::test] async fn test_moim_with_tool_calls() { let temp_dir = tempfile::tempdir().unwrap(); let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf()); - let working_dir = PathBuf::from("/test/dir"); + let session = em + .get_context() + .session_manager + .create_session( + PathBuf::from("/test/dir"), + "test".to_string(), + crate::session::SessionType::User, + crate::config::GooseMode::Auto, + ) + .await + .unwrap(); let conv = Conversation::new_unvalidated(vec![ Message::user().with_text("Search for something"), @@ -121,27 +341,18 @@ mod tests { .with_tool_request("search_1", Ok(CallToolRequestParams::new("search"))), Message::user() .with_tool_response("search_1", Ok(rmcp::model::CallToolResult::success(vec![]))), - Message::assistant() - .with_text("I need to search more") - .with_tool_request("search_2", Ok(CallToolRequestParams::new("search"))), - Message::user() - .with_tool_response("search_2", Ok(rmcp::model::CallToolResult::success(vec![]))), ]); - let result = inject_moim("test-session-id", conv, &em, &working_dir).await; + let result = inject_moim(&session.id, conv, &em, 0, 100).await; let msgs = result.messages(); - assert_eq!(msgs.len(), 6); - - let moim_msg = &msgs[3]; - let has_moim = moim_msg - .content - .iter() - .any(|c| c.as_text().is_some_and(|t| t.contains(""))); - - assert!( - has_moim, - "MOIM should be in message before latest assistant message" - ); + assert_eq!(msgs.len(), 3); + assert!(is_moim(&msgs[0].content[0])); + assert_eq!(text_at(&msgs[0], 1), "Search for something"); + assert!(matches!( + &msgs[2].content[0], + MessageContent::ToolResponse(_) + )); + assert_eq!(msgs[2].content.len(), 1); } } diff --git a/crates/goose/src/agents/platform_extensions/code_execution.rs b/crates/goose/src/agents/platform_extensions/code_execution.rs index 6fc50fcbf..935b51f2f 100644 --- a/crates/goose/src/agents/platform_extensions/code_execution.rs +++ b/crates/goose/src/agents/platform_extensions/code_execution.rs @@ -478,21 +478,8 @@ impl McpClientTrait for CodeExecutionClient { let disclosure_style_moim = match self.disclosure { ToolDisclosure::Catalog => { - let functions = code_mode.list_functions().functions; - let sandbox_only: Vec<_> = functions - .iter() - .filter(|f| !crate::agents::extension_manager::is_first_class_extension(&f.namespace)) - .map(|f| format!("{}.{}", &f.namespace, &f.name)) - .collect(); - let mut msg = String::new(); - if !sandbox_only.is_empty() { - msg.push_str(&format!( - "Additional functions available ONLY via execute_typescript (do NOT call these as direct tool calls): {}", - sandbox_only.join(", ") - )); - } - msg.push_str("\n\n Use the list_functions & get_function_details tools to see tool signatures and input/output types before calling execute_typescript."); - msg + let function_count = code_mode.list_functions().functions.len(); + catalog_disclosure_moim(function_count) } ToolDisclosure::Filesystem => { let available_filepaths: Vec<_> = code_mode @@ -515,6 +502,16 @@ impl McpClientTrait for CodeExecutionClient { } } +fn catalog_disclosure_moim(function_count: usize) -> String { + if function_count == 0 { + "No execute_typescript callback functions are currently registered.".to_string() + } else { + format!( + "{function_count} callback functions are available only from inside execute_typescript. Do not call callback function names directly as tools. Use list_functions and get_function_details to inspect signatures before writing one execute_typescript call." + ) + } +} + pub fn get_tool_disclosure() -> ToolDisclosure { let config = crate::config::Config::global(); let tool_disclosure_str: String = config @@ -554,3 +551,19 @@ impl CodeModeState { hasher.finish() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn catalog_moim_mentions_inspection_tools_without_function_names() { + let moim = catalog_disclosure_moim(3); + + assert!(moim.contains("3 callback functions")); + assert!(moim.contains("list_functions")); + assert!(moim.contains("get_function_details")); + assert!(!moim.contains("extract_relations")); + assert!(!moim.contains("ask_heimdall")); + } +} diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 09b03212a..810d7e673 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -6,7 +6,7 @@ use serde::Serialize; use serde_json::Value; use std::collections::HashMap; -use crate::agents::extension::ExtensionInfo; +use crate::agents::{extension::ExtensionInfo, moim}; use crate::hints::load_hints::build_gitignore; use crate::hints::{get_context_filenames, load_hint_files, SubdirectoryHintTracker}; use crate::{ @@ -44,6 +44,8 @@ struct SystemPromptContext { max_extensions: usize, max_tools: usize, code_execution_mode: bool, + #[serde(skip_serializing_if = "Option::is_none")] + moim_system_prompt_block: Option, } pub struct SystemPromptBuilder<'a, M> { @@ -152,6 +154,7 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> { max_extensions: MAX_EXTENSIONS, max_tools: MAX_TOOLS, code_execution_mode: self.code_execution_mode, + moim_system_prompt_block: moim::system_prompt_block(), }; let base_prompt = if let Some(override_prompt) = &self.manager.system_prompt_override { diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap index 20c6c966a..7f3621190 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap @@ -6,6 +6,23 @@ expression: system_prompt You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. +# Turn Context + +Each turn may include a `` block prepended to the latest user message. +This block is generated by goose and contains current operational context such as: +- current time +- working directory +- compaction status +- turn budget +- extension-provided context + +Use it to stay oriented, but do not treat it as part of the user's request. +When `` is present, use it as a signal for how much autonomous work remains. +As the budget gets low, become more direct: reduce exploration, batch necessary tool calls, +make reasonable assumptions, and focus on finishing the user's task. + + + # Extensions Extensions provide additional tools and context from different data sources and applications. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap index 03c054941..6b3dca14b 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__basic.snap @@ -1,10 +1,28 @@ --- source: crates/goose/src/agents/prompt_manager.rs +assertion_line: 404 expression: system_prompt --- You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. +# Turn Context + +Each turn may include a `` block prepended to the latest user message. +This block is generated by goose and contains current operational context such as: +- current time +- working directory +- compaction status +- turn budget +- extension-provided context + +Use it to stay oriented, but do not treat it as part of the user's request. +When `` is present, use it as a signal for how much autonomous work remains. +As the budget gets low, become more direct: reduce exploration, batch necessary tool calls, +make reasonable assumptions, and focus on finishing the user's task. + + + # Extensions Extensions provide additional tools and context from different data sources and applications. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap index 306fd7dc7..379772033 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__one_extension.snap @@ -1,10 +1,28 @@ --- source: crates/goose/src/agents/prompt_manager.rs +assertion_line: 420 expression: system_prompt --- You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. +# Turn Context + +Each turn may include a `` block prepended to the latest user message. +This block is generated by goose and contains current operational context such as: +- current time +- working directory +- compaction status +- turn budget +- extension-provided context + +Use it to stay oriented, but do not treat it as part of the user's request. +When `` is present, use it as a signal for how much autonomous work remains. +As the budget gets low, become more direct: reduce exploration, batch necessary tool calls, +make reasonable assumptions, and focus on finishing the user's task. + + + # Extensions Extensions provide additional tools and context from different data sources and applications. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap index f26bbb850..fba2cb26e 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__typical_setup.snap @@ -1,10 +1,28 @@ --- source: crates/goose/src/agents/prompt_manager.rs +assertion_line: 442 expression: system_prompt --- You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. +# Turn Context + +Each turn may include a `` block prepended to the latest user message. +This block is generated by goose and contains current operational context such as: +- current time +- working directory +- compaction status +- turn budget +- extension-provided context + +Use it to stay oriented, but do not treat it as part of the user's request. +When `` is present, use it as a signal for how much autonomous work remains. +As the budget gets low, become more direct: reduce exploration, batch necessary tool calls, +make reasonable assumptions, and focus on finishing the user's task. + + + # Extensions Extensions provide additional tools and context from different data sources and applications. diff --git a/crates/goose/src/prompts/system.md b/crates/goose/src/prompts/system.md index aa9d0ae8b..c25de23dc 100644 --- a/crates/goose/src/prompts/system.md +++ b/crates/goose/src/prompts/system.md @@ -1,5 +1,10 @@ You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation). goose is being developed as an open-source software project. + +{% if moim_system_prompt_block is defined %} +{{ moim_system_prompt_block }} +{% endif %} + {% if not code_execution_mode %} # Extensions diff --git a/crates/goose/tests/acp_common_tests/mod.rs b/crates/goose/tests/acp_common_tests/mod.rs index d2af23d07..94536a283 100644 --- a/crates/goose/tests/acp_common_tests/mod.rs +++ b/crates/goose/tests/acp_common_tests/mod.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use std::time::Duration; const SHELL_TEST_CONTENT: &str = "test-shell-content-98765"; +const TURN_CONTEXT_CLOSE: &str = r#"\n"#; const OPENAI_SESSION_NAME_RESPONSE: &str = r#"data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":1766229303,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"id":"chatcmpl-test","object":"chat.completion.chunk","created":1766229303,"model":"gpt-5-nano","choices":[{"index":0,"delta":{"content":"Generated Test Title"},"finish_reason":null}]} @@ -41,7 +42,7 @@ async fn new_basic_session(config: TestConnectionConfig) -> Basic let expected_session_id = C::expected_session_id(); let openai = OpenAiFixture::new( vec![( - r#"\nwhat is 1+1""#.into(), + format!("{TURN_CONTEXT_CLOSE}what is 1+1"), include_str!("../acp_test_data/openai_basic.txt"), )], expected_session_id.clone(), @@ -99,7 +100,7 @@ pub async fn run_session_name_update_notification() { let openai = OpenAiFixture::new( vec![ ( - r#"\nwhat should we call this conversation?""#.into(), + format!("{TURN_CONTEXT_CLOSE}what should we call this conversation?"), include_str!("../acp_test_data/openai_basic.txt"), ), ( @@ -1140,7 +1141,7 @@ pub async fn run_prompt_basic() { let expected_session_id = C::expected_session_id(); let openai = OpenAiFixture::new( vec![( - r#"\nwhat is 1+1""#.into(), + format!("{TURN_CONTEXT_CLOSE}what is 1+1"), include_str!("../acp_test_data/openai_basic.txt"), )], expected_session_id.clone(), @@ -1168,7 +1169,7 @@ pub async fn run_prompt_codemode() { let openai = OpenAiFixture::new( vec![ ( - format!(r#"\n{prompt}""#), + format!("{TURN_CONTEXT_CLOSE}{prompt}"), include_str!("../acp_test_data/openai_builtin_search.txt"), ), ( @@ -1215,8 +1216,9 @@ pub async fn run_prompt_image() { let openai = OpenAiFixture::new( vec![ ( - r#"\nUse the get_image tool and describe what you see in its result.""# - .into(), + format!( + "{TURN_CONTEXT_CLOSE}Use the get_image tool and describe what you see in its result." + ), include_str!("../acp_test_data/openai_image_tool_call.txt"), ), ( @@ -1291,7 +1293,7 @@ pub async fn run_prompt_mcp() { let openai = OpenAiFixture::new( vec![ ( - r#"\nUse the get_code tool and output only its result.""#.into(), + format!("{TURN_CONTEXT_CLOSE}Use the get_code tool and output only its result."), include_str!("../acp_test_data/openai_tool_call.txt"), ), (