diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 9243eff96bd3..6c38b9f4de57 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -271,6 +271,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { Ok(call) => match call.name.to_string().as_str() { "developer__text_editor" => render_text_editor_request(call, debug), "developer__shell" => render_shell_request(call, debug), + "code_execution__execute_code" => render_execute_code_request(call, debug), "subagent" => render_subagent_request(call, debug), "todo__write" => render_todo_request(call, debug), _ => render_default_request(call, debug), @@ -445,6 +446,61 @@ fn render_shell_request(call: &CallToolRequestParam, debug: bool) { println!(); } +fn render_execute_code_request(call: &CallToolRequestParam, debug: bool) { + let tool_graph = call + .arguments + .as_ref() + .and_then(|args| args.get("tool_graph")) + .and_then(Value::as_array) + .filter(|arr| !arr.is_empty()); + + let Some(tool_graph) = tool_graph else { + return render_default_request(call, debug); + }; + + let count = tool_graph.len(); + let plural = if count == 1 { "" } else { "s" }; + println!(); + println!( + "─── {} tool call{} | {} ──────────────────────────", + style(count).cyan(), + plural, + style("execute_code").magenta().dim() + ); + + for (i, node) in tool_graph.iter().filter_map(Value::as_object).enumerate() { + let tool = node + .get("tool") + .and_then(Value::as_str) + .unwrap_or("unknown"); + let desc = node + .get("description") + .and_then(Value::as_str) + .unwrap_or(""); + let deps: Vec<_> = node + .get("depends_on") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_u64) + .map(|d| (d + 1).to_string()) + .collect(); + let deps_str = if deps.is_empty() { + String::new() + } else { + format!(" (uses {})", deps.join(", ")) + }; + println!( + " {}. {}: {}{}", + style(i + 1).dim(), + style(tool).cyan(), + style(desc).green(), + style(deps_str).dim() + ); + } + println!(); +} + fn render_subagent_request(call: &CallToolRequestParam, debug: bool) { print_tool_header(call); diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index 5bbf6b547310..c2f6742553fe 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -31,10 +31,25 @@ type ToolCallRequest = ( tokio::sync::oneshot::Sender>, ); +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +struct ToolGraphNode { + /// Tool name in format "server/tool" (e.g., "developer/shell") + tool: String, + /// Brief description of what this call does (e.g., "list files in /src") + description: String, + /// Indices of nodes this depends on (empty if no dependencies) + #[serde(default)] + depends_on: Vec, +} + #[derive(Debug, Serialize, Deserialize, JsonSchema)] struct ExecuteCodeParams { /// JavaScript code with ES6 imports for MCP tools. code: String, + /// DAG of tool calls showing execution flow. Each node represents a tool call. + /// Use depends_on to show data flow (e.g., node 1 uses output from node 0). + #[serde(default)] + tool_graph: Vec, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -701,6 +716,7 @@ impl McpClientTrait for CodeExecutionClient { Err(Error::TransportClosed) } + #[allow(clippy::too_many_lines)] async fn list_tools( &self, _next_cursor: Option, @@ -746,6 +762,15 @@ impl McpClientTrait for CodeExecutionClient { - Last expression is the result - No comments in code + TOOL_GRAPH: Always provide tool_graph to describe the execution flow for the UI. + Each node has: tool (server/name), description (what it does), depends_on (indices of dependencies). + Example for chained operations: + [ + {"tool": "developer/shell", "description": "list files", "depends_on": []}, + {"tool": "developer/text_editor", "description": "read README.md", "depends_on": []}, + {"tool": "developer/text_editor", "description": "write output.txt", "depends_on": [0, 1]} + ] + BEFORE CALLING: Use the read_module tool to check required parameters. "#} .to_string(), diff --git a/crates/goose/src/providers/canonical/build_canonical_models.rs b/crates/goose/src/providers/canonical/build_canonical_models.rs index 667edb41b8e2..9b70646f0417 100644 --- a/crates/goose/src/providers/canonical/build_canonical_models.rs +++ b/crates/goose/src/providers/canonical/build_canonical_models.rs @@ -289,6 +289,7 @@ impl MappingReport { } } +#[allow(clippy::too_many_lines)] async fn build_canonical_models() -> Result<()> { println!("Fetching models from OpenRouter API..."); diff --git a/scripts/test_providers.sh b/scripts/test_providers.sh index 17c6e5941c3c..de2979137b70 100755 --- a/scripts/test_providers.sh +++ b/scripts/test_providers.sh @@ -55,8 +55,10 @@ fi if [ "$CODE_EXEC_MODE" = true ]; then echo "Mode: code_execution (JS batching)" BUILTINS="developer,code_execution" - # Match "execute_code | code_execution" or "read_module | code_execution" in output - SUCCESS_PATTERN="(execute_code \| code_execution)|(read_module \| code_execution)" + # Match code_execution tool usage: + # - "execute_code | code_execution" or "read_module | code_execution" (fallback format) + # - "tool call | execute_code" or "tool calls | execute_code" (new format with tool_graph) + SUCCESS_PATTERN="(execute_code \| code_execution)|(read_module \| code_execution)|(tool calls? \| execute_code)" SUCCESS_MSG="code_execution tool called" FAILURE_MSG="no code_execution tools called" else diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index b3d2e56d0ce9..739128155742 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -17,6 +17,12 @@ import MCPUIResourceRenderer from './MCPUIResourceRenderer'; import { isUIResource } from '@mcp-ui/client'; import { CallToolResponse, Content, EmbeddedResource } from '../api'; +interface ToolGraphNode { + tool: string; + description: string; + depends_on: number[]; +} + interface ToolCallWithResponseProps { isCancelledMessage: boolean; toolRequest: ToolRequestMessageContent; @@ -410,6 +416,20 @@ function ToolCallView({ case 'computer_control': return `poking around...`; + case 'execute_code': { + const toolGraph = args.tool_graph as unknown as ToolGraphNode[] | undefined; + if (toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0) { + if (toolGraph.length === 1) { + return `${toolGraph[0].description}`; + } + if (toolGraph.length === 2) { + return `${toolGraph[0].tool}, ${toolGraph[1].tool}`; + } + return `${toolGraph.length} tools used`; + } + return 'executing code'; + } + default: { // Generic fallback for unknown tools: ToolName + CompactArguments // This ensures any MCP tool works without explicit handling @@ -489,12 +509,34 @@ function ToolCallView({ ) } > - {/* Tool Details */} - {isToolDetails && ( -
- -
- )} + {(() => { + const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2); + const toolGraph = toolCall.arguments?.tool_graph as unknown as ToolGraphNode[] | undefined; + const code = toolCall.arguments?.code as unknown as string | undefined; + const hasToolGraph = + toolName === 'execute_code' && + toolGraph && + Array.isArray(toolGraph) && + toolGraph.length > 0; + + if (hasToolGraph) { + return ( +
+ +
+ ); + } + + if (isToolDetails) { + return ( +
+ +
+ ); + } + + return null; + })()} {logs && logs.length > 0 && (
@@ -553,6 +595,45 @@ function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) { ); } +interface ToolGraphViewProps { + toolGraph: ToolGraphNode[]; + code?: string; +} + +function ToolGraphView({ toolGraph, code }: ToolGraphViewProps) { + const renderGraph = () => { + if (toolGraph.length === 0) return null; + + const lines: string[] = []; + + toolGraph.forEach((node, index) => { + const deps = + node.depends_on.length > 0 ? ` (uses ${node.depends_on.map((d) => d + 1).join(', ')})` : ''; + lines.push(`${index + 1}. ${node.tool}: ${node.description}${deps}`); + }); + + return lines.join('\n'); + }; + + return ( +
+
{renderGraph()}
+ {code && ( +
+ Code} + isStartExpanded={false} + > +
+              {code}
+            
+
+
+ )} +
+ ); +} + interface ToolResultViewProps { result: Content; isStartExpanded: boolean;