Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ use super::final_output_tool::FinalOutputTool;
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_fixer::{debug_conversation_fix, ConversationFixer};

const DEFAULT_MAX_TURNS: u32 = 1000;
Expand Down Expand Up @@ -97,6 +100,7 @@ pub struct Agent {
pub(super) tool_route_manager: ToolRouteManager,
pub(super) scheduler_service: Mutex<Option<Arc<dyn SchedulerTrait>>>,
pub(super) retry_manager: RetryManager,
pub(super) todo_list: Arc<Mutex<String>>,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -173,6 +177,7 @@ impl Agent {
tool_route_manager: ToolRouteManager::new(),
scheduler_service: Mutex::new(None),
retry_manager,
todo_list: Arc::new(Mutex::new(String::new())),
}
}

Expand Down Expand Up @@ -462,6 +467,23 @@ impl Agent {
ToolCallResult::from(Err(ToolError::ExecutionError(
"Frontend tool execution required".to_string(),
)))
} else if tool_call.name == TODO_READ_TOOL_NAME {
// Handle TODO read tool
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated
let todo_content = self.todo_list.lock().await.clone();
ToolCallResult::from(Ok(vec![Content::text(todo_content)]))
} else if tool_call.name == TODO_WRITE_TOOL_NAME {
// Handle TODO write tool
let content = tool_call
.arguments
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();

let mut todo_list = self.todo_list.lock().await;
*todo_list = content.clone();

ToolCallResult::from(Ok(vec![Content::text("TODO list updated successfully")]))
} else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME
|| tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME
{
Expand Down Expand Up @@ -679,6 +701,9 @@ impl Agent {
platform_tools::manage_schedule_tool(),
]);

// Add TODO tools
prefixed_tools.extend([todo_read_tool(), todo_write_tool()]);

// Dynamic task tool
prefixed_tools.push(create_dynamic_task_tool());

Expand Down Expand Up @@ -1426,4 +1451,24 @@ mod tests {
assert!(system_prompt.contains(&final_output_tool_system_prompt));
Ok(())
}

#[tokio::test]
async fn test_todo_tools_integration() -> Result<()> {
let agent = Agent::new();

// Test that todo 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");

// Test todo_list initialization
let todo_content = agent.todo_list.lock().await;
assert_eq!(*todo_content, "", "TODO list should be initially empty");

Ok(())
}
}
1 change: 1 addition & 0 deletions crates/goose/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod subagent;
pub mod subagent_execution_tool;
pub mod subagent_handler;
mod subagent_task_config;
pub mod todo_tools;
mod tool_execution;
mod tool_route_manager;
mod tool_router_index_manager;
Expand Down
160 changes: 160 additions & 0 deletions crates/goose/src/agents/todo_tools.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use indoc::indoc;
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;

/// Tool name constant for reading TODO content
pub const TODO_READ_TOOL_NAME: &str = "todo__read";

/// Tool name constant for writing TODO content
pub const TODO_WRITE_TOOL_NAME: &str = "todo__write";

/// Creates a tool for reading TODO content.
///
/// This tool reads the entire TODO file content as a string.
/// It is marked as read-only and safe to use repeatedly.
///
/// # Returns
/// A configured `Tool` instance for reading TODO 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 TODO content.
///
/// This tool writes or overwrites the entire TODO file with new content.
/// It replaces the complete file content with the provided string.
///
/// # Returns
/// A configured `Tool` instance for writing TODO 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 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");
}
}
28 changes: 28 additions & 0 deletions crates/goose/src/prompts/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ No extensions are defined. You should let the user know that they should add ext

{{tool_selection_strategy}}

# Task Management
Comment thread
tlongwell-block marked this conversation as resolved.

You have access to `todo__read` and `todo__write` tools for maintaining a working todo list throughout your session. These tools help you track progress on complex tasks and ensure thorough completion of all requested work.

The todo list is particularly useful for:
- Breaking down multi-step problems into manageable tasks
- Tracking progress across multiple files or components
- Maintaining context between conversation turns
- Planning implementation approaches before starting work

When working on complex tasks, consider using the todo list to:
1. Plan your approach by listing all necessary steps
2. Track completion status as you progress
3. Note any blockers or dependencies discovered during work
4. Ensure all aspects of the request are addressed

The `todo__write` tool replaces the entire todo content, so read the current list first if you need to preserve existing items. Using markdown with checkboxes provides clear visual status:

```markdown
- [x] Completed task
- [ ] Pending task
- [ ] Task with subtasks
- [ ] Subtask 1
- [ ] Subtask 2
```

Regular updates to the todo list help maintain visibility into your progress and prevent important steps from being overlooked.

# Response Guidelines

- Use Markdown formatting for all responses.
Expand Down
Loading
Loading