diff --git a/Cargo.lock b/Cargo.lock index 587e8e017328..c505614db7c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2625,6 +2625,7 @@ dependencies = [ "regex", "reqwest 0.12.12", "rmcp", + "schemars", "serde", "serde_json", "serde_urlencoded", diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 4b524391c726..da53ae2420e5 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -101,6 +101,7 @@ tokio-util = "0.7.15" unicode-normalization = "0.1" oauth2 = "5.0.0" +schemars = { version = "1.0.4", default-features = false, features = ["derive"] } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } diff --git a/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs index 4174fc1055a4..721ccd45dd92 100644 --- a/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs +++ b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs @@ -12,72 +12,96 @@ use crate::agents::tool_execution::ToolCallResult; use crate::recipe::{Recipe, RecipeBuilder}; use anyhow::{anyhow, Result}; use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations}; -use rmcp::object; +use rmcp::schemars::{schema_for, JsonSchema}; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::borrow::Cow; pub const DYNAMIC_TASK_TOOL_NAME_PREFIX: &str = "dynamic_task__create_task"; +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CreateDynamicTaskParams { + /// Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required). + #[schemars(length(min = 1))] + pub task_parameters: Vec, + + /// How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task) + #[serde(skip_serializing_if = "Option::is_none")] + pub execution_mode: Option, +} + +/// Execution mode for tasks +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum ExecutionModeParam { + Sequential, + Parallel, +} + +impl From for ExecutionMode { + fn from(mode: ExecutionModeParam) -> Self { + match mode { + ExecutionModeParam::Sequential => ExecutionMode::Sequential, + ExecutionModeParam::Parallel => ExecutionMode::Parallel, + } + } +} + +/// Parameters for a single task +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct TaskParameter { + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub extensions: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub response: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub retry: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub activities: Option>, + + /// If true, return only the last message from the subagent (default: false, returns full conversation) + #[serde(skip_serializing_if = "Option::is_none")] + pub return_last_only: Option, +} + pub fn create_dynamic_task_tool() -> Tool { + let schema = schema_for!(CreateDynamicTaskParams); + let schema_value = + serde_json::to_value(schema).expect("Failed to serialize CreateDynamicTaskParams schema"); + + let input_schema = schema_value + .as_object() + .expect("Schema should be an object") + .clone(); + Tool::new( DYNAMIC_TASK_TOOL_NAME_PREFIX.to_string(), "Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, context, activities. Arrays for multiple tasks.".to_string(), - object!({ - "type": "object", - "properties": { - "task_parameters": { - "type": "array", - "description": "Array of tasks. Each task must have either 'instructions' OR 'prompt' field (at least one is required).", - "items": { - "type": "object", - "properties": { - // Either instructions or prompt is required (validated at runtime) - "instructions": { - "type": "string", - "description": "Task instructions (required if prompt is not provided)" - }, - "prompt": { - "type": "string", - "description": "Initial prompt (required if instructions is not provided)" - }, - // Optional - auto-generated if not provided - "title": {"type": "string"}, - "description": {"type": "string"}, - "extensions": { - "type": "array", - "items": {"type": "object"} - }, - "settings": {"type": "object"}, - "parameters": { - "type": "array", - "items": {"type": "object"} - }, - "response": {"type": "object"}, - "retry": {"type": "object"}, - "context": { - "type": "array", - "items": {"type": "string"} - }, - "activities": { - "type": "array", - "items": {"type": "string"} - }, - "return_last_only": { - "type": "boolean", - "description": "If true, return only the last message from the subagent (default: false, returns full conversation)" - } - } - }, - "minItems": 1 - }, - "execution_mode": { - "type": "string", - "enum": ["sequential", "parallel"], - "description": "How to execute multiple tasks (default: parallel for multiple tasks, sequential for single task)" - } - }, - "required": ["task_parameters"] - }) + input_schema, ).annotate(ToolAnnotations { title: Some("Create Dynamic Tasks".to_string()), read_only_hint: Some(false),