Some system prompt tidying (#5313)
Some checks failed
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
CI / bundle-desktop-unsigned (push) Blocked by required conditions
Documentation Site Preview / deploy (push) Waiting to run
Canary / Prepare Version (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Release (push) Has been cancelled

This commit is contained in:
Jack Amadeo 2025-10-24 20:22:27 -04:00 committed by GitHub
parent 774baf1cdb
commit 97743fd1b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 285 additions and 184 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
crates/goose/src/agents/snapshots/*.snap linguist-language=Text

View file

@ -1369,6 +1369,8 @@ impl Agent {
let extensions_info = self.extension_manager.get_extensions_info().await;
tracing::debug!("Retrieved {} extensions info", extensions_info.len());
let (extension_count, tool_count) =
self.extension_manager.get_extension_and_tool_counts().await;
// Get model name from provider
let provider = self.provider().await.map_err(|e| {
@ -1380,15 +1382,12 @@ impl Agent {
tracing::debug!("Using model: {}", model_name);
let prompt_manager = self.prompt_manager.lock().await;
let system_prompt = prompt_manager.build_system_prompt(
extensions_info,
self.frontend_instructions.lock().await.clone(),
self.extension_manager
.suggest_disable_extensions_prompt()
.await,
model_name,
false,
);
let system_prompt = prompt_manager
.builder(model_name)
.with_extensions(extensions_info.into_iter())
.with_frontend_instructions(self.frontend_instructions.lock().await.clone())
.with_extension_and_tool_counts(extension_count, tool_count)
.build();
let recipe_prompt = prompt_manager.get_recipe_prompt().await;
let tools = self
@ -1612,8 +1611,7 @@ mod tests {
);
let prompt_manager = agent.prompt_manager.lock().await;
let system_prompt =
prompt_manager.build_system_prompt(vec![], None, Value::Null, "gpt-4o", false);
let system_prompt = prompt_manager.builder("gpt-4o").build();
let final_output_tool_ref = agent.final_output_tool.lock().await;
let final_output_tool_system_prompt =

View file

@ -549,7 +549,7 @@ impl ExtensionManager {
Ok(())
}
pub async fn suggest_disable_extensions_prompt(&self) -> Value {
pub async fn get_extension_and_tool_counts(&self) -> (usize, usize) {
let enabled_extensions_count = self.extensions.lock().await.len();
let total_tools = self
@ -558,27 +558,7 @@ impl ExtensionManager {
.map(|tools| tools.len())
.unwrap_or(0);
// Check if either condition is met
const MIN_EXTENSIONS: usize = 5;
const MIN_TOOLS: usize = 50;
if enabled_extensions_count > MIN_EXTENSIONS || total_tools > MIN_TOOLS {
Value::String(format!(
"The user currently has enabled {} extensions with a total of {} tools. \
Since this exceeds the recommended limits ({} extensions or {} tools), \
you should ask the user if they would like to disable some extensions for this session.\n\n\
Use the search_available_extensions tool to find extensions available to disable. \
You should only disable extensions found from the search_available_extensions tool. \
List all the extensions available to disable in the response. \
Explain that minimizing extensions helps with the recall of the correct tools to use.",
enabled_extensions_count,
total_tools,
MIN_EXTENSIONS,
MIN_TOOLS,
))
} else {
Value::String(String::new()) // Empty string if under limits
}
(enabled_extensions_count, total_tools)
}
pub async fn list_extensions(&self) -> ExtensionResult<Vec<String>> {

View file

@ -1,7 +1,9 @@
#[cfg(test)]
use chrono::DateTime;
use chrono::Utc;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use crate::agents::extension::ExtensionInfo;
@ -9,6 +11,9 @@ use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents;
use crate::agents::router_tools::llm_search_tool_prompt;
use crate::{config::Config, prompt_template, utils::sanitize_unicode_tags};
const MAX_EXTENSIONS: usize = 5;
const MAX_TOOLS: usize = 50;
pub struct PromptManager {
system_prompt_override: Option<String>,
system_prompt_extras: Vec<String>,
@ -21,50 +26,68 @@ impl Default for PromptManager {
}
}
impl PromptManager {
pub fn new() -> Self {
PromptManager {
system_prompt_override: None,
system_prompt_extras: Vec::new(),
// Use the fixed current date time so that prompt cache can be used.
// Filtering to an hour to balance user time accuracy and multi session prompt cache hits.
current_date_timestamp: Utc::now().format("%Y-%m-%d %H:00").to_string(),
}
}
#[derive(Serialize)]
struct SystemPromptContext {
extensions: Vec<ExtensionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_selection_strategy: Option<String>,
current_date_time: String,
#[serde(skip_serializing_if = "Option::is_none")]
extension_tool_limits: Option<(usize, usize)>,
goose_mode: String,
is_autonomous: bool,
enable_subagents: bool,
max_extensions: usize,
max_tools: usize,
}
#[cfg(test)]
pub fn with_timestamp(dt: DateTime<Utc>) -> Self {
PromptManager {
system_prompt_override: None,
system_prompt_extras: Vec::new(),
// Use the fixed current date time so that prompt cache can be used.
current_date_timestamp: dt.format("%Y-%m-%d %H:%M:%S").to_string(),
}
}
pub struct SystemPromptBuilder<'a, M> {
model_name: String,
manager: &'a M,
/// Add an additional instruction to the system prompt
pub fn add_system_prompt_extra(&mut self, instruction: String) {
self.system_prompt_extras.push(instruction);
}
/// Override the system prompt with custom text
pub fn set_system_prompt_override(&mut self, template: String) {
self.system_prompt_override = Some(template);
}
pub fn build_system_prompt(
&self,
extensions_info: Vec<ExtensionInfo>,
frontend_instructions: Option<String>,
suggest_disable_extensions_prompt: Value,
model_name: &str,
extension_tool_count: Option<(usize, usize)>,
router_enabled: bool,
) -> String {
let mut context: HashMap<&str, Value> = HashMap::new();
let mut extensions_info = extensions_info.clone();
}
impl<'a> SystemPromptBuilder<'a, PromptManager> {
pub fn with_extension(mut self, extension: ExtensionInfo) -> Self {
self.extensions_info.push(extension);
self
}
pub fn with_extensions(mut self, extensions: impl Iterator<Item = ExtensionInfo>) -> Self {
for extension in extensions {
self.extensions_info.push(extension);
}
self
}
pub fn with_frontend_instructions(mut self, frontend_instructions: Option<String>) -> Self {
self.frontend_instructions = frontend_instructions;
self
}
pub fn with_extension_and_tool_counts(
mut self,
extension_count: usize,
tool_count: usize,
) -> Self {
self.extension_tool_count = Some((extension_count, tool_count));
self
}
pub fn with_router_enabled(mut self, enabled: bool) -> Self {
self.router_enabled = enabled;
self
}
pub fn build(self) -> String {
let mut extensions_info = self.extensions_info;
// Add frontend instructions to extensions_info to simplify json rendering
if let Some(frontend_instructions) = frontend_instructions {
if let Some(frontend_instructions) = self.frontend_instructions {
extensions_info.push(ExtensionInfo::new(
"frontend",
&frontend_instructions,
@ -82,38 +105,28 @@ impl PromptManager {
})
.collect();
context.insert(
"extensions",
serde_json::to_value(sanitized_extensions_info).unwrap(),
);
if router_enabled {
context.insert(
"tool_selection_strategy",
Value::String(llm_search_tool_prompt()),
);
}
context.insert(
"current_date_time",
Value::String(self.current_date_timestamp.clone()),
);
// Add the suggestion about disabling extensions if flag is true
context.insert(
"suggest_disable",
Value::String(suggest_disable_extensions_prompt.to_string()),
);
let config = Config::global();
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
context.insert("goose_mode", Value::String(goose_mode.clone()));
context.insert(
"enable_subagents",
Value::Bool(should_enabled_subagents(model_name)),
);
let goose_mode = config
.get_param("GOOSE_MODE")
.unwrap_or_else(|_| Cow::from("auto"));
let base_prompt = if let Some(override_prompt) = &self.system_prompt_override {
let extension_tool_limits = self
.extension_tool_count
.filter(|(extensions, tools)| *extensions > MAX_EXTENSIONS || *tools > MAX_TOOLS);
let context = SystemPromptContext {
extensions: sanitized_extensions_info,
tool_selection_strategy: self.router_enabled.then(llm_search_tool_prompt),
current_date_time: self.manager.current_date_timestamp.clone(),
extension_tool_limits,
goose_mode: goose_mode.to_string(),
is_autonomous: goose_mode == "auto",
enable_subagents: should_enabled_subagents(self.model_name.as_str()),
max_extensions: MAX_EXTENSIONS,
max_tools: MAX_TOOLS,
};
let base_prompt = if let Some(override_prompt) = &self.manager.system_prompt_override {
let sanitized_override_prompt = sanitize_unicode_tags(override_prompt);
prompt_template::render_inline_once(&sanitized_override_prompt, &context)
} else {
@ -123,7 +136,7 @@ impl PromptManager {
"You are a general-purpose AI agent called goose, created by Block".to_string()
});
let mut system_prompt_extras = self.system_prompt_extras.clone();
let mut system_prompt_extras = self.manager.system_prompt_extras.clone();
if goose_mode == "chat" {
system_prompt_extras.push(
"Right now you are in the chat only mode, no access to any tool use and system."
@ -146,6 +159,49 @@ impl PromptManager {
)
}
}
}
impl PromptManager {
pub fn new() -> Self {
PromptManager {
system_prompt_override: None,
system_prompt_extras: Vec::new(),
// Use the fixed current date time so that prompt cache can be used.
// Filtering to an hour to balance user time accuracy and multi session prompt cache hits.
current_date_timestamp: Utc::now().format("%Y-%m-%d %H:00").to_string(),
}
}
#[cfg(test)]
pub fn with_timestamp(dt: DateTime<Utc>) -> Self {
PromptManager {
system_prompt_override: None,
system_prompt_extras: Vec::new(),
current_date_timestamp: dt.format("%Y-%m-%d %H:%M:%S").to_string(),
}
}
/// Add an additional instruction to the system prompt
pub fn add_system_prompt_extra(&mut self, instruction: String) {
self.system_prompt_extras.push(instruction);
}
/// Override the system prompt with custom text
pub fn set_system_prompt_override(&mut self, template: String) {
self.system_prompt_override = Some(template);
}
pub fn builder<'a>(&'a self, model_name: &str) -> SystemPromptBuilder<'a, Self> {
SystemPromptBuilder {
model_name: model_name.to_string(),
manager: self,
extensions_info: vec![],
frontend_instructions: None,
extension_tool_count: None,
router_enabled: false,
}
}
pub async fn get_recipe_prompt(&self) -> String {
let context: HashMap<&str, Value> = HashMap::new();
@ -166,13 +222,7 @@ mod tests {
let malicious_override = "System prompt\u{E0041}\u{E0042}\u{E0043}with hidden text";
manager.set_system_prompt_override(malicious_override.to_string());
let result = manager.build_system_prompt(
vec![],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let result = manager.builder("gpt-4o").build();
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@ -187,13 +237,7 @@ mod tests {
let malicious_extra = "Extra instruction\u{E0041}\u{E0042}\u{E0043}hidden";
manager.add_system_prompt_extra(malicious_extra.to_string());
let result = manager.build_system_prompt(
vec![],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let result = manager.builder("gpt-4o").build();
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@ -209,13 +253,7 @@ mod tests {
manager.add_system_prompt_extra("Second\u{E0042}instruction".to_string());
manager.add_system_prompt_extra("Third\u{E0043}instruction".to_string());
let result = manager.build_system_prompt(
vec![],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let result = manager.builder("gpt-4o").build();
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@ -231,13 +269,7 @@ mod tests {
let legitimate_unicode = "Instruction with 世界 and 🌍 emojis";
manager.add_system_prompt_extra(legitimate_unicode.to_string());
let result = manager.build_system_prompt(
vec![],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let result = manager.builder("gpt-4o").build();
assert!(result.contains("世界"));
assert!(result.contains("🌍"));
@ -254,13 +286,10 @@ mod tests {
false,
);
let result = manager.build_system_prompt(
vec![malicious_extension_info],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let result = manager
.builder("gpt-4o")
.with_extension(malicious_extension_info)
.build();
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@ -273,13 +302,7 @@ mod tests {
fn test_basic() {
let manager = PromptManager::with_timestamp(DateTime::<Utc>::from_timestamp(0, 0).unwrap());
let system_prompt = manager.build_system_prompt(
vec![],
None,
Value::String("".to_string()),
"gpt-4o",
false,
);
let system_prompt = manager.builder("gpt-4o").build();
assert_snapshot!(system_prompt)
}
@ -288,17 +311,38 @@ mod tests {
fn test_one_extension() {
let manager = PromptManager::with_timestamp(DateTime::<Utc>::from_timestamp(0, 0).unwrap());
let system_prompt = manager.build_system_prompt(
vec![ExtensionInfo::new(
let system_prompt = manager
.builder("gpt-4o")
.with_extension(ExtensionInfo::new(
"test",
"how to use this extension",
true,
)],
None,
Value::String("".to_string()),
"gpt-4o",
))
.with_router_enabled(true)
.build();
assert_snapshot!(system_prompt)
}
#[test]
fn test_typical_setup() {
let manager = PromptManager::with_timestamp(DateTime::<Utc>::from_timestamp(0, 0).unwrap());
let system_prompt = manager
.builder("gpt-4o")
.with_extension(ExtensionInfo::new(
"extension_A",
"<instructions on how to use extension A>",
true,
);
))
.with_extension(ExtensionInfo::new(
"extension_B",
"<instructions on how to use extension B (no resources)>",
false,
))
.with_router_enabled(true)
.with_extension_and_tool_counts(MAX_EXTENSIONS + 1, MAX_TOOLS + 1)
.build();
assert_snapshot!(system_prompt)
}

View file

@ -67,6 +67,8 @@ impl Agent {
// Prepare system prompt
let extensions_info = self.extension_manager.get_extensions_info().await;
let (extension_count, tool_count) =
self.extension_manager.get_extension_and_tool_counts().await;
// Get model name from provider
let provider = self.provider().await?;
@ -74,15 +76,13 @@ impl Agent {
let model_name = &model_config.model_name;
let prompt_manager = self.prompt_manager.lock().await;
let mut system_prompt = prompt_manager.build_system_prompt(
extensions_info,
self.frontend_instructions.lock().await.clone(),
self.extension_manager
.suggest_disable_extensions_prompt()
.await,
model_name,
router_enabled,
);
let mut system_prompt = prompt_manager
.builder(model_name)
.with_extensions(extensions_info.into_iter())
.with_frontend_instructions(self.frontend_instructions.lock().await.clone())
.with_extension_and_tool_counts(extension_count, tool_count)
.with_router_enabled(router_enabled)
.build();
// Handle toolshim if enabled
let mut toolshim_tools = vec![];

View file

@ -25,20 +25,11 @@ extension_name. You should only enable extensions found from the search_availabl
If Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load
new ones.
No extensions are defined. You should let the user know that they should add extensions.
# Suggestion
""
# sub agents
Execute self contained tasks where step-by-step visibility is not important through subagents.
@ -51,7 +42,6 @@ Execute self contained tasks where step-by-step visibility is not important thro
- Use extension filters to limit resource access
- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice.
# Response Guidelines
- Use Markdown formatting for all responses.

View file

@ -25,33 +25,20 @@ extension_name. You should only enable extensions found from the search_availabl
If Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load
new ones.
Because you dynamically load extensions, your conversation history may refer
to interactions with extensions that are not currently active. The currently
active extensions are below. Each of these extensions provides tools that are
in your tool specification.
## test
test supports resources, you can use platform__read_resource,
and platform__list_resources on this extension.
### Instructions
how to use this extension
# Suggestion
""
# LLM Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the llm search tool to actually retrieve the most relevant tools to use according to the user's messages.
@ -67,7 +54,6 @@ how to use this extension
- list_resources
# sub agents
Execute self contained tasks where step-by-step visibility is not important through subagents.
@ -80,7 +66,6 @@ Execute self contained tasks where step-by-step visibility is not important thro
- Use extension filters to limit resource access
- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice.
# Response Guidelines
- Use Markdown formatting for all responses.

View file

@ -0,0 +1,93 @@
---
source: crates/goose/src/agents/prompt_manager.rs
expression: system_prompt
---
You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.
goose is being developed as an open-source software project.
The current date is 1970-01-01 00:00:00.
goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,
claude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).
These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10
months prior to the current date.
# Extensions
Extensions allow other applications to provide context to goose. Extensions connect goose to different data sources and
tools.
You are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level
problems using the tools in these extensions, and can interact with multiple at once.
If the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional
extensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the
extension_name. You should only enable extensions found from the search_available_extensions tool.
If Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load
new ones.
Because you dynamically load extensions, your conversation history may refer
to interactions with extensions that are not currently active. The currently
active extensions are below. Each of these extensions provides tools that are
in your tool specification.
## extension_A
extension_A supports resources, you can use platform__read_resource,
and platform__list_resources on this extension.
### Instructions
<instructions on how to use extension A>
## extension_B
### Instructions
<instructions on how to use extension B (no resources)>
# Suggestion
The user currently has enabled 6 extensions with a total of 51 tools.
Since this exceeds the recommended limits (5 extensions or 50 tools),
you should ask the user if they would like to disable some extensions for this session.
Use the search_available_extensions tool to find extensions available to disable.
You should only disable extensions found from the search_available_extensions tool.
List all the extensions available to disable in the response.
Explain that minimizing extensions helps with the recall of the correct tools to use.
# LLM Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the llm search tool to actually retrieve the most relevant tools to use according to the user's messages.
For example, if the user has 3 extensions enabled, but they are asking for a tool to read a pdf file, \
you would invoke the llm_search tool to find the most relevant read pdf tool.
By dynamically enabling tools, you (goose) as the agent save context window space and allow the user to dynamically retrieve the most relevant tools.
Be sure to format a query packed with relevant keywords to search for the most relevant tools.
In addition to the extension names available to you, you also have platform extension tools available to you.
The platform extension contains the following tools:
- search_available_extensions
- manage_extensions
- read_resource
- list_resources
# sub agents
Execute self contained tasks where step-by-step visibility is not important through subagents.
- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests,
verification, exploration
- Parallel subagents for multiple operations, single subagents for independent work
- Explore solutions in parallel — launch parallel subagents with different approaches (if non-interfering)
- Provide all needed context — subagents cannot see your context
- Use extension filters to limit resource access
- Use return_last_only when only a summary or simple answer is required — inform subagent of this choice.
# Response Guidelines
- Use Markdown formatting for all responses.
- Follow best practices for Markdown, including:
- Using headers for organization.
- Bullet points for lists.
- Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic
links using angle brackets (e.g., <http://example.com/>).
- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the
language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting.
- Ensure clarity, conciseness, and proper formatting to enhance readability and usability.

View file

@ -16,6 +16,8 @@ static CORE_PROMPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/prompts");
/// - *Not* used for extension prompts (which are ephemeral).
static GLOBAL_ENV: Lazy<Arc<RwLock<Environment<'static>>>> = Lazy::new(|| {
let mut env = Environment::new();
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
// Pre-load all core templates from the embedded dir.
for file in CORE_PROMPTS_DIR.files() {

View file

@ -43,11 +43,19 @@ and platform__list_resources on this extension.
No extensions are defined. You should let the user know that they should add extensions.
{% endif %}
{% if suggest_disable is defined %}
{% if extension_tool_limits is defined %}
{% with (extension_count, tool_count) = extension_tool_limits %}
# Suggestion
{{suggest_disable}}
The user currently has enabled {{extension_count}} extensions with a total of {{tool_count}} tools.
Since this exceeds the recommended limits ({{max_extensions}} extensions or {{max_tools}} tools),
you should ask the user if they would like to disable some extensions for this session.
Use the search_available_extensions tool to find extensions available to disable.
You should only disable extensions found from the search_available_extensions tool.
List all the extensions available to disable in the response.
Explain that minimizing extensions helps with the recall of the correct tools to use.
{% endwith %}
{% endif %}
{{tool_selection_strategy}}