@@ -248,6 +248,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
|
||||
"developer__text_editor" => render_text_editor_request(call, debug),
|
||||
"developer__shell" => render_shell_request(call, debug),
|
||||
"dynamic_task__create_task" => render_dynamic_task_request(call, debug),
|
||||
"todo__read" | "todo__write" => render_todo_request(call, debug),
|
||||
_ => render_default_request(call, debug),
|
||||
},
|
||||
Err(e) => print_markdown(&e.to_string(), theme),
|
||||
@@ -452,6 +453,19 @@ fn render_dynamic_task_request(call: &ToolCall, debug: bool) {
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_todo_request(call: &ToolCall, _debug: bool) {
|
||||
print_tool_header(call);
|
||||
|
||||
// For todo tools, always show the full content without redaction
|
||||
if let Some(Value::String(content)) = call.arguments.get("content") {
|
||||
println!("{}: {}", style("content").dim(), style(content).green());
|
||||
} else {
|
||||
// For todo__read, there are no arguments
|
||||
// Just print an empty line for consistency
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn render_default_request(call: &ToolCall, debug: bool) {
|
||||
print_tool_header(call);
|
||||
print_params(&call.arguments, 0, debug);
|
||||
|
||||
@@ -2292,33 +2292,28 @@ mod tests {
|
||||
assert!(text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc
|
||||
.contains("Replace a string in a file with a new string")));
|
||||
.is_some_and(|desc| desc.contains("Replace a string in a file with a new string")));
|
||||
assert!(text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc
|
||||
.contains("the `old_str` needs to exactly match one")));
|
||||
.is_some_and(|desc| desc.contains("the `old_str` needs to exactly match one")));
|
||||
assert!(text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc.contains("str_replace")));
|
||||
.is_some_and(|desc| desc.contains("str_replace")));
|
||||
|
||||
// Should not contain editor API description or edit_file command
|
||||
assert!(!text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc
|
||||
.contains("Edit the file with the new content")));
|
||||
.is_some_and(|desc| desc.contains("Edit the file with the new content")));
|
||||
assert!(!text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc.contains("edit_file")));
|
||||
assert!(!text_editor_tool
|
||||
.description
|
||||
.as_ref()
|
||||
.map_or(false, |desc| desc
|
||||
.contains("work out how to place old_str with it intelligently")));
|
||||
.is_some_and(|desc| desc.contains("edit_file")));
|
||||
assert!(!text_editor_tool.description.as_ref().is_some_and(
|
||||
|desc| desc.contains("work out how to place old_str with it intelligently")
|
||||
));
|
||||
|
||||
temp_dir.close().unwrap();
|
||||
}
|
||||
|
||||
@@ -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::message::{Message, ToolRequest};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
@@ -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)]
|
||||
@@ -148,6 +152,15 @@ where
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
const DEFAULT_TODO_MAX_CHARS: usize = 50_000;
|
||||
|
||||
fn get_todo_max_chars() -> usize {
|
||||
std::env::var("GOOSE_TODO_MAX_CHARS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(Self::DEFAULT_TODO_MAX_CHARS)
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
// Create channels with buffer size 32 (adjust if needed)
|
||||
let (confirm_tx, confirm_rx) = mpsc::channel(32);
|
||||
@@ -173,6 +186,7 @@ impl Agent {
|
||||
tool_route_manager: ToolRouteManager::new(),
|
||||
scheduler_service: Mutex::new(None),
|
||||
retry_manager,
|
||||
todo_list: Arc::new(Mutex::new(String::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +481,45 @@ impl Agent {
|
||||
ToolCallResult::from(Err(ToolError::ExecutionError(
|
||||
"Frontend tool execution required".to_string(),
|
||||
)))
|
||||
} else if tool_call.name == TODO_READ_TOOL_NAME {
|
||||
// Handle task planner read tool
|
||||
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 task planner write tool
|
||||
let content = tool_call
|
||||
.arguments
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
// Acquire lock first to prevent race condition
|
||||
let mut todo_list = self.todo_list.lock().await;
|
||||
|
||||
// Character limit validation
|
||||
let char_count = content.chars().count();
|
||||
let max_chars = Self::get_todo_max_chars();
|
||||
|
||||
// Simple validation - reject if over limit (0 means unlimited)
|
||||
if max_chars > 0 && char_count > max_chars {
|
||||
return (
|
||||
request_id,
|
||||
Ok(ToolCallResult::from(Err(ToolError::ExecutionError(
|
||||
format!(
|
||||
"Todo list too large: {} chars (max: {})",
|
||||
char_count, max_chars
|
||||
),
|
||||
)))),
|
||||
);
|
||||
}
|
||||
|
||||
*todo_list = content;
|
||||
|
||||
ToolCallResult::from(Ok(vec![Content::text(format!(
|
||||
"Updated ({} chars)",
|
||||
char_count
|
||||
))]))
|
||||
} else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME
|
||||
|| tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME
|
||||
{
|
||||
@@ -684,6 +737,9 @@ impl Agent {
|
||||
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());
|
||||
|
||||
@@ -1431,4 +1487,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 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");
|
||||
|
||||
// Test todo_list initialization
|
||||
let todo_content = agent.todo_list.lock().await;
|
||||
assert_eq!(*todo_content, "", "TODO list should be initially empty");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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 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");
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,23 @@ No extensions are defined. You should let the user know that they should add ext
|
||||
|
||||
{{tool_selection_strategy}}
|
||||
|
||||
# Task Management
|
||||
|
||||
- Required — use `todo__read` and `todo__write` for any task with 2+ steps, multiple files/components, or uncertain scope. Skipping them is an error.
|
||||
- Start — `todo__read`, then `todo__write` a brief checklist (Markdown checkboxes).
|
||||
- During — after each major action, update via `todo__write`: mark done, add/edit items, note blockers/dependencies.
|
||||
- Finish — ensure every item is checked, or clearly list what remains.
|
||||
- Overwrite warning — `todo__write` replaces the entire list; always read before writing. It is an error to not read before writing.
|
||||
- Quality — keep items short, specific, and action‑oriented.
|
||||
|
||||
Template:
|
||||
```markdown
|
||||
- [ ] Implement feature X
|
||||
- [ ] Update API
|
||||
- [ ] Write tests
|
||||
- [ ] Blocked: waiting on credentials
|
||||
```
|
||||
|
||||
# Response Guidelines
|
||||
|
||||
- Use Markdown formatting for all responses.
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
use goose::agents::todo_tools::{TODO_READ_TOOL_NAME, TODO_WRITE_TOOL_NAME};
|
||||
use goose::agents::Agent;
|
||||
use mcp_core::tool::ToolCall;
|
||||
use serde_json::json;
|
||||
use serial_test::serial;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_todo_tools_in_agent_list() {
|
||||
let agent = Agent::new();
|
||||
let tools = agent.list_tools(None).await;
|
||||
|
||||
// Check that todo tools are present
|
||||
let todo_read = tools.iter().find(|t| t.name == TODO_READ_TOOL_NAME);
|
||||
let todo_write = tools.iter().find(|t| t.name == TODO_WRITE_TOOL_NAME);
|
||||
|
||||
assert!(
|
||||
todo_read.is_some(),
|
||||
"Todo read tool should be in agent's tool list"
|
||||
);
|
||||
assert!(
|
||||
todo_write.is_some(),
|
||||
"Todo write tool should be in agent's tool list"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_write_and_read() {
|
||||
// Ensure we have a clean environment for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
// Write to the todo list
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": "1. Buy milk\n2. Walk the dog\n3. Review code"
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, write_result) = agent
|
||||
.dispatch_tool_call(write_call, "test-write-1".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result.is_ok(), "Write should succeed");
|
||||
|
||||
// Read from the todo list
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "test-read-1".to_string(), None)
|
||||
.await;
|
||||
assert!(read_result.is_ok(), "Read should succeed");
|
||||
|
||||
// Verify the content matches what we wrote
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
assert!(!contents.is_empty(), "Should have content");
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
assert_eq!(text, "1. Buy milk\n2. Walk the dog\n3. Review code");
|
||||
} else {
|
||||
panic!("Failed to get content from read result");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_todo_empty_initially() {
|
||||
let agent = Agent::new();
|
||||
|
||||
// Read from empty todo list
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "test-read-empty".to_string(), None)
|
||||
.await;
|
||||
assert!(read_result.is_ok(), "Read should succeed even when empty");
|
||||
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
assert!(!contents.is_empty(), "Should have content");
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
assert_eq!(text, "", "Empty todo list should return empty string");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_overwrite() {
|
||||
// Ensure no limit is set for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
// Write initial content
|
||||
let write_call1 = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": "Initial todo list"
|
||||
}),
|
||||
};
|
||||
let (_, write_result1) = agent
|
||||
.dispatch_tool_call(write_call1, "test-write-1".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result1.is_ok(), "First write should succeed");
|
||||
|
||||
// Overwrite with new content
|
||||
let write_call2 = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": "Completely new todo list"
|
||||
}),
|
||||
};
|
||||
let (_, write_result2) = agent
|
||||
.dispatch_tool_call(write_call2, "test-write-2".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result2.is_ok(), "Second write should succeed");
|
||||
|
||||
// Read and verify it was overwritten
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "test-read-2".to_string(), None)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
assert_eq!(
|
||||
text, "Completely new todo list",
|
||||
"Content should be overwritten"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_todo_concurrent_access() {
|
||||
let agent = Arc::new(Agent::new());
|
||||
|
||||
// Spawn multiple concurrent writes
|
||||
let mut handles = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let agent_clone = agent.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": format!("Todo list {}", i)
|
||||
}),
|
||||
};
|
||||
agent_clone
|
||||
.dispatch_tool_call(write_call, format!("concurrent-{}", i), None)
|
||||
.await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all writes to complete
|
||||
for handle in handles {
|
||||
let _ = handle.await.unwrap();
|
||||
}
|
||||
|
||||
// Read the final state
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "final-read".to_string(), None)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
// The last write wins - we just verify it's one of the valid values
|
||||
assert!(
|
||||
text.starts_with("Todo list "),
|
||||
"Should have valid todo content"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_large_content() {
|
||||
// Ensure we have a clean environment for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
// Create a large todo list that exceeds the 50,000 character limit
|
||||
let large_content = "X".repeat(100_000);
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": large_content.clone()
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, write_result) = agent
|
||||
.dispatch_tool_call(write_call, "large-write".to_string(), None)
|
||||
.await;
|
||||
|
||||
// Should fail because it exceeds the 50,000 character limit
|
||||
if let Ok(result) = write_result {
|
||||
let response = result.result.await;
|
||||
assert!(
|
||||
response.is_err(),
|
||||
"Should fail with error for content exceeding limit"
|
||||
);
|
||||
if let Err(error) = response {
|
||||
let error_str = error.to_string();
|
||||
assert!(error_str.contains("Todo list too large"));
|
||||
assert!(error_str.contains("100000 chars"));
|
||||
assert!(error_str.contains("max: 50000"));
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Ok(ToolCallResult) with inner error, got Err");
|
||||
}
|
||||
|
||||
// Test with content within the limit
|
||||
let valid_content = "X".repeat(50_000);
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": valid_content.clone()
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, write_result) = agent
|
||||
.dispatch_tool_call(write_call, "valid-write".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result.is_ok(), "Should handle content within limit");
|
||||
|
||||
// Read it back
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "valid-read".to_string(), None)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
assert_eq!(
|
||||
text.len(),
|
||||
valid_content.len(),
|
||||
"Valid content should be preserved"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_unicode_content() {
|
||||
// Ensure no limit is set for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
let unicode_content = "📝 Todo List:\n✅ Task 1\n⭐ Task 2\n🔥 Urgent: Task 3\n日本語のタスク";
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": unicode_content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, write_result) = agent
|
||||
.dispatch_tool_call(write_call, "unicode-write".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result.is_ok(), "Write should succeed");
|
||||
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, read_result) = agent
|
||||
.dispatch_tool_call(read_call, "unicode-read".to_string(), None)
|
||||
.await;
|
||||
|
||||
if let Ok(result) = read_result {
|
||||
let content_future = result.result;
|
||||
let content_result = content_future.await;
|
||||
|
||||
if let Ok(contents) = content_result {
|
||||
let text = contents[0].as_text().map(|t| t.text.as_str()).unwrap_or("");
|
||||
assert_eq!(text, unicode_content, "Unicode content should be preserved");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_character_limit_enforcement() {
|
||||
// Set a small limit for testing
|
||||
std::env::set_var("GOOSE_TODO_MAX_CHARS", "100");
|
||||
|
||||
// Create agent AFTER setting the environment variable
|
||||
let agent = Agent::new();
|
||||
|
||||
// Create content that exceeds the limit
|
||||
let large_content = "x".repeat(101);
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": large_content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(write_call, "test-limit".to_string(), None)
|
||||
.await;
|
||||
|
||||
// Should fail with error
|
||||
assert!(result.is_ok(), "dispatch_tool_call should return Ok");
|
||||
if let Ok(result) = result {
|
||||
let response = result.result.await;
|
||||
assert!(response.is_err(), "Should fail with error");
|
||||
if let Err(error) = response {
|
||||
let error_str = error.to_string();
|
||||
assert!(
|
||||
error_str.contains("Todo list too large"),
|
||||
"Error should mention 'Todo list too large'"
|
||||
);
|
||||
assert!(
|
||||
error_str.contains("101 chars"),
|
||||
"Error should mention '101 chars'"
|
||||
);
|
||||
assert!(
|
||||
error_str.contains("max: 100"),
|
||||
"Error should mention 'max: 100'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_character_count_in_write_response() {
|
||||
// Ensure no limit is set for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
let content = "Test todo content";
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(write_call, "test-count".to_string(), None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Ok(tool_result) = result {
|
||||
let response = tool_result.result.await.unwrap();
|
||||
let text = response[0].as_text().unwrap().text.clone();
|
||||
assert!(text.contains("Updated (17 chars)")); // "Test todo content" is 17 chars
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_read_returns_clean_content() {
|
||||
// Ensure no limit is set for this test
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
// Write some content
|
||||
let content = "My todo list\n- Task 1\n- Task 2";
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, write_result) = agent
|
||||
.dispatch_tool_call(write_call, "test-write".to_string(), None)
|
||||
.await;
|
||||
assert!(write_result.is_ok(), "Write should succeed");
|
||||
|
||||
// Read should return exact content, no metadata
|
||||
let read_call = ToolCall {
|
||||
name: TODO_READ_TOOL_NAME.to_string(),
|
||||
arguments: json!({}),
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(read_call, "test-read".to_string(), None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
if let Ok(tool_result) = result {
|
||||
let response = tool_result.result.await.unwrap();
|
||||
let text = response[0].as_text().unwrap().text.clone();
|
||||
|
||||
// Should be exactly the original content
|
||||
assert_eq!(text, content);
|
||||
// Should NOT contain any metadata
|
||||
assert!(!text.contains("chars"));
|
||||
assert!(!text.contains("<!--"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_unlimited_with_zero_limit() {
|
||||
std::env::set_var("GOOSE_TODO_MAX_CHARS", "0");
|
||||
|
||||
let agent = Agent::new();
|
||||
|
||||
// Should accept very large content when limit is 0
|
||||
let huge_content = "x".repeat(100_000);
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": huge_content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(write_call, "test-unlimited".to_string(), None)
|
||||
.await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok());
|
||||
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_todo_unicode_character_counting() {
|
||||
std::env::set_var("GOOSE_TODO_MAX_CHARS", "10");
|
||||
|
||||
// Create agent AFTER setting the environment variable
|
||||
let agent = Agent::new();
|
||||
|
||||
// Test with emoji - each emoji is 1 character in .chars().count()
|
||||
let content = "📝📝📝📝📝📝📝📝📝📝📝"; // 11 emoji = 11 chars
|
||||
|
||||
let write_call = ToolCall {
|
||||
name: TODO_WRITE_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
"content": content
|
||||
}),
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(write_call, "test-unicode".to_string(), None)
|
||||
.await;
|
||||
|
||||
// Should fail as it's 11 chars
|
||||
assert!(result.is_ok(), "dispatch_tool_call should return Ok");
|
||||
if let Ok(result) = result {
|
||||
let response = result.result.await;
|
||||
assert!(
|
||||
response.is_err(),
|
||||
"Should fail with error - 11 chars exceeds limit of 10"
|
||||
);
|
||||
if let Err(error) = response {
|
||||
let error_str = error.to_string();
|
||||
assert!(
|
||||
error_str.contains("Todo list too large"),
|
||||
"Error should mention 'Todo list too large'"
|
||||
);
|
||||
assert!(
|
||||
error_str.contains("11 chars"),
|
||||
"Error should mention '11 chars'"
|
||||
);
|
||||
assert!(
|
||||
error_str.contains("max: 10"),
|
||||
"Error should mention 'max: 10'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::env::remove_var("GOOSE_TODO_MAX_CHARS");
|
||||
}
|
||||
Reference in New Issue
Block a user