Platform extensions sketch (#4868)
Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -61,12 +61,8 @@ use super::model_selector::autopilot::AutoPilot;
|
||||
use super::platform_tools;
|
||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||
use crate::agents::subagent_task_config::TaskConfig;
|
||||
use crate::agents::todo_tools::{
|
||||
todo_read_tool, todo_write_tool, TODO_READ_TOOL_NAME, TODO_WRITE_TOOL_NAME,
|
||||
};
|
||||
use crate::conversation::message::{Message, ToolRequest};
|
||||
use crate::session::extension_data::ExtensionState;
|
||||
use crate::session::{extension_data, SessionManager};
|
||||
use crate::session::SessionManager;
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
|
||||
@@ -300,7 +296,6 @@ impl Agent {
|
||||
permission_check_result: &PermissionCheckResult,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
session: &Option<SessionConfig>,
|
||||
) -> Result<Vec<(String, ToolStream)>> {
|
||||
let mut tool_futures: Vec<(String, ToolStream)> = Vec::new();
|
||||
|
||||
@@ -308,12 +303,7 @@ impl Agent {
|
||||
for request in &permission_check_result.approved {
|
||||
if let Ok(tool_call) = request.tool_call.clone() {
|
||||
let (req_id, tool_result) = self
|
||||
.dispatch_tool_call(
|
||||
tool_call,
|
||||
request.id.clone(),
|
||||
cancel_token.clone(),
|
||||
session,
|
||||
)
|
||||
.dispatch_tool_call(tool_call, request.id.clone(), cancel_token.clone())
|
||||
.await;
|
||||
|
||||
tool_futures.push((
|
||||
@@ -393,7 +383,6 @@ impl Agent {
|
||||
tool_call: CallToolRequestParam,
|
||||
request_id: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: &Option<SessionConfig>,
|
||||
) -> (String, Result<ToolCallResult, ErrorData>) {
|
||||
if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME {
|
||||
let arguments = tool_call
|
||||
@@ -521,93 +510,6 @@ impl Agent {
|
||||
"Frontend tool execution required".to_string(),
|
||||
None,
|
||||
)))
|
||||
} else if tool_call.name == TODO_READ_TOOL_NAME {
|
||||
// Handle task planner read tool
|
||||
let todo_content = if let Some(session_config) = session {
|
||||
SessionManager::get_session(&session_config.id, false)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|metadata| {
|
||||
extension_data::TodoState::from_extension_data(&metadata.extension_data)
|
||||
.map(|state| state.content)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
ToolCallResult::from(Ok(vec![Content::text(todo_content)]))
|
||||
} else if tool_call.name == TODO_WRITE_TOOL_NAME {
|
||||
// Handle task planner write tool
|
||||
let content = match tool_call.arguments {
|
||||
Some(args) => args
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
// Character limit validation
|
||||
let char_count = content.chars().count();
|
||||
let max_chars = std::env::var("GOOSE_TODO_MAX_CHARS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(50_000);
|
||||
|
||||
if max_chars > 0 && char_count > max_chars {
|
||||
ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!(
|
||||
"Todo list too large: {} chars (max: {})",
|
||||
char_count, max_chars
|
||||
),
|
||||
None,
|
||||
)))
|
||||
} else if let Some(session_config) = session {
|
||||
match SessionManager::get_session(&session_config.id, false).await {
|
||||
Ok(mut session) => {
|
||||
let todo_state = extension_data::TodoState::new(content);
|
||||
if todo_state
|
||||
.to_extension_data(&mut session.extension_data)
|
||||
.is_ok()
|
||||
{
|
||||
match SessionManager::update_session(&session_config.id)
|
||||
.extension_data(session.extension_data)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(_) => ToolCallResult::from(Ok(vec![Content::text(format!(
|
||||
"Updated ({} chars)",
|
||||
char_count
|
||||
))])),
|
||||
Err(_) => ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to update session metadata".to_string(),
|
||||
None,
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to serialize TODO state".to_string(),
|
||||
None,
|
||||
)))
|
||||
}
|
||||
}
|
||||
Err(_) => ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Failed to read session metadata".to_string(),
|
||||
None,
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
ToolCallResult::from(Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"TODO tools require an active session to persist data".to_string(),
|
||||
None,
|
||||
)))
|
||||
}
|
||||
} else if tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME {
|
||||
match self
|
||||
.tool_route_manager
|
||||
@@ -762,11 +664,9 @@ impl Agent {
|
||||
pub async fn add_extension(&self, extension: ExtensionConfig) -> ExtensionResult<()> {
|
||||
match &extension {
|
||||
ExtensionConfig::Frontend {
|
||||
name: _,
|
||||
tools,
|
||||
instructions,
|
||||
bundled: _,
|
||||
available_tools: _,
|
||||
..
|
||||
} => {
|
||||
// For frontend tools, just store them in the frontend_tools map
|
||||
let mut frontend_tools = self.frontend_tools.lock().await;
|
||||
@@ -834,10 +734,6 @@ impl Agent {
|
||||
platform_tools::manage_extensions_tool(),
|
||||
platform_tools::manage_schedule_tool(),
|
||||
]);
|
||||
|
||||
// Add task planner tools
|
||||
prefixed_tools.extend([todo_read_tool(), todo_write_tool()]);
|
||||
|
||||
// Dynamic task tool
|
||||
prefixed_tools.push(create_dynamic_task_tool());
|
||||
|
||||
@@ -1248,7 +1144,6 @@ impl Agent {
|
||||
&permission_check_result,
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
&session
|
||||
).await?;
|
||||
|
||||
let tool_futures_arc = Arc::new(Mutex::new(tool_futures));
|
||||
@@ -1756,21 +1651,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_todo_tools_integration() -> Result<()> {
|
||||
let agent = Agent::new();
|
||||
|
||||
// Test that task planner tools are listed
|
||||
let tools = agent.list_tools(None).await;
|
||||
|
||||
let todo_read = tools.iter().find(|tool| tool.name == TODO_READ_TOOL_NAME);
|
||||
let todo_write = tools.iter().find(|tool| tool.name == TODO_WRITE_TOOL_NAME);
|
||||
|
||||
assert!(todo_read.is_some(), "TODO read tool should be present");
|
||||
assert!(todo_write.is_some(), "TODO write tool should be present");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_inspection_manager_has_all_inspectors() -> Result<()> {
|
||||
let agent = Agent::new();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use crate::agents::todo_extension;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::agents::mcp_client::McpClientTrait;
|
||||
use crate::config;
|
||||
use crate::config::extensions::name_to_key;
|
||||
use crate::config::permission::PermissionLevel;
|
||||
use once_cell::sync::Lazy;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::service::ClientInitializeError;
|
||||
use rmcp::ServiceError as ClientError;
|
||||
@@ -8,10 +14,6 @@ use thiserror::Error;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::config;
|
||||
use crate::config::extensions::name_to_key;
|
||||
use crate::config::permission::PermissionLevel;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("process quit before initialization: stderr = {stderr}")]
|
||||
pub struct ProcessExit {
|
||||
@@ -32,6 +34,37 @@ impl ProcessExit {
|
||||
}
|
||||
}
|
||||
|
||||
pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>> =
|
||||
Lazy::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
map.insert(
|
||||
todo_extension::EXTENSION_NAME,
|
||||
PlatformExtensionDef {
|
||||
name: todo_extension::EXTENSION_NAME,
|
||||
description:
|
||||
"Enable a todo list for Goose so it can keep track of what it is doing",
|
||||
default_enabled: true,
|
||||
client_factory: |ctx| Box::new(todo_extension::TodoClient::new(ctx).unwrap()),
|
||||
},
|
||||
);
|
||||
|
||||
map
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlatformExtensionContext {
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlatformExtensionDef {
|
||||
pub name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub default_enabled: bool,
|
||||
pub client_factory: fn(PlatformExtensionContext) -> Box<dyn McpClientTrait>,
|
||||
}
|
||||
|
||||
/// Errors from Extension operation
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
@@ -151,16 +184,15 @@ pub enum ExtensionConfig {
|
||||
Sse {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
envs: Envs,
|
||||
#[serde(default)]
|
||||
env_keys: Vec<String>,
|
||||
description: Option<String>,
|
||||
// NOTE: set timeout to be optional for compatibility.
|
||||
// However, new configurations should include this field.
|
||||
timeout: Option<u64>,
|
||||
/// Whether this extension is bundled with goose
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
@@ -171,6 +203,7 @@ pub enum ExtensionConfig {
|
||||
Stdio {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
cmd: String,
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
@@ -178,22 +211,30 @@ pub enum ExtensionConfig {
|
||||
#[serde(default)]
|
||||
env_keys: Vec<String>,
|
||||
timeout: Option<u64>,
|
||||
description: Option<String>,
|
||||
/// Whether this extension is bundled with goose
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
available_tools: Vec<String>,
|
||||
},
|
||||
/// Built-in extension that is part of the goose binary
|
||||
/// Built-in extension that is part of the bundled goose MCP server
|
||||
#[serde(rename = "builtin")]
|
||||
Builtin {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
display_name: Option<String>, // needed for the UI
|
||||
description: Option<String>,
|
||||
timeout: Option<u64>,
|
||||
/// Whether this extension is bundled with goose
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
available_tools: Vec<String>,
|
||||
},
|
||||
/// Platform extensions that have direct access to the agent etc and run in the agent process
|
||||
#[serde(rename = "platform")]
|
||||
Platform {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
@@ -204,6 +245,7 @@ pub enum ExtensionConfig {
|
||||
StreamableHttp {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
envs: Envs,
|
||||
@@ -211,11 +253,9 @@ pub enum ExtensionConfig {
|
||||
env_keys: Vec<String>,
|
||||
#[serde(default)]
|
||||
headers: HashMap<String, String>,
|
||||
description: Option<String>,
|
||||
// NOTE: set timeout to be optional for compatibility.
|
||||
// However, new configurations should include this field.
|
||||
timeout: Option<u64>,
|
||||
/// Whether this extension is bundled with goose
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
@@ -226,11 +266,11 @@ pub enum ExtensionConfig {
|
||||
Frontend {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
/// The tools provided by the frontend
|
||||
tools: Vec<Tool>,
|
||||
/// Instructions for how to use these tools
|
||||
instructions: Option<String>,
|
||||
/// Whether this extension is bundled with goose
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
@@ -241,10 +281,9 @@ pub enum ExtensionConfig {
|
||||
InlinePython {
|
||||
/// The name used to identify this extension
|
||||
name: String,
|
||||
description: String,
|
||||
/// The Python code to execute
|
||||
code: String,
|
||||
/// Description of what the extension does
|
||||
description: Option<String>,
|
||||
/// Timeout in seconds
|
||||
timeout: Option<u64>,
|
||||
/// Python package dependencies required by this extension
|
||||
@@ -260,7 +299,7 @@ impl Default for ExtensionConfig {
|
||||
Self::Builtin {
|
||||
name: config::DEFAULT_EXTENSION.to_string(),
|
||||
display_name: Some(config::DEFAULT_DISPLAY_NAME.to_string()),
|
||||
description: None,
|
||||
description: "default".to_string(),
|
||||
timeout: Some(config::DEFAULT_EXTENSION_TIMEOUT),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
@@ -275,7 +314,7 @@ impl ExtensionConfig {
|
||||
uri: uri.into(),
|
||||
envs: Envs::default(),
|
||||
env_keys: Vec::new(),
|
||||
description: Some(description.into()),
|
||||
description: description.into(),
|
||||
timeout: Some(timeout.into()),
|
||||
bundled: None,
|
||||
available_tools: Vec::new(),
|
||||
@@ -294,7 +333,7 @@ impl ExtensionConfig {
|
||||
envs: Envs::default(),
|
||||
env_keys: Vec::new(),
|
||||
headers: HashMap::new(),
|
||||
description: Some(description.into()),
|
||||
description: description.into(),
|
||||
timeout: Some(timeout.into()),
|
||||
bundled: None,
|
||||
available_tools: Vec::new(),
|
||||
@@ -313,7 +352,7 @@ impl ExtensionConfig {
|
||||
args: vec![],
|
||||
envs: Envs::default(),
|
||||
env_keys: Vec::new(),
|
||||
description: Some(description.into()),
|
||||
description: description.into(),
|
||||
timeout: Some(timeout.into()),
|
||||
bundled: None,
|
||||
available_tools: Vec::new(),
|
||||
@@ -329,7 +368,7 @@ impl ExtensionConfig {
|
||||
Self::InlinePython {
|
||||
name: name.into(),
|
||||
code: code.into(),
|
||||
description: Some(description.into()),
|
||||
description: description.into(),
|
||||
timeout: Some(timeout.into()),
|
||||
dependencies: None,
|
||||
available_tools: Vec::new(),
|
||||
@@ -379,6 +418,7 @@ impl ExtensionConfig {
|
||||
Self::StreamableHttp { name, .. } => name,
|
||||
Self::Stdio { name, .. } => name,
|
||||
Self::Builtin { name, .. } => name,
|
||||
Self::Platform { name, .. } => name,
|
||||
Self::Frontend { name, .. } => name,
|
||||
Self::InlinePython { name, .. } => name,
|
||||
}
|
||||
@@ -400,6 +440,9 @@ impl ExtensionConfig {
|
||||
| Self::Builtin {
|
||||
available_tools, ..
|
||||
}
|
||||
| Self::Platform {
|
||||
available_tools, ..
|
||||
}
|
||||
| Self::InlinePython {
|
||||
available_tools, ..
|
||||
}
|
||||
@@ -427,6 +470,7 @@ impl std::fmt::Display for ExtensionConfig {
|
||||
write!(f, "Stdio({}: {} {})", name, cmd, args.join(" "))
|
||||
}
|
||||
ExtensionConfig::Builtin { name, .. } => write!(f, "Builtin({})", name),
|
||||
ExtensionConfig::Platform { name, .. } => write!(f, "Platform({})", name),
|
||||
ExtensionConfig::Frontend { name, tools, .. } => {
|
||||
write!(f, "Frontend({}: {} tools)", name, tools.len())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@ use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo};
|
||||
use super::extension::{
|
||||
ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, PlatformExtensionContext,
|
||||
ToolInfo, PLATFORM_EXTENSIONS,
|
||||
};
|
||||
use super::tool_execution::ToolCallResult;
|
||||
use crate::agents::extension::{Envs, ProcessExit};
|
||||
use crate::agents::extension_malware_check;
|
||||
@@ -85,6 +88,7 @@ impl Extension {
|
||||
/// Manages goose extensions / MCP clients and their interactions
|
||||
pub struct ExtensionManager {
|
||||
extensions: Mutex<HashMap<String, Extension>>,
|
||||
context: Mutex<PlatformExtensionContext>,
|
||||
}
|
||||
|
||||
/// A flattened representation of a resource used by the agent to prepare inference
|
||||
@@ -234,9 +238,18 @@ impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
extensions: Mutex::new(HashMap::new()),
|
||||
context: Mutex::new(PlatformExtensionContext { session_id: None }),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_context(&self, context: PlatformExtensionContext) {
|
||||
*self.context.lock().await = context;
|
||||
}
|
||||
|
||||
pub async fn get_context(&self) -> PlatformExtensionContext {
|
||||
self.context.lock().await.clone()
|
||||
}
|
||||
|
||||
pub async fn supports_resources(&self) -> bool {
|
||||
self.extensions
|
||||
.lock()
|
||||
@@ -417,16 +430,33 @@ impl ExtensionManager {
|
||||
available_tools: _,
|
||||
} => {
|
||||
let cmd = std::env::current_exe()
|
||||
.expect("should find the current executable")
|
||||
.to_str()
|
||||
.expect("should resolve executable to string path")
|
||||
.to_string();
|
||||
.and_then(|path| {
|
||||
path.to_str().map(|s| s.to_string()).ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Invalid UTF-8 in executable path",
|
||||
)
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
ExtensionError::ConfigError(format!(
|
||||
"Failed to resolve executable path: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let command = Command::new(cmd).configure(|command| {
|
||||
command.arg("mcp").arg(name);
|
||||
});
|
||||
let client = child_process_client(command, timeout).await?;
|
||||
Box::new(client)
|
||||
}
|
||||
ExtensionConfig::Platform { name, .. } => {
|
||||
let def = PLATFORM_EXTENSIONS.get(name.as_str()).ok_or_else(|| {
|
||||
ExtensionError::ConfigError(format!("Unknown platform extension: {}", name))
|
||||
})?;
|
||||
let context = self.get_context().await;
|
||||
(def.client_factory)(context)
|
||||
}
|
||||
ExtensionConfig::InlinePython {
|
||||
name,
|
||||
code,
|
||||
@@ -453,7 +483,11 @@ impl ExtensionManager {
|
||||
|
||||
Box::new(client)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
ExtensionConfig::Frontend { .. } => {
|
||||
return Err(ExtensionError::ConfigError(
|
||||
"Invalid extension type: Frontend extensions cannot be added as server extensions".to_string()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let server_info = client.get_info().cloned();
|
||||
@@ -1006,35 +1040,22 @@ impl ExtensionManager {
|
||||
let config = extension.config.clone();
|
||||
let description = match &config {
|
||||
ExtensionConfig::Builtin {
|
||||
name, display_name, ..
|
||||
description,
|
||||
display_name,
|
||||
..
|
||||
} => {
|
||||
// For builtin extensions, use display name if available
|
||||
display_name
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| name.clone())
|
||||
}
|
||||
ExtensionConfig::Sse {
|
||||
description, name, ..
|
||||
}
|
||||
| ExtensionConfig::StreamableHttp {
|
||||
description, name, ..
|
||||
}
|
||||
| ExtensionConfig::Stdio {
|
||||
description, name, ..
|
||||
}
|
||||
| ExtensionConfig::InlinePython {
|
||||
description, name, ..
|
||||
} => {
|
||||
// For SSE/StreamableHttp/Stdio/InlinePython, use description if available
|
||||
description
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("Extension '{}'", name))
|
||||
}
|
||||
ExtensionConfig::Frontend { name, .. } => {
|
||||
format!("Frontend extension '{}'", name)
|
||||
if description.is_empty() {
|
||||
display_name.as_deref().unwrap_or("Built-in extension")
|
||||
} else {
|
||||
description
|
||||
}
|
||||
}
|
||||
ExtensionConfig::Platform { description, .. }
|
||||
| ExtensionConfig::Sse { description, .. }
|
||||
| ExtensionConfig::StreamableHttp { description, .. }
|
||||
| ExtensionConfig::Stdio { description, .. }
|
||||
| ExtensionConfig::Frontend { description, .. }
|
||||
| ExtensionConfig::InlinePython { description, .. } => description,
|
||||
};
|
||||
disabled_extensions.push(format!("- {} - {}", config.name(), description));
|
||||
}
|
||||
@@ -1110,7 +1131,7 @@ mod tests {
|
||||
let config = ExtensionConfig::Builtin {
|
||||
name: name.clone(),
|
||||
display_name: Some(name.clone()),
|
||||
description: None,
|
||||
description: "built-in".to_string(),
|
||||
timeout: None,
|
||||
bundled: None,
|
||||
available_tools,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub mod subagent;
|
||||
pub mod subagent_execution_tool;
|
||||
pub mod subagent_handler;
|
||||
mod subagent_task_config;
|
||||
pub mod todo_tools;
|
||||
pub(crate) mod todo_extension;
|
||||
mod tool_execution;
|
||||
mod tool_route_manager;
|
||||
mod tool_router_index_manager;
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
use crate::agents::extension::PlatformExtensionContext;
|
||||
use crate::agents::mcp_client::{Error, McpClientTrait};
|
||||
use crate::session::extension_data::ExtensionState;
|
||||
use crate::session::{extension_data, SessionManager};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use indoc::indoc;
|
||||
use rmcp::model::{
|
||||
CallToolResult, Content, GetPromptResult, Implementation, InitializeResult, JsonObject,
|
||||
ListPromptsResult, ListResourcesResult, ListToolsResult, ProtocolVersion, ReadResourceResult,
|
||||
ServerCapabilities, ServerNotification, Tool, ToolAnnotations, ToolsCapability,
|
||||
};
|
||||
use rmcp::object;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub static EXTENSION_NAME: &str = "todo";
|
||||
|
||||
pub struct TodoClient {
|
||||
info: InitializeResult,
|
||||
context: PlatformExtensionContext,
|
||||
fallback_content: tokio::sync::RwLock<String>,
|
||||
}
|
||||
|
||||
impl TodoClient {
|
||||
pub fn new(context: PlatformExtensionContext) -> Result<Self> {
|
||||
let info = InitializeResult {
|
||||
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||
capabilities: ServerCapabilities {
|
||||
tools: Some(ToolsCapability {
|
||||
list_changed: Some(false),
|
||||
}),
|
||||
resources: None,
|
||||
prompts: None,
|
||||
completions: None,
|
||||
experimental: None,
|
||||
logging: None,
|
||||
},
|
||||
server_info: Implementation {
|
||||
name: EXTENSION_NAME.to_string(),
|
||||
title: Some("Todo".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
instructions: Some(indoc! {r#"
|
||||
Task Management
|
||||
|
||||
Use todo_read and todo_write for tasks with 2+ steps, multiple files/components, or uncertain scope.
|
||||
|
||||
Workflow:
|
||||
- Start: read → write checklist
|
||||
- During: read → update progress
|
||||
- End: verify all complete
|
||||
|
||||
Warning: todo_write overwrites entirely; always todo_read first (skipping is an error)
|
||||
|
||||
Keep items short, specific, action-oriented. Not using the todo tools for complex tasks is an error.
|
||||
|
||||
Template:
|
||||
- [ ] Implement feature X
|
||||
- [ ] Update API
|
||||
- [ ] Write tests
|
||||
- [ ] Run tests (subagent in parallel)
|
||||
- [ ] Run lint (subagent in parallel)
|
||||
- [ ] Blocked: waiting on credentials
|
||||
"#}.to_string()),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
info,
|
||||
context,
|
||||
fallback_content: tokio::sync::RwLock::new(String::new()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_read_todo(&self) -> Result<Vec<Content>, String> {
|
||||
if let Some(session_id) = &self.context.session_id {
|
||||
match SessionManager::get_session(session_id, false).await {
|
||||
Ok(metadata) => {
|
||||
let content =
|
||||
extension_data::TodoState::from_extension_data(&metadata.extension_data)
|
||||
.map(|state| state.content)
|
||||
.unwrap_or_default();
|
||||
Ok(vec![Content::text(content)])
|
||||
}
|
||||
Err(_) => Ok(vec![Content::text(String::new())]),
|
||||
}
|
||||
} else {
|
||||
let content = self.fallback_content.read().await;
|
||||
Ok(vec![Content::text(content.clone())])
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_write_todo(
|
||||
&self,
|
||||
arguments: Option<JsonObject>,
|
||||
) -> Result<Vec<Content>, String> {
|
||||
let content = arguments
|
||||
.as_ref()
|
||||
.ok_or("Missing arguments")?
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing required parameter: content")?
|
||||
.to_string();
|
||||
|
||||
let char_count = content.chars().count();
|
||||
let max_chars = std::env::var("GOOSE_TODO_MAX_CHARS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(50_000);
|
||||
|
||||
if max_chars > 0 && char_count > max_chars {
|
||||
return Err(format!(
|
||||
"Todo list too large: {} chars (max: {})",
|
||||
char_count, max_chars
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(session_id) = &self.context.session_id {
|
||||
match SessionManager::get_session(session_id, false).await {
|
||||
Ok(mut session) => {
|
||||
let todo_state = extension_data::TodoState::new(content);
|
||||
if todo_state
|
||||
.to_extension_data(&mut session.extension_data)
|
||||
.is_ok()
|
||||
{
|
||||
match SessionManager::update_session(session_id)
|
||||
.extension_data(session.extension_data)
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(vec![Content::text(format!(
|
||||
"Updated ({} chars)",
|
||||
char_count
|
||||
))]),
|
||||
Err(_) => Err("Failed to update session metadata".to_string()),
|
||||
}
|
||||
} else {
|
||||
Err("Failed to serialize TODO state".to_string())
|
||||
}
|
||||
}
|
||||
Err(_) => Err("Failed to read session metadata".to_string()),
|
||||
}
|
||||
} else {
|
||||
let mut fallback = self.fallback_content.write().await;
|
||||
*fallback = content;
|
||||
Ok(vec![Content::text(format!(
|
||||
"Updated ({} chars)",
|
||||
char_count
|
||||
))])
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tools() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool::new(
|
||||
"todo_read".to_string(),
|
||||
indoc! {r#"
|
||||
Read the entire TODO file content.
|
||||
|
||||
This tool reads the complete TODO file and returns its content as a string.
|
||||
Use this to view current tasks, notes, and any other information stored in the TODO file.
|
||||
|
||||
The tool will return an error if the TODO file doesn't exist or cannot be read.
|
||||
"#}.to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
).annotate(ToolAnnotations {
|
||||
title: Some("Read TODO file".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(true),
|
||||
open_world_hint: Some(false),
|
||||
}),
|
||||
Tool::new(
|
||||
"todo_write".to_string(),
|
||||
indoc! {r#"
|
||||
Write or overwrite the entire TODO file content.
|
||||
|
||||
This tool replaces the complete TODO file content with the provided string.
|
||||
Use this to update tasks, add new items, or reorganize the TODO file.
|
||||
|
||||
WARNING: This operation completely replaces the file content. Make sure to include
|
||||
all content you want to keep, not just the changes.
|
||||
|
||||
The tool will create the TODO file if it doesn't exist, or overwrite it if it does.
|
||||
Returns an error if the file cannot be written due to permissions or other I/O issues.
|
||||
"#}.to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The TODO list content to save"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}),
|
||||
).annotate(ToolAnnotations {
|
||||
title: Some("Write TODO file".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(true),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl McpClientTrait for TodoClient {
|
||||
async fn list_resources(
|
||||
&self,
|
||||
_next_cursor: Option<String>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<ListResourcesResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn read_resource(
|
||||
&self,
|
||||
_uri: &str,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<ReadResourceResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_next_cursor: Option<String>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<ListToolsResult, Error> {
|
||||
Ok(ListToolsResult {
|
||||
tools: Self::get_tools(),
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Option<JsonObject>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
let content = match name {
|
||||
"todo_read" => self.handle_read_todo().await,
|
||||
"todo_write" => self.handle_write_todo(arguments).await,
|
||||
_ => Err(format!("Unknown tool: {}", name)),
|
||||
};
|
||||
|
||||
match content {
|
||||
Ok(content) => Ok(CallToolResult::success(content)),
|
||||
Err(error) => Ok(CallToolResult::error(vec![Content::text(format!(
|
||||
"Error: {}",
|
||||
error
|
||||
))])),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
_next_cursor: Option<String>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<ListPromptsResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
_name: &str,
|
||||
_arguments: Value,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<GetPromptResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
|
||||
mpsc::channel(1).1
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Option<&InitializeResult> {
|
||||
Some(&self.info)
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
use indoc::indoc;
|
||||
use rmcp::model::{Tool, ToolAnnotations};
|
||||
use rmcp::object;
|
||||
|
||||
/// Tool name constant for reading task planner content
|
||||
pub const TODO_READ_TOOL_NAME: &str = "todo__read";
|
||||
|
||||
/// Tool name constant for writing task planner content
|
||||
pub const TODO_WRITE_TOOL_NAME: &str = "todo__write";
|
||||
|
||||
/// Creates a tool for reading task planner content.
|
||||
///
|
||||
/// This tool reads the entire task planner file content as a string.
|
||||
/// It is marked as read-only and safe to use repeatedly.
|
||||
///
|
||||
/// # Returns
|
||||
/// A configured `Tool` instance for reading task planner content
|
||||
pub fn todo_read_tool() -> Tool {
|
||||
Tool::new(
|
||||
TODO_READ_TOOL_NAME.to_string(),
|
||||
indoc! {r#"
|
||||
Read the entire TODO file content.
|
||||
|
||||
This tool reads the complete TODO file and returns its content as a string.
|
||||
Use this to view current tasks, notes, and any other information stored in the TODO file.
|
||||
|
||||
The tool will return an error if the TODO file doesn't exist or cannot be read.
|
||||
"#}
|
||||
.to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {}
|
||||
}),
|
||||
)
|
||||
.annotate(ToolAnnotations {
|
||||
title: Some("Read TODO content".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(true),
|
||||
open_world_hint: Some(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a tool for writing task planner content.
|
||||
///
|
||||
/// This tool writes or overwrites the entire task planner file with new content.
|
||||
/// It replaces the complete file content with the provided string.
|
||||
///
|
||||
/// # Returns
|
||||
/// A configured `Tool` instance for writing task planner content
|
||||
pub fn todo_write_tool() -> Tool {
|
||||
Tool::new(
|
||||
TODO_WRITE_TOOL_NAME.to_string(),
|
||||
indoc! {r#"
|
||||
Write or overwrite the entire TODO file content.
|
||||
|
||||
This tool replaces the complete TODO file content with the provided string.
|
||||
Use this to update tasks, add new items, or reorganize the TODO file.
|
||||
|
||||
WARNING: This operation completely replaces the file content. Make sure to include
|
||||
all content you want to keep, not just the changes.
|
||||
|
||||
The tool will create the TODO file if it doesn't exist, or overwrite it if it does.
|
||||
Returns an error if the file cannot be written due to permissions or other I/O issues.
|
||||
"#}
|
||||
.to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"required": ["content"],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The complete content to write to the TODO file. This will replace all existing content."
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.annotate(ToolAnnotations {
|
||||
title: Some("Write TODO content".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(true), // It overwrites the entire file
|
||||
idempotent_hint: Some(true), // Writing the same content multiple times has the same effect
|
||||
open_world_hint: Some(false),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_todo_read_tool_creation() {
|
||||
let tool = todo_read_tool();
|
||||
|
||||
// Verify tool name
|
||||
assert_eq!(tool.name, TODO_READ_TOOL_NAME);
|
||||
|
||||
// Verify description exists and is not empty
|
||||
assert!(tool.description.is_some());
|
||||
let description = tool.description.as_ref().unwrap();
|
||||
assert!(!description.is_empty());
|
||||
|
||||
// Verify input schema
|
||||
let schema = &tool.input_schema;
|
||||
assert_eq!(schema["type"], "object");
|
||||
assert_eq!(schema["required"].as_array().unwrap().len(), 0);
|
||||
|
||||
// Verify annotations
|
||||
let annotations = tool.annotations.as_ref().unwrap();
|
||||
assert_eq!(annotations.title, Some("Read TODO content".to_string()));
|
||||
assert_eq!(annotations.read_only_hint, Some(true));
|
||||
assert_eq!(annotations.destructive_hint, Some(false));
|
||||
assert_eq!(annotations.idempotent_hint, Some(true));
|
||||
assert_eq!(annotations.open_world_hint, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_write_tool_creation() {
|
||||
let tool = todo_write_tool();
|
||||
|
||||
// Verify tool name
|
||||
assert_eq!(tool.name, TODO_WRITE_TOOL_NAME);
|
||||
|
||||
// Verify description exists and is not empty
|
||||
assert!(tool.description.is_some());
|
||||
let description = tool.description.as_ref().unwrap();
|
||||
assert!(!description.is_empty());
|
||||
|
||||
// Verify input schema
|
||||
let schema = &tool.input_schema;
|
||||
assert_eq!(schema["type"], "object");
|
||||
|
||||
// Verify required parameters
|
||||
let required = schema["required"].as_array().unwrap();
|
||||
assert_eq!(required.len(), 1);
|
||||
assert_eq!(required[0], "content");
|
||||
|
||||
// Verify properties
|
||||
assert!(schema["properties"]["content"].is_object());
|
||||
assert_eq!(schema["properties"]["content"]["type"], "string");
|
||||
|
||||
// Verify annotations
|
||||
let annotations = tool.annotations.as_ref().unwrap();
|
||||
assert_eq!(annotations.title, Some("Write TODO content".to_string()));
|
||||
assert_eq!(annotations.read_only_hint, Some(false));
|
||||
assert_eq!(annotations.destructive_hint, Some(true));
|
||||
assert_eq!(annotations.idempotent_hint, Some(true));
|
||||
assert_eq!(annotations.open_world_hint, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_name_constants() {
|
||||
// Verify the constants follow the naming pattern
|
||||
assert!(TODO_READ_TOOL_NAME.starts_with("todo__"));
|
||||
assert!(TODO_WRITE_TOOL_NAME.starts_with("todo__"));
|
||||
assert_eq!(TODO_READ_TOOL_NAME, "todo__read");
|
||||
assert_eq!(TODO_WRITE_TOOL_NAME, "todo__write");
|
||||
}
|
||||
}
|
||||
@@ -90,8 +90,7 @@ impl Agent {
|
||||
}
|
||||
|
||||
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
||||
// Clone tool_call to avoid moving it
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), &None).await;
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone()).await;
|
||||
let mut futures = tool_futures.lock().await;
|
||||
|
||||
futures.push((req_id, match tool_result {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use super::base::Config;
|
||||
use crate::agents::extension::PLATFORM_EXTENSIONS;
|
||||
use crate::agents::ExtensionConfig;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const DEFAULT_EXTENSION: &str = "developer";
|
||||
@@ -29,10 +32,78 @@ pub struct ExtensionConfigManager;
|
||||
|
||||
impl ExtensionConfigManager {
|
||||
fn get_extensions_map() -> Result<HashMap<String, ExtensionEntry>> {
|
||||
let config = Config::global();
|
||||
Ok(config
|
||||
.get_param(EXTENSIONS_CONFIG_KEY)
|
||||
.unwrap_or_else(|_| HashMap::new()))
|
||||
let raw: Value = Config::global()
|
||||
.get_param::<Value>(EXTENSIONS_CONFIG_KEY)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!(
|
||||
"Failed to load {}: {err}. Falling back to empty object.",
|
||||
EXTENSIONS_CONFIG_KEY
|
||||
);
|
||||
Value::Object(serde_json::Map::new())
|
||||
});
|
||||
|
||||
let mut extensions_map: HashMap<String, ExtensionEntry> = match raw {
|
||||
Value::Object(obj) => {
|
||||
let mut m = HashMap::with_capacity(obj.len());
|
||||
for (k, mut v) in obj {
|
||||
if let Value::Object(ref mut inner) = v {
|
||||
match inner.get("description") {
|
||||
Some(Value::Null) | None => {
|
||||
inner.insert(
|
||||
"description".to_string(),
|
||||
Value::String(String::new()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match serde_json::from_value::<ExtensionEntry>(v.clone()) {
|
||||
Ok(entry) => {
|
||||
m.insert(k, entry);
|
||||
}
|
||||
Err(err) => {
|
||||
let bad_json = serde_json::to_string(&v).unwrap_or_else(|e| {
|
||||
format!("<failed to serialize malformed value: {e}>")
|
||||
});
|
||||
warn!(
|
||||
extension = %k,
|
||||
error = %err,
|
||||
bad_json = %bad_json,
|
||||
"Skipping malformed extension"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
other => {
|
||||
warn!(
|
||||
"Expected object for {}, got {}. Using empty map.",
|
||||
EXTENSIONS_CONFIG_KEY, other
|
||||
);
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
if !extensions_map.is_empty() {
|
||||
for (name, def) in PLATFORM_EXTENSIONS.iter() {
|
||||
if !extensions_map.contains_key(*name) {
|
||||
extensions_map.insert(
|
||||
name.to_string(),
|
||||
ExtensionEntry {
|
||||
config: ExtensionConfig::Platform {
|
||||
name: def.name.to_string(),
|
||||
description: def.description.to_string(),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(extensions_map)
|
||||
}
|
||||
|
||||
fn save_extensions_map(extensions: HashMap<String, ExtensionEntry>) -> Result<()> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::agents::extension::PlatformExtensionContext;
|
||||
use crate::agents::Agent;
|
||||
use crate::config::APP_STRATEGY;
|
||||
use crate::model::ModelConfig;
|
||||
@@ -119,6 +120,12 @@ impl AgentManager {
|
||||
|
||||
let agent = Arc::new(Agent::new());
|
||||
agent.set_scheduler(Arc::clone(&self.scheduler)).await;
|
||||
agent
|
||||
.extension_manager
|
||||
.set_context(PlatformExtensionContext {
|
||||
session_id: Some(session_id.clone()),
|
||||
})
|
||||
.await;
|
||||
if let Some(provider) = &*self.default_provider.read().await {
|
||||
agent.update_provider(Arc::clone(provider)).await?;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
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.
|
||||
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 {{current_date_time}}.
|
||||
|
||||
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.
|
||||
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.
|
||||
Use the search_available_extensions tool to find additional extensions to enable to help with your task. To enable extensions, use the enable_extension tool and provide the extension_name. You should only enable extensions found from the search_available_extensions tool.
|
||||
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.
|
||||
Use the search_available_extensions tool to find additional extensions to enable to help with your task. To enable
|
||||
extensions, use the enable_extension tool and provide the extension_name. You should only enable extensions found from
|
||||
the search_available_extensions tool.
|
||||
|
||||
{% if (extensions is defined) and extensions %}
|
||||
Because you dynamically load extensions, your conversation history may refer
|
||||
@@ -18,7 +25,9 @@ active extensions are below. Each of these extensions provides tools that are
|
||||
in your tool specification.
|
||||
|
||||
{% for extension in extensions %}
|
||||
|
||||
## {{extension.name}}
|
||||
|
||||
{% if extension.has_resources %}
|
||||
{{extension.name}} supports resources, you can use platform__read_resource,
|
||||
and platform__list_resources on this extension.
|
||||
@@ -32,32 +41,20 @@ No extensions are defined. You should let the user know that they should add ext
|
||||
{% endif %}
|
||||
|
||||
{% if suggest_disable is defined %}
|
||||
|
||||
# Suggestion
|
||||
|
||||
{{suggest_disable}}
|
||||
{% endif %}
|
||||
|
||||
{{tool_selection_strategy}}
|
||||
|
||||
# Task Management
|
||||
|
||||
- Use `todo__read` and `todo__write` for tasks with 2+ steps, multiple files/components, or uncertain scope
|
||||
- Workflow — Start: read → write checklist | During: read → update progress | End: verify all complete
|
||||
- Warning — `todo__write` overwrites entirely; always `todo__read` first (skipping is an error)
|
||||
- Keep items short, specific, action-oriented
|
||||
- Not using the todo tools for complex tasks is an error
|
||||
|
||||
Template:
|
||||
```markdown
|
||||
- [ ] Implement feature X
|
||||
- [ ] Update API
|
||||
- [ ] Write tests
|
||||
- [ ] Run tests (subagent in parallel)
|
||||
- [ ] Run lint (subagent in parallel)
|
||||
- [ ] Blocked: waiting on credentials
|
||||
```
|
||||
# sub agents
|
||||
|
||||
Execute via subagent by default — only handle directly when step-by-step visibility is essential.
|
||||
- Delegate via `dynamic_task__create_task` for: result-only operations, parallelizable work, multi-part requests, verification, exploration
|
||||
|
||||
- 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
|
||||
@@ -68,8 +65,10 @@ Execute via subagent by default — only handle directly when step-by-step visib
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -712,7 +712,7 @@ sub_recipes:
|
||||
} => {
|
||||
assert_eq!(name, "test_python");
|
||||
assert_eq!(code, "print('hello world')");
|
||||
assert_eq!(description.as_deref(), Some("Test python extension"));
|
||||
assert_eq!(description, "Test python extension");
|
||||
assert_eq!(timeout, &Some(300));
|
||||
assert!(dependencies.is_some());
|
||||
let deps = dependencies.as_ref().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user