Update Moim (#9636)
Signed-off-by: Douwe M Osinga <douwe@sidewalklabs.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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<String> {
|
||||
// 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!(
|
||||
"<info-msg>\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<String> {
|
||||
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</info-msg>");
|
||||
|
||||
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();
|
||||
|
||||
+291
-80
@@ -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 `<turn-budget>` 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<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
pub fn system_prompt_block() -> Option<String> {
|
||||
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::<f64>("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<usize>) -> bool {
|
||||
context_limit.is_some_and(|limit| limit < MIN_CONTEXT_FOR_MOIM)
|
||||
}
|
||||
|
||||
fn compose_moim(
|
||||
working_dir: &Path,
|
||||
total_tokens: Option<i32>,
|
||||
context_limit: Option<usize>,
|
||||
compaction_threshold: f64,
|
||||
turns_taken: u32,
|
||||
max_turns: u32,
|
||||
extension_parts: Vec<String>,
|
||||
) -> 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!("</{name}>")
|
||||
}
|
||||
|
||||
fn tag(name: &str, value: &str) -> String {
|
||||
format!("<{name}>{}</{name}>", escape_xml_text(value))
|
||||
}
|
||||
|
||||
fn escape_xml_text(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn compaction_remaining_line(
|
||||
total_tokens: Option<i32>,
|
||||
context_limit: Option<usize>,
|
||||
threshold: f64,
|
||||
) -> Option<String> {
|
||||
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<String> {
|
||||
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::<Vec<_>>()
|
||||
.join("");
|
||||
assert!(merged_content.contains("Hello"));
|
||||
assert!(merged_content.contains("<info-msg>"));
|
||||
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::<Vec<_>>()
|
||||
.join("");
|
||||
assert!(merged_content.contains("Hello"));
|
||||
assert!(merged_content.contains("<info-msg>"));
|
||||
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("<info-msg>")));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
+17
@@ -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 `<turn-context>` 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 `<turn-budget>` 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.
|
||||
|
||||
@@ -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 `<turn-context>` 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 `<turn-budget>` 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.
|
||||
|
||||
+18
@@ -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 `<turn-context>` 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 `<turn-budget>` 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.
|
||||
|
||||
+18
@@ -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 `<turn-context>` 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 `<turn-budget>` 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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#"</turn-context>\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<C: Connection>(config: TestConnectionConfig) -> Basic
|
||||
let expected_session_id = C::expected_session_id();
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![(
|
||||
r#"</info-msg>\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<C: Connection>() {
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![
|
||||
(
|
||||
r#"</info-msg>\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<C: Connection>() {
|
||||
let expected_session_id = C::expected_session_id();
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![(
|
||||
r#"</info-msg>\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<C: Connection>() {
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![
|
||||
(
|
||||
format!(r#"</info-msg>\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<C: Connection>() {
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![
|
||||
(
|
||||
r#"</info-msg>\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<C: Connection>() {
|
||||
let openai = OpenAiFixture::new(
|
||||
vec![
|
||||
(
|
||||
r#"</info-msg>\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"),
|
||||
),
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user