diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx index 94a17e6d..cd3ace49 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx @@ -196,7 +196,7 @@ describe('', () => { it('shows subagent execution display for task tool with proper result display', () => { const subagentResultDisplay = { - type: 'subagent_execution' as const, + type: 'task_execution' as const, subagentName: 'file-search', taskDescription: 'Search for files matching pattern', status: 'running' as const, diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index 70d2d6a7..abe5225a 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -34,7 +34,7 @@ type DisplayRendererResult = | { type: 'todo'; data: TodoResultDisplay } | { type: 'string'; data: string } | { type: 'diff'; data: { fileDiff: string; fileName: string } } - | { type: 'subagent_execution'; data: TaskResultDisplay }; + | { type: 'task'; data: TaskResultDisplay }; /** * Custom hook to determine the type of result display and return appropriate rendering info @@ -65,10 +65,10 @@ const useResultDisplayRenderer = ( typeof resultDisplay === 'object' && resultDisplay !== null && 'type' in resultDisplay && - resultDisplay.type === 'subagent_execution' + resultDisplay.type === 'task_execution' ) { return { - type: 'subagent_execution', + type: 'task', data: resultDisplay as TaskResultDisplay, }; } @@ -216,7 +216,7 @@ export const ToolMessage: React.FC = ({ {displayRenderer.type === 'todo' && ( )} - {displayRenderer.type === 'subagent_execution' && ( + {displayRenderer.type === 'task' && ( { {toolsDisplay} - {shouldShowColor(agent.backgroundColor) && ( + {shouldShowColor(agent.color) && ( Color: - - {` ${agent.name} `} - + {` ${agent.name} `} )} diff --git a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx index 4971ca3c..fa300e03 100644 --- a/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx +++ b/packages/cli/src/ui/components/subagents/AgentsManagerDialog.tsx @@ -248,7 +248,7 @@ export function AgentsManagerDialog({ return ( { // Save changes and reload agents @@ -258,7 +258,7 @@ export function AgentsManagerDialog({ const subagentManager = config.getSubagentManager(); await subagentManager.updateSubagent( selectedAgent.name, - { backgroundColor: color }, + { color }, selectedAgent.level, ); // Reload agents to get updated state diff --git a/packages/cli/src/ui/components/subagents/ColorSelector.tsx b/packages/cli/src/ui/components/subagents/ColorSelector.tsx index 3ffe061a..50af090f 100644 --- a/packages/cli/src/ui/components/subagents/ColorSelector.tsx +++ b/packages/cli/src/ui/components/subagents/ColorSelector.tsx @@ -14,7 +14,7 @@ import { COLOR_OPTIONS } from './constants.js'; const colorOptions: ColorOption[] = COLOR_OPTIONS; interface ColorSelectorProps { - backgroundColor?: string; + color?: string; agentName?: string; onSelect: (color: string) => void; } @@ -23,16 +23,16 @@ interface ColorSelectorProps { * Color selection with preview. */ export function ColorSelector({ - backgroundColor = 'auto', + color = 'auto', agentName = 'Agent', onSelect, }: ColorSelectorProps) { - const [selectedColor, setSelectedColor] = useState(backgroundColor); + const [selectedColor, setSelectedColor] = useState(color); - // Update selected color when backgroundColor prop changes + // Update selected color when color prop changes useEffect(() => { - setSelectedColor(backgroundColor); - }, [backgroundColor]); + setSelectedColor(color); + }, [color]); const handleSelect = (selectedValue: string) => { const colorOption = colorOptions.find( @@ -75,8 +75,8 @@ export function ColorSelector({ Preview: - - {` ${agentName} `} + + {` ${agentName} `} diff --git a/packages/cli/src/ui/components/subagents/CreationSummary.tsx b/packages/cli/src/ui/components/subagents/CreationSummary.tsx index 1b21df84..b2a6f0b5 100644 --- a/packages/cli/src/ui/components/subagents/CreationSummary.tsx +++ b/packages/cli/src/ui/components/subagents/CreationSummary.tsx @@ -122,7 +122,7 @@ export function CreationSummary({ tools: Array.isArray(state.selectedTools) ? state.selectedTools : undefined, - backgroundColor: state.backgroundColor, + color: state.color, }; // Create the subagent @@ -243,12 +243,12 @@ export function CreationSummary({ {toolsDisplay} - {shouldShowColor(state.backgroundColor) && ( + {shouldShowColor(state.color) && ( Color: - - {` ${state.generatedName} `} - + {` ${state.generatedName} `} )} diff --git a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx index 86992beb..126b3e4b 100644 --- a/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx +++ b/packages/cli/src/ui/components/subagents/SubagentCreationWizard.tsx @@ -207,7 +207,7 @@ export function SubagentCreationWizard({ case WIZARD_STEPS.COLOR_SELECTION: return ( { dispatch({ type: 'SET_BACKGROUND_COLOR', color }); @@ -230,7 +230,7 @@ export function SubagentCreationWizard({ stepProps, state.currentStep, state.selectedTools, - state.backgroundColor, + state.color, state.generatedName, config, handleNext, diff --git a/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx index 7068cb41..4796ece3 100644 --- a/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx +++ b/packages/cli/src/ui/components/subagents/SubagentExecutionDisplay.tsx @@ -4,15 +4,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; -import { TaskResultDisplay } from '@qwen-code/qwen-code-core'; +import { + TaskResultDisplay, + SubagentStatsSummary, +} from '@qwen-code/qwen-code-core'; +import { theme } from '../../semantic-colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import { COLOR_OPTIONS } from './constants.js'; +import { fmtDuration } from './utils.js'; + +export type DisplayMode = 'compact' | 'default' | 'verbose'; export interface SubagentExecutionDisplayProps { data: TaskResultDisplay; } +const getStatusColor = ( + status: TaskResultDisplay['status'] | 'executing' | 'success', +) => { + switch (status) { + case 'running': + case 'executing': + return theme.status.warning; + case 'completed': + case 'success': + return theme.status.success; + case 'failed': + return theme.status.error; + default: + return Colors.Gray; + } +}; + +const getStatusText = (status: TaskResultDisplay['status']) => { + switch (status) { + case 'running': + return 'Running'; + case 'completed': + return 'Completed'; + case 'failed': + return 'Failed'; + default: + return 'Unknown'; + } +}; + /** * Component to display subagent execution progress and results. * This is now a pure component that renders the provided SubagentExecutionResultDisplay data. @@ -20,61 +59,133 @@ export interface SubagentExecutionDisplayProps { */ export const SubagentExecutionDisplay: React.FC< SubagentExecutionDisplayProps -> = ({ data }) => ( - - {/* Header with subagent name and status */} - - - - {data.subagentName} - - - +> = ({ data }) => { + const [displayMode, setDisplayMode] = React.useState('default'); + + const agentColor = useMemo(() => { + const colorOption = COLOR_OPTIONS.find( + (option) => option.name === data.subagentColor, + ); + return colorOption?.value || theme.text.accent; + }, [data.subagentColor]); + + const footerText = React.useMemo(() => { + // This component only listens to keyboard shortcut events when the subagent is running + if (data.status !== 'running') return ''; + + if (displayMode === 'verbose') return 'Press ctrl+r to show less.'; + + if (displayMode === 'default') { + const hasMoreLines = data.taskPrompt.split('\n').length > 10; + const hasMoreToolCalls = data.toolCalls && data.toolCalls.length > 5; + + if (hasMoreToolCalls || hasMoreLines) { + return 'Press ctrl+s to show more.'; + } + return ''; + } + return ''; + }, [displayMode, data.toolCalls, data.taskPrompt, data.status]); + + // Handle ctrl+s and ctrl+r keypresses to control display mode + useKeypress( + (key) => { + if (key.ctrl && key.name === 's') { + setDisplayMode((current) => + current === 'default' ? 'verbose' : 'verbose', + ); + } else if (key.ctrl && key.name === 'r') { + setDisplayMode((current) => + current === 'verbose' ? 'default' : 'default', + ); + } + }, + { isActive: true }, + ); + + return ( + + {/* Header with subagent name and status */} + + + {data.subagentName} + + + + + + {/* Task description */} + + + {/* Progress section for running tasks */} + {data.status === 'running' && + data.toolCalls && + data.toolCalls.length > 0 && ( + + + + )} + + {/* Results section for completed/failed tasks */} + {(data.status === 'completed' || data.status === 'failed') && ( + + )} + + {/* Footer with keyboard shortcuts */} + {footerText && ( + + {footerText} + + )} + ); +}; - {/* Task description */} - - Task: - {data.taskDescription} +/** + * Task prompt section with truncation support + */ +const TaskPromptSection: React.FC<{ + taskPrompt: string; + displayMode: DisplayMode; +}> = ({ taskPrompt, displayMode }) => { + const lines = taskPrompt.split('\n'); + const shouldTruncate = lines.length > 10; + const showFull = displayMode === 'verbose'; + const displayLines = showFull ? lines : lines.slice(0, 10); + + return ( + + + Task Detail: + {shouldTruncate && displayMode === 'default' && ( + Showing the first 10 lines. + )} + + + + {displayLines.join('\n') + (shouldTruncate && !showFull ? '...' : '')} + + - - {/* Progress section for running tasks */} - {data.status === 'running' && ( - - )} - - {/* Results section for completed/failed tasks */} - {(data.status === 'completed' || data.status === 'failed') && ( - - )} - -); + ); +}; /** * Status dot component with similar height as text */ const StatusDot: React.FC<{ status: TaskResultDisplay['status']; -}> = ({ status }) => { - const color = React.useMemo(() => { - switch (status) { - case 'running': - return Colors.AccentYellow; - case 'completed': - return Colors.AccentGreen; - case 'failed': - return Colors.AccentRed; - default: - return Colors.Gray; - } - }, [status]); - - return ( - - - - ); -}; +}> = ({ status }) => ( + + + +); /** * Status indicator component @@ -82,109 +193,95 @@ const StatusDot: React.FC<{ const StatusIndicator: React.FC<{ status: TaskResultDisplay['status']; }> = ({ status }) => { - switch (status) { - case 'running': - return Running; - case 'completed': - return Completed; - case 'failed': - return Failed; - default: - return Unknown; - } + const color = getStatusColor(status); + const text = getStatusText(status); + return {text}; }; /** - * Progress section for running executions + * Tool calls list - format consistent with ToolInfo in ToolMessage.tsx */ -const ProgressSection: React.FC<{ - progress: { - toolCalls?: Array<{ - name: string; - status: 'executing' | 'success' | 'failed'; - error?: string; - args?: Record; - result?: string; - returnDisplay?: string; - }>; - }; -}> = ({ progress }) => ( - - {progress.toolCalls && progress.toolCalls.length > 0 && ( - - )} - -); +const ToolCallsList: React.FC<{ + toolCalls: TaskResultDisplay['toolCalls']; + displayMode: DisplayMode; +}> = ({ toolCalls, displayMode }) => { + const calls = toolCalls || []; + const shouldTruncate = calls.length > 5; + const showAll = displayMode === 'verbose'; + const displayCalls = showAll ? calls : calls.slice(-5); // Show last 5 -/** - * Clean tool calls list - format consistent with ToolInfo in ToolMessage.tsx - */ -const CleanToolCallsList: React.FC<{ - toolCalls: Array<{ - name: string; - status: 'executing' | 'success' | 'failed'; - error?: string; - args?: Record; - result?: string; - returnDisplay?: string; - }>; -}> = ({ toolCalls }) => ( - - - Tools: + // Reverse the order to show most recent first + const reversedDisplayCalls = [...displayCalls].reverse(); + + return ( + + + Tools: + {shouldTruncate && displayMode === 'default' && ( + + {' '} + Showing the last 5 of {calls.length} tools. + + )} + + {reversedDisplayCalls.map((toolCall, index) => ( + + ))} - {toolCalls.map((toolCall, index) => ( - - ))} - -); + ); +}; /** * Individual tool call item - consistent with ToolInfo format */ -const CleanToolCallItem: React.FC<{ +const ToolCallItem: React.FC<{ toolCall: { name: string; status: 'executing' | 'success' | 'failed'; error?: string; args?: Record; result?: string; - returnDisplay?: string; + resultDisplay?: string; + description?: string; }; }> = ({ toolCall }) => { const STATUS_INDICATOR_WIDTH = 3; // Map subagent status to ToolCallStatus-like display const statusIcon = React.useMemo(() => { + const color = getStatusColor(toolCall.status); switch (toolCall.status) { case 'executing': - return ; // Using same as ToolMessage + return ; // Using same as ToolMessage case 'success': - return ; + return ; case 'failed': return ( - + x ); default: - return o; + return o; } }, [toolCall.status]); - const description = getToolDescription(toolCall); - - // Get first line of returnDisplay for truncated output - const truncatedOutput = React.useMemo(() => { - if (!toolCall.returnDisplay) return ''; - const firstLine = toolCall.returnDisplay.split('\n')[0]; + const description = React.useMemo(() => { + if (!toolCall.description) return ''; + const firstLine = toolCall.description.split('\n')[0]; return firstLine.length > 80 ? firstLine.substring(0, 80) + '...' : firstLine; - }, [toolCall.returnDisplay]); + }, [toolCall.description]); + + // Get first line of resultDisplay for truncated output + const truncatedOutput = React.useMemo(() => { + if (!toolCall.resultDisplay) return ''; + const firstLine = toolCall.resultDisplay.split('\n')[0]; + return firstLine.length > 80 + ? firstLine.substring(0, 80) + '...' + : firstLine; + }, [toolCall.resultDisplay]); return ( @@ -197,7 +294,7 @@ const CleanToolCallItem: React.FC<{ {' '} {description} {toolCall.error && ( - - {toolCall.error} + - {toolCall.error} )} @@ -212,59 +309,16 @@ const CleanToolCallItem: React.FC<{ ); }; -/** - * Helper function to get tool description from args - */ -const getToolDescription = (toolCall: { - name: string; - args?: Record; -}): string => { - if (!toolCall.args) return ''; - - // Handle common tool patterns - if (toolCall.name === 'Glob' && toolCall.args['glob_pattern']) { - return `"${toolCall.args['glob_pattern']}"`; - } - if (toolCall.name === 'ReadFile' && toolCall.args['target_file']) { - const path = toolCall.args['target_file'] as string; - return path.split('/').pop() || path; - } - if (toolCall.name === 'SearchFileContent' && toolCall.args['pattern']) { - return `"${toolCall.args['pattern']}"`; - } - - // Generic fallback - const firstArg = Object.values(toolCall.args)[0]; - if (typeof firstArg === 'string' && firstArg.length < 50) { - return firstArg; - } - - return ''; -}; - /** * Execution summary details component */ const ExecutionSummaryDetails: React.FC<{ data: TaskResultDisplay; -}> = ({ data }) => { - // Parse execution summary for structured data - const summaryData = React.useMemo(() => { - if (!data.executionSummary) return null; + displayMode: DisplayMode; +}> = ({ data, displayMode: _displayMode }) => { + const stats = data.executionSummary; - // Try to extract structured data from execution summary - const durationMatch = data.executionSummary.match(/Duration:\s*([^\n]+)/i); - const roundsMatch = data.executionSummary.match(/Rounds:\s*(\d+)/i); - const tokensMatch = data.executionSummary.match(/Tokens:\s*([\d,]+)/i); - - return { - duration: durationMatch?.[1] || 'N/A', - rounds: roundsMatch?.[1] || 'N/A', - tokens: tokensMatch?.[1] || 'N/A', - }; - }, [data.executionSummary]); - - if (!summaryData) { + if (!stats) { return ( • No summary available @@ -275,13 +329,13 @@ const ExecutionSummaryDetails: React.FC<{ return ( - • Duration: {summaryData.duration} + • Duration: {fmtDuration(stats.totalDurationMs)} - • Rounds: {summaryData.rounds} + • Rounds: {stats.rounds} - • Tokens: {summaryData.tokens} + • Tokens: {stats.totalTokens.toLocaleString()} ); @@ -291,37 +345,35 @@ const ExecutionSummaryDetails: React.FC<{ * Tool usage statistics component */ const ToolUsageStats: React.FC<{ - toolCalls: Array<{ - name: string; - status: 'executing' | 'success' | 'failed'; - error?: string; - args?: Record; - result?: string; - returnDisplay?: string; - }>; -}> = ({ toolCalls }) => { - const stats = React.useMemo(() => { - const total = toolCalls.length; - const successful = toolCalls.filter( - (call) => call.status === 'success', - ).length; - const failed = toolCalls.filter((call) => call.status === 'failed').length; - const successRate = - total > 0 ? ((successful / total) * 100).toFixed(1) : '0.0'; - - return { total, successful, failed, successRate }; - }, [toolCalls]); + executionSummary?: SubagentStatsSummary; +}> = ({ executionSummary }) => { + if (!executionSummary) { + return ( + + • No tool usage data available + + ); + } return ( - • Total Calls: {stats.total} + • Total Calls: {executionSummary.totalToolCalls} Success Rate:{' '} - {stats.successRate}% ( - {stats.successful} success,{' '} - {stats.failed} failed) + + {executionSummary.successRate.toFixed(1)}% + {' '} + ( + + {executionSummary.successfulToolCalls} success + + ,{' '} + + {executionSummary.failedToolCalls} failed + + ) ); @@ -332,47 +384,39 @@ const ToolUsageStats: React.FC<{ */ const ResultsSection: React.FC<{ data: TaskResultDisplay; -}> = ({ data }) => ( - + displayMode: DisplayMode; +}> = ({ data, displayMode }) => ( + {/* Tool calls section - clean list format */} - {data.progress?.toolCalls && data.progress.toolCalls.length > 0 && ( - + {data.toolCalls && data.toolCalls.length > 0 && ( + )} - {/* Task Completed section */} - - 📄 - Task Completed: - {data.taskDescription} - - {/* Execution Summary section */} - + - 📊 - + Execution Summary: - + {/* Tool Usage section */} - {data.progress?.toolCalls && data.progress.toolCalls.length > 0 && ( + {data.executionSummary && ( - 🔧 - + Tool Usage: - + )} {/* Error reason for failed tasks */} {data.status === 'failed' && data.terminateReason && ( - + ❌ Failed: {data.terminateReason} diff --git a/packages/cli/src/ui/components/subagents/reducers.tsx b/packages/cli/src/ui/components/subagents/reducers.tsx index ff31fee7..ebd96ffc 100644 --- a/packages/cli/src/ui/components/subagents/reducers.tsx +++ b/packages/cli/src/ui/components/subagents/reducers.tsx @@ -19,7 +19,7 @@ export const initialWizardState: CreationWizardState = { generatedDescription: '', generatedName: '', selectedTools: [], - backgroundColor: 'auto', + color: 'auto', isGenerating: false, validationErrors: [], canProceed: false, @@ -84,7 +84,7 @@ export function wizardReducer( case 'SET_BACKGROUND_COLOR': return { ...state, - backgroundColor: action.color, + color: action.color, canProceed: true, }; @@ -157,7 +157,7 @@ function validateStep(step: number, state: CreationWizardState): boolean { return true; // Always can proceed from tool selection case WIZARD_STEPS.FINAL_CONFIRMATION: // Final confirmation - return state.backgroundColor.length > 0; + return state.color.length > 0; default: return false; diff --git a/packages/cli/src/ui/components/subagents/types.ts b/packages/cli/src/ui/components/subagents/types.ts index 54173292..edad9656 100644 --- a/packages/cli/src/ui/components/subagents/types.ts +++ b/packages/cli/src/ui/components/subagents/types.ts @@ -34,8 +34,8 @@ export interface CreationWizardState { /** Selected tools for the subagent */ selectedTools: string[]; - /** Background color for runtime display */ - backgroundColor: string; + /** Color for runtime display */ + color: string; /** Whether LLM generation is in progress */ isGenerating: boolean; @@ -108,7 +108,7 @@ export interface WizardResult { systemPrompt: string; location: SubagentLevel; tools?: string[]; - backgroundColor: string; + color: string; } export const MANAGEMENT_STEPS = { diff --git a/packages/cli/src/ui/components/subagents/utils.ts b/packages/cli/src/ui/components/subagents/utils.ts index 4259c261..448719b4 100644 --- a/packages/cli/src/ui/components/subagents/utils.ts +++ b/packages/cli/src/ui/components/subagents/utils.ts @@ -1,7 +1,7 @@ import { COLOR_OPTIONS } from './constants.js'; -export const shouldShowColor = (backgroundColor?: string): boolean => - backgroundColor !== undefined && backgroundColor !== 'auto'; +export const shouldShowColor = (color?: string): boolean => + color !== undefined && color !== 'auto'; export const getColorForDisplay = (colorName?: string): string | undefined => !colorName || colorName === 'auto' @@ -20,3 +20,16 @@ export function sanitizeInput(input: string): string { .replace(/\s+/g, ' ') // Normalize whitespace ); // Limit length } + +export function fmtDuration(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + if (ms < 3600000) { + const m = Math.floor(ms / 60000); + const s = Math.floor((ms % 60000) / 1000); + return `${m}m ${s}s`; + } + const h = Math.floor(ms / 3600000); + const m = Math.floor((ms % 3600000) / 60000); + return `${h}h ${m}m`; +} diff --git a/packages/core/src/subagents/index.ts b/packages/core/src/subagents/index.ts index d427b9fa..c24d9774 100644 --- a/packages/core/src/subagents/index.ts +++ b/packages/core/src/subagents/index.ts @@ -47,7 +47,7 @@ export type { ToolConfig, SubagentTerminateMode, OutputObject, -} from './subagent.js'; +} from './types.js'; export { SubAgentScope } from './subagent.js'; @@ -55,11 +55,19 @@ export { SubAgentScope } from './subagent.js'; export type { SubAgentEvent, SubAgentStartEvent, - SubAgentFinishEvent, SubAgentRoundEvent, + SubAgentStreamTextEvent, SubAgentToolCallEvent, SubAgentToolResultEvent, - SubAgentModelTextEvent, + SubAgentFinishEvent, + SubAgentErrorEvent, } from './subagent-events.js'; export { SubAgentEventEmitter } from './subagent-events.js'; + +// Statistics and formatting +export type { + SubagentStatsSummary, + ToolUsageStats, +} from './subagent-statistics.js'; +export { formatCompact, formatDetailed } from './subagent-result-format.js'; diff --git a/packages/core/src/subagents/subagent-events.ts b/packages/core/src/subagents/subagent-events.ts index 520b8bd1..1ae90c86 100644 --- a/packages/core/src/subagents/subagent-events.ts +++ b/packages/core/src/subagents/subagent-events.ts @@ -10,17 +10,21 @@ export type SubAgentEvent = | 'start' | 'round_start' | 'round_end' - | 'model_text' + | 'stream_text' | 'tool_call' | 'tool_result' | 'finish' | 'error'; -export interface SubAgentModelTextEvent { - subagentId: string; - round: number; - text: string; - timestamp: number; +export enum SubAgentEventType { + START = 'start', + ROUND_START = 'round_start', + ROUND_END = 'round_end', + STREAM_TEXT = 'stream_text', + TOOL_CALL = 'tool_call', + TOOL_RESULT = 'tool_result', + FINISH = 'finish', + ERROR = 'error', } export interface SubAgentStartEvent { @@ -38,12 +42,20 @@ export interface SubAgentRoundEvent { timestamp: number; } +export interface SubAgentStreamTextEvent { + subagentId: string; + round: number; + text: string; + timestamp: number; +} + export interface SubAgentToolCallEvent { subagentId: string; round: number; callId: string; name: string; args: Record; + description: string; timestamp: number; } @@ -54,6 +66,7 @@ export interface SubAgentToolResultEvent { name: string; success: boolean; error?: string; + resultDisplay?: string; durationMs?: number; timestamp: number; } @@ -72,6 +85,12 @@ export interface SubAgentFinishEvent { totalTokens?: number; } +export interface SubAgentErrorEvent { + subagentId: string; + error: string; + timestamp: number; +} + export class SubAgentEventEmitter { private ee = new EventEmitter(); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index 9970289f..339bc522 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -21,15 +21,13 @@ import { CreateSubagentOptions, SubagentError, SubagentErrorCode, -} from './types.js'; -import { SubagentValidator } from './validation.js'; -import { - SubAgentScope, PromptConfig, ModelConfig, RunConfig, ToolConfig, -} from './subagent.js'; +} from './types.js'; +import { SubagentValidator } from './validation.js'; +import { SubAgentScope } from './subagent.js'; import { Config } from '../config/config.js'; const QWEN_CONFIG_DIR = '.qwen'; @@ -369,9 +367,7 @@ export class SubagentManager { const runConfig = frontmatter['runConfig'] as | Record | undefined; - const backgroundColor = frontmatter['backgroundColor'] as - | string - | undefined; + const color = frontmatter['color'] as string | undefined; // Determine level from file path // Project level paths contain the project root, user level paths are in home directory @@ -387,11 +383,9 @@ export class SubagentManager { systemPrompt: systemPrompt.trim(), level, filePath, - modelConfig: modelConfig as Partial< - import('./subagent.js').ModelConfig - >, - runConfig: runConfig as Partial, - backgroundColor, + modelConfig: modelConfig as Partial, + runConfig: runConfig as Partial, + color, }; // Validate the parsed configuration @@ -436,8 +430,8 @@ export class SubagentManager { frontmatter['runConfig'] = config.runConfig; } - if (config.backgroundColor && config.backgroundColor !== 'auto') { - frontmatter['backgroundColor'] = config.backgroundColor; + if (config.color && config.color !== 'auto') { + frontmatter['color'] = config.color; } // Serialize to YAML diff --git a/packages/core/src/subagents/subagent-result-format.ts b/packages/core/src/subagents/subagent-result-format.ts index 213e0924..8558b492 100644 --- a/packages/core/src/subagents/subagent-result-format.ts +++ b/packages/core/src/subagents/subagent-result-format.ts @@ -4,17 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export interface SubAgentBasicStats { - rounds: number; - totalDurationMs: number; - totalToolCalls: number; - successfulToolCalls: number; - failedToolCalls: number; - successRate?: number; - inputTokens?: number; - outputTokens?: number; - totalTokens?: number; -} +import { SubagentStatsSummary } from './subagent-statistics.js'; function fmtDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; @@ -30,7 +20,7 @@ function fmtDuration(ms: number): string { } export function formatCompact( - stats: SubAgentBasicStats, + stats: SubagentStatsSummary, taskDesc: string, ): string { const sr = @@ -52,16 +42,7 @@ export function formatCompact( } export function formatDetailed( - stats: SubAgentBasicStats & { - toolUsage?: Array<{ - name: string; - count: number; - success: number; - failure: number; - lastError?: string; - averageDurationMs?: number; - }>; - }, + stats: SubagentStatsSummary, taskDesc: string, ): string { const sr = @@ -118,18 +99,7 @@ export function formatDetailed( return lines.join('\n'); } -export function generatePerformanceTips( - stats: SubAgentBasicStats & { - toolUsage?: Array<{ - name: string; - count: number; - success: number; - failure: number; - lastError?: string; - averageDurationMs?: number; - }>; - }, -): string[] { +export function generatePerformanceTips(stats: SubagentStatsSummary): string[] { const tips: string[] = []; const totalCalls = stats.totalToolCalls; const sr = diff --git a/packages/core/src/subagents/subagent-statistics.ts b/packages/core/src/subagents/subagent-statistics.ts index 35529bb9..bd32b326 100644 --- a/packages/core/src/subagents/subagent-statistics.ts +++ b/packages/core/src/subagents/subagent-statistics.ts @@ -14,7 +14,7 @@ export interface ToolUsageStats { averageDurationMs: number; } -export interface SubagentSummary { +export interface SubagentStatsSummary { rounds: number; totalDurationMs: number; totalToolCalls: number; @@ -79,7 +79,7 @@ export class SubagentStatistics { this.outputTokens += Math.max(0, output || 0); } - getSummary(now = Date.now()): SubagentSummary { + getSummary(now = Date.now()): SubagentStatsSummary { const totalDurationMs = this.startTimeMs ? now - this.startTimeMs : 0; const totalToolCalls = this.totalToolCalls; const successRate = diff --git a/packages/core/src/subagents/subagent.test.ts b/packages/core/src/subagents/subagent.test.ts index 1e253fae..bcea499f 100644 --- a/packages/core/src/subagents/subagent.test.ts +++ b/packages/core/src/subagents/subagent.test.ts @@ -5,15 +5,14 @@ */ import { vi, describe, it, expect, beforeEach, Mock, afterEach } from 'vitest'; +import { ContextState, SubAgentScope } from './subagent.js'; import { - ContextState, - SubAgentScope, SubagentTerminateMode, PromptConfig, ModelConfig, RunConfig, ToolConfig, -} from './subagent.js'; +} from './types.js'; import { Config, ConfigParameters } from '../config/config.js'; import { GeminiChat } from '../core/geminiChat.js'; import { createContentGenerator } from '../core/contentGenerator.js'; diff --git a/packages/core/src/subagents/subagent.ts b/packages/core/src/subagents/subagent.ts index 5d3ac257..3065d1af 100644 --- a/packages/core/src/subagents/subagent.ts +++ b/packages/core/src/subagents/subagent.ts @@ -19,9 +19,30 @@ import { GenerateContentResponseUsageMetadata, } from '@google/genai'; import { GeminiChat } from '../core/geminiChat.js'; -import { SubAgentEventEmitter } from './subagent-events.js'; -import { formatCompact, formatDetailed } from './subagent-result-format.js'; -import { SubagentStatistics } from './subagent-statistics.js'; +import { + OutputObject, + SubagentTerminateMode, + PromptConfig, + ModelConfig, + RunConfig, + ToolConfig, +} from './types.js'; +import { + SubAgentEventEmitter, + SubAgentEventType, + SubAgentFinishEvent, + SubAgentRoundEvent, + SubAgentStartEvent, + SubAgentToolCallEvent, + SubAgentToolResultEvent, + SubAgentStreamTextEvent, + SubAgentErrorEvent, +} from './subagent-events.js'; +import { formatCompact } from './subagent-result-format.js'; +import { + SubagentStatistics, + SubagentStatsSummary, +} from './subagent-statistics.js'; import { SubagentHooks } from './subagent-hooks.js'; import { logSubagentExecution } from '../telemetry/loggers.js'; import { SubagentExecutionEvent } from '../telemetry/types.js'; @@ -46,114 +67,6 @@ interface ExecutionStats { estimatedCost?: number; } -/** - * Describes the possible termination modes for a subagent. - * This enum provides a clear indication of why a subagent's execution might have ended. - */ -export enum SubagentTerminateMode { - /** - * Indicates that the subagent's execution terminated due to an unrecoverable error. - */ - ERROR = 'ERROR', - /** - * Indicates that the subagent's execution terminated because it exceeded the maximum allowed working time. - */ - TIMEOUT = 'TIMEOUT', - /** - * Indicates that the subagent's execution successfully completed all its defined goals. - */ - GOAL = 'GOAL', - /** - * Indicates that the subagent's execution terminated because it exceeded the maximum number of turns. - */ - MAX_TURNS = 'MAX_TURNS', -} - -/** - * Represents the output structure of a subagent's execution. - * This interface defines the data that a subagent will return upon completion, - * including the final result and the reason for its termination. - */ -export interface OutputObject { - /** - * The final result text returned by the subagent upon completion. - * This contains the direct output from the model's final response. - */ - result: string; - /** - * The reason for the subagent's termination, indicating whether it completed - * successfully, timed out, or encountered an error. - */ - terminate_reason: SubagentTerminateMode; -} - -/** - * Configures the initial prompt for the subagent. - */ -export interface PromptConfig { - /** - * A single system prompt string that defines the subagent's persona and instructions. - * Note: You should use either `systemPrompt` or `initialMessages`, but not both. - */ - systemPrompt?: string; - - /** - * An array of user/model content pairs to seed the chat history for few-shot prompting. - * Note: You should use either `systemPrompt` or `initialMessages`, but not both. - */ - initialMessages?: Content[]; -} - -/** - * Configures the tools available to the subagent during its execution. - */ -export interface ToolConfig { - /** - * A list of tool names (from the tool registry) or full function declarations - * that the subagent is permitted to use. - */ - tools: Array; -} - -/** - * Configures the generative model parameters for the subagent. - * This interface specifies the model to be used and its associated generation settings, - * such as temperature and top-p values, which influence the creativity and diversity of the model's output. - */ -export interface ModelConfig { - /** - * The name or identifier of the model to be used (e.g., 'gemini-2.5-pro'). - * - * TODO: In the future, this needs to support 'auto' or some other string to support routing use cases. - */ - model?: string; - /** - * The temperature for the model's sampling process. - */ - temp?: number; - /** - * The top-p value for nucleus sampling. - */ - top_p?: number; -} - -/** - * Configures the execution environment and constraints for the subagent. - * This interface defines parameters that control the subagent's runtime behavior, - * such as maximum execution time, to prevent infinite loops or excessive resource consumption. - * - * TODO: Consider adding max_tokens as a form of budgeting. - */ -export interface RunConfig { - /** The maximum execution time for the subagent in minutes. */ - max_time_minutes?: number; - /** - * The maximum number of conversational turns (a user message + model response) - * before the execution is terminated. Helps prevent infinite loops. - */ - max_turns?: number; -} - /** * Manages the runtime context state for the subagent. * This class provides a mechanism to store and retrieve key-value pairs @@ -450,7 +363,7 @@ export class SubAgentScope { let turnCounter = 0; try { // Emit start event - this.eventEmitter?.emit('start', { + this.eventEmitter?.emit(SubAgentEventType.START, { subagentId: this.subagentId, name: this.name, model: this.modelConfig.model, @@ -458,7 +371,7 @@ export class SubAgentScope { typeof t === 'string' ? t : t.name, ), timestamp: Date.now(), - }); + } as SubAgentStartEvent); // Log telemetry for subagent start const startEvent = new SubagentExecutionEvent(this.name, 'started'); @@ -494,12 +407,12 @@ export class SubAgentScope { messageParams, promptId, ); - this.eventEmitter?.emit('round_start', { + this.eventEmitter?.emit(SubAgentEventType.ROUND_START, { subagentId: this.subagentId, round: turnCounter, promptId, timestamp: Date.now(), - }); + } as SubAgentRoundEvent); const functionCalls: FunctionCall[] = []; let roundText = ''; @@ -514,12 +427,12 @@ export class SubAgentScope { const txt = (p as Part & { text?: string }).text; if (txt) roundText += txt; if (txt) - this.eventEmitter?.emit('model_text', { + this.eventEmitter?.emit(SubAgentEventType.STREAM_TEXT, { subagentId: this.subagentId, round: turnCounter, text: txt, timestamp: Date.now(), - }); + } as SubAgentStreamTextEvent); } if (resp.usageMetadata) lastUsage = resp.usageMetadata; } @@ -565,6 +478,7 @@ export class SubAgentScope { functionCalls, abortController, promptId, + turnCounter, ); } else { // No tool calls — treat this as the model's final answer. @@ -586,21 +500,21 @@ export class SubAgentScope { }, ]; } - this.eventEmitter?.emit('round_end', { + this.eventEmitter?.emit(SubAgentEventType.ROUND_END, { subagentId: this.subagentId, round: turnCounter, promptId, timestamp: Date.now(), - }); + } as SubAgentRoundEvent); } } catch (error) { console.error('Error during subagent execution:', error); this.output.terminate_reason = SubagentTerminateMode.ERROR; - this.eventEmitter?.emit('error', { + this.eventEmitter?.emit(SubAgentEventType.ERROR, { subagentId: this.subagentId, error: error instanceof Error ? error.message : String(error), timestamp: Date.now(), - }); + } as SubAgentErrorEvent); // Log telemetry for subagent error const errorEvent = new SubagentExecutionEvent(this.name, 'failed', { @@ -614,7 +528,7 @@ export class SubAgentScope { if (externalSignal) externalSignal.removeEventListener('abort', onAbort); this.executionStats.totalDurationMs = Date.now() - startTime; const summary = this.stats.getSummary(Date.now()); - this.eventEmitter?.emit('finish', { + this.eventEmitter?.emit(SubAgentEventType.FINISH, { subagentId: this.subagentId, terminate_reason: this.output.terminate_reason, timestamp: Date.now(), @@ -626,7 +540,7 @@ export class SubAgentScope { inputTokens: summary.inputTokens, outputTokens: summary.outputTokens, totalTokens: summary.totalTokens, - }); + } as SubAgentFinishEvent); // Log telemetry for subagent completion const completionEvent = new SubagentExecutionEvent( @@ -637,7 +551,8 @@ export class SubAgentScope { { terminate_reason: this.output.terminate_reason, result: this.finalText, - execution_summary: this.formatCompactResult( + execution_summary: formatCompact( + summary, 'Subagent execution completed', ), }, @@ -669,6 +584,7 @@ export class SubAgentScope { functionCalls: FunctionCall[], abortController: AbortController, promptId: string, + currentRound: number, ): Promise { const toolResponseParts: Part[] = []; @@ -683,6 +599,20 @@ export class SubAgentScope { prompt_id: promptId, }; + // Get tool description before execution + const description = this.getToolDescription(toolName, requestInfo.args); + + // Emit tool call event BEFORE execution + this.eventEmitter?.emit(SubAgentEventType.TOOL_CALL, { + subagentId: this.subagentId, + round: currentRound, + callId, + name: toolName, + args: requestInfo.args, + description, + timestamp: Date.now(), + } as SubAgentToolCallEvent); + // Execute tools with timing and hooks const start = Date.now(); await this.hooks?.preToolUse?.({ @@ -717,13 +647,7 @@ export class SubAgentScope { tu.count += 1; if (toolResponse?.error) { tu.failure += 1; - const disp = - typeof toolResponse.resultDisplay === 'string' - ? toolResponse.resultDisplay - : toolResponse.resultDisplay - ? JSON.stringify(toolResponse.resultDisplay) - : undefined; - tu.lastError = disp || toolResponse.error?.message || 'Unknown error'; + tu.lastError = toolResponse.error?.message || 'Unknown error'; } else { tu.success += 1; } @@ -737,31 +661,22 @@ export class SubAgentScope { } this.toolUsage.set(toolName, tu); - // Emit tool call/result events - this.eventEmitter?.emit('tool_call', { + // Emit tool result event + this.eventEmitter?.emit(SubAgentEventType.TOOL_RESULT, { subagentId: this.subagentId, - round: this.executionStats.rounds, - callId, - name: toolName, - args: requestInfo.args, - timestamp: Date.now(), - }); - this.eventEmitter?.emit('tool_result', { - subagentId: this.subagentId, - round: this.executionStats.rounds, + round: currentRound, callId, name: toolName, success: !toolResponse?.error, - error: toolResponse?.error + error: toolResponse?.error?.message, + resultDisplay: toolResponse?.resultDisplay ? typeof toolResponse.resultDisplay === 'string' ? toolResponse.resultDisplay - : toolResponse.resultDisplay - ? JSON.stringify(toolResponse.resultDisplay) - : toolResponse.error.message + : JSON.stringify(toolResponse.resultDisplay) : undefined, durationMs: duration, timestamp: Date.now(), - }); + } as SubAgentToolResultEvent); // Update statistics service this.stats.recordToolCall( @@ -779,19 +694,13 @@ export class SubAgentScope { args: requestInfo.args, success: !toolResponse?.error, durationMs: duration, - errorMessage: toolResponse?.error - ? typeof toolResponse.resultDisplay === 'string' - ? toolResponse.resultDisplay - : toolResponse.resultDisplay - ? JSON.stringify(toolResponse.resultDisplay) - : toolResponse.error.message - : undefined, + errorMessage: toolResponse?.error?.message, timestamp: Date.now(), }); if (toolResponse.error) { console.error( - `Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, + `Error executing tool ${functionCall.name}: ${toolResponse.error.message}`, ); } @@ -836,47 +745,14 @@ export class SubAgentScope { }; } - formatCompactResult(taskDesc: string, _useColors = false) { - const stats = this.getStatistics(); - return formatCompact( - { - rounds: stats.rounds, - totalDurationMs: stats.totalDurationMs, - totalToolCalls: stats.totalToolCalls, - successfulToolCalls: stats.successfulToolCalls, - failedToolCalls: stats.failedToolCalls, - successRate: stats.successRate, - inputTokens: this.executionStats.inputTokens, - outputTokens: this.executionStats.outputTokens, - totalTokens: this.executionStats.totalTokens, - }, - taskDesc, - ); + getExecutionSummary(): SubagentStatsSummary { + return this.stats.getSummary(); } getFinalText(): string { return this.finalText; } - formatDetailedResult(taskDesc: string) { - const stats = this.getStatistics(); - return formatDetailed( - { - rounds: stats.rounds, - totalDurationMs: stats.totalDurationMs, - totalToolCalls: stats.totalToolCalls, - successfulToolCalls: stats.successfulToolCalls, - failedToolCalls: stats.failedToolCalls, - successRate: stats.successRate, - inputTokens: this.executionStats.inputTokens, - outputTokens: this.executionStats.outputTokens, - totalTokens: this.executionStats.totalTokens, - toolUsage: stats.toolUsage, - }, - taskDesc, - ); - } - private async createChatObject(context: ContextState) { if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) { throw new Error( @@ -944,6 +820,33 @@ export class SubAgentScope { } } + /** + * Safely retrieves the description of a tool by attempting to build it. + * Returns an empty string if any error occurs during the process. + * + * @param toolName The name of the tool to get description for. + * @param args The arguments that would be passed to the tool. + * @returns The tool description or empty string if error occurs. + */ + private getToolDescription( + toolName: string, + args: Record, + ): string { + try { + const toolRegistry = this.runtimeContext.getToolRegistry(); + const tool = toolRegistry.getTool(toolName); + if (!tool) { + return ''; + } + + const toolInstance = tool.build(args); + return toolInstance.getDescription() || ''; + } catch { + // Safely ignore all runtime errors and return empty string + return ''; + } + } + private buildChatSystemPrompt(context: ContextState): string { if (!this.promptConfig.systemPrompt) { // This should ideally be caught in createChatObject, but serves as a safeguard. diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts index 5236b766..3be51f16 100644 --- a/packages/core/src/subagents/types.ts +++ b/packages/core/src/subagents/types.ts @@ -4,12 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - PromptConfig, - ModelConfig, - RunConfig, - ToolConfig, -} from './subagent.js'; +import { Content, FunctionDeclaration } from '@google/genai'; /** * Represents the storage level for a subagent configuration. @@ -61,10 +56,10 @@ export interface SubagentConfig { runConfig?: Partial; /** - * Optional background color for runtime display. + * Optional color for runtime display. * If 'auto' or omitted, uses automatic color assignment. */ - backgroundColor?: string; + color?: string; } /** @@ -159,3 +154,111 @@ export const SubagentErrorCode = { export type SubagentErrorCode = (typeof SubagentErrorCode)[keyof typeof SubagentErrorCode]; + +/** + * Describes the possible termination modes for a subagent. + * This enum provides a clear indication of why a subagent's execution might have ended. + */ +export enum SubagentTerminateMode { + /** + * Indicates that the subagent's execution terminated due to an unrecoverable error. + */ + ERROR = 'ERROR', + /** + * Indicates that the subagent's execution terminated because it exceeded the maximum allowed working time. + */ + TIMEOUT = 'TIMEOUT', + /** + * Indicates that the subagent's execution successfully completed all its defined goals. + */ + GOAL = 'GOAL', + /** + * Indicates that the subagent's execution terminated because it exceeded the maximum number of turns. + */ + MAX_TURNS = 'MAX_TURNS', +} + +/** + * Represents the output structure of a subagent's execution. + * This interface defines the data that a subagent will return upon completion, + * including the final result and the reason for its termination. + */ +export interface OutputObject { + /** + * The final result text returned by the subagent upon completion. + * This contains the direct output from the model's final response. + */ + result: string; + /** + * The reason for the subagent's termination, indicating whether it completed + * successfully, timed out, or encountered an error. + */ + terminate_reason: SubagentTerminateMode; +} + +/** + * Configures the initial prompt for the subagent. + */ +export interface PromptConfig { + /** + * A single system prompt string that defines the subagent's persona and instructions. + * Note: You should use either `systemPrompt` or `initialMessages`, but not both. + */ + systemPrompt?: string; + + /** + * An array of user/model content pairs to seed the chat history for few-shot prompting. + * Note: You should use either `systemPrompt` or `initialMessages`, but not both. + */ + initialMessages?: Content[]; +} + +/** + * Configures the tools available to the subagent during its execution. + */ +export interface ToolConfig { + /** + * A list of tool names (from the tool registry) or full function declarations + * that the subagent is permitted to use. + */ + tools: Array; +} + +/** + * Configures the generative model parameters for the subagent. + * This interface specifies the model to be used and its associated generation settings, + * such as temperature and top-p values, which influence the creativity and diversity of the model's output. + */ +export interface ModelConfig { + /** + * The name or identifier of the model to be used (e.g., 'gemini-2.5-pro'). + * + * TODO: In the future, this needs to support 'auto' or some other string to support routing use cases. + */ + model?: string; + /** + * The temperature for the model's sampling process. + */ + temp?: number; + /** + * The top-p value for nucleus sampling. + */ + top_p?: number; +} + +/** + * Configures the execution environment and constraints for the subagent. + * This interface defines parameters that control the subagent's runtime behavior, + * such as maximum execution time, to prevent infinite loops or excessive resource consumption. + * + * TODO: Consider adding max_tokens as a form of budgeting. + */ +export interface RunConfig { + /** The maximum execution time for the subagent in minutes. */ + max_time_minutes?: number; + /** + * The maximum number of conversational turns (a user message + model response) + * before the execution is terminated. Helps prevent infinite loops. + */ + max_turns?: number; +} diff --git a/packages/core/src/subagents/validation.ts b/packages/core/src/subagents/validation.ts index 8c7ace3c..f109cf05 100644 --- a/packages/core/src/subagents/validation.ts +++ b/packages/core/src/subagents/validation.ts @@ -9,6 +9,8 @@ import { ValidationResult, SubagentError, SubagentErrorCode, + ModelConfig, + RunConfig, } from './types.js'; /** @@ -250,9 +252,7 @@ export class SubagentValidator { * @param modelConfig - Partial model configuration to validate * @returns ValidationResult */ - validateModelConfig( - modelConfig: Partial, - ): ValidationResult { + validateModelConfig(modelConfig: ModelConfig): ValidationResult { const errors: string[] = []; const warnings: string[] = []; @@ -298,9 +298,7 @@ export class SubagentValidator { * @param runConfig - Partial run configuration to validate * @returns ValidationResult */ - validateRunConfig( - runConfig: Partial, - ): ValidationResult { + validateRunConfig(runConfig: RunConfig): ValidationResult { const errors: string[] = []; const warnings: string[] = []; diff --git a/packages/core/src/tools/task.test.ts b/packages/core/src/tools/task.test.ts index 8760b24c..a446a988 100644 --- a/packages/core/src/tools/task.test.ts +++ b/packages/core/src/tools/task.test.ts @@ -6,14 +6,12 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { TaskTool, TaskParams } from './task.js'; +import type { PartListUnion } from '@google/genai'; +import type { ToolResultDisplay, TaskResultDisplay } from './tools.js'; import { Config } from '../config/config.js'; import { SubagentManager } from '../subagents/subagent-manager.js'; -import { SubagentConfig } from '../subagents/types.js'; -import { - SubAgentScope, - ContextState, - SubagentTerminateMode, -} from '../subagents/subagent.js'; +import { SubagentConfig, SubagentTerminateMode } from '../subagents/types.js'; +import { SubAgentScope, ContextState } from '../subagents/subagent.js'; import { partToString } from '../utils/partUtils.js'; // Type for accessing protected methods in tests @@ -23,8 +21,8 @@ type TaskToolWithProtectedMethods = TaskTool & { signal?: AbortSignal, liveOutputCallback?: (chunk: string) => void, ) => Promise<{ - llmContent: string; - returnDisplay: unknown; + llmContent: PartListUnion; + returnDisplay: ToolResultDisplay; }>; getDescription: () => string; shouldConfirmExecute: () => Promise; @@ -270,6 +268,36 @@ describe('TaskTool', () => { .mockReturnValue( '✅ Success: Search files completed with GOAL termination', ), + getExecutionSummary: vi.fn().mockReturnValue({ + rounds: 2, + totalDurationMs: 1500, + totalToolCalls: 3, + successfulToolCalls: 3, + failedToolCalls: 0, + successRate: 100, + inputTokens: 1000, + outputTokens: 500, + totalTokens: 1500, + estimatedCost: 0.045, + toolUsage: [ + { + name: 'grep', + count: 2, + success: 2, + failure: 0, + totalDurationMs: 800, + averageDurationMs: 400, + }, + { + name: 'read_file', + count: 1, + success: 1, + failure: 0, + totalDurationMs: 200, + averageDurationMs: 200, + }, + ], + }), getStatistics: vi.fn().mockReturnValue({ rounds: 2, totalDurationMs: 1500, @@ -319,13 +347,11 @@ describe('TaskTool', () => { ); const llmText = partToString(result.llmContent); - const parsedResult = JSON.parse(llmText) as { - success: boolean; - subagent_name?: string; - error?: string; - }; - expect(parsedResult.success).toBe(true); - expect(parsedResult.subagent_name).toBe('file-search'); + expect(llmText).toBe('Task completed successfully'); + const display = result.returnDisplay as TaskResultDisplay; + expect(display.type).toBe('task_execution'); + expect(display.status).toBe('completed'); + expect(display.subagentName).toBe('file-search'); }); it('should handle subagent not found error', async () => { @@ -343,13 +369,10 @@ describe('TaskTool', () => { const result = await invocation.execute(); const llmText = partToString(result.llmContent); - const parsedResult = JSON.parse(llmText) as { - success: boolean; - subagent_name?: string; - error?: string; - }; - expect(parsedResult.success).toBe(false); - expect(parsedResult.error).toContain('Subagent "non-existent" not found'); + expect(llmText).toContain('Subagent "non-existent" not found'); + const display = result.returnDisplay as TaskResultDisplay; + expect(display.status).toBe('failed'); + expect(display.subagentName).toBe('non-existent'); }); it('should handle subagent execution failure', async () => { @@ -366,16 +389,9 @@ describe('TaskTool', () => { ).createInvocation(params); const result = await invocation.execute(); - const llmText = partToString(result.llmContent); - const parsedResult = JSON.parse(llmText) as { - success: boolean; - subagent_name?: string; - error?: string; - }; - expect(parsedResult.success).toBe(false); - expect(parsedResult.error).toContain( - 'Task did not complete successfully', - ); + const display = result.returnDisplay as TaskResultDisplay; + expect(display.status).toBe('failed'); + expect(display.terminateReason).toBe('ERROR'); }); it('should handle execution errors gracefully', async () => { @@ -395,13 +411,13 @@ describe('TaskTool', () => { const result = await invocation.execute(); const llmText = partToString(result.llmContent); - const parsedResult = JSON.parse(llmText) as { - success: boolean; - subagent_name?: string; - error?: string; - }; - expect(parsedResult.success).toBe(false); - expect(parsedResult.error).toContain('Failed to start subagent'); + expect(llmText).toContain('Failed to run subagent: Creation failed'); + const display = result.returnDisplay as TaskResultDisplay; + + expect(display.status).toBe('failed'); + expect(display.result ?? '').toContain( + 'Failed to run subagent: Creation failed', + ); }); it('should execute subagent without live output callback', async () => { @@ -421,12 +437,11 @@ describe('TaskTool', () => { expect(result.returnDisplay).toBeDefined(); // Verify the result has the expected structure - const llmContent = Array.isArray(result.llmContent) - ? result.llmContent - : [result.llmContent]; - const parsedResult = JSON.parse((llmContent[0] as { text: string }).text); - expect(parsedResult.success).toBe(true); - expect(parsedResult.subagent_name).toBe('file-search'); + const text = partToString(result.llmContent); + expect(text).toBe('Task completed successfully'); + const display = result.returnDisplay as TaskResultDisplay; + expect(display.status).toBe('completed'); + expect(display.subagentName).toBe('file-search'); }); it('should set context variables correctly', async () => { @@ -460,7 +475,7 @@ describe('TaskTool', () => { const result = await invocation.execute(); expect(typeof result.returnDisplay).toBe('object'); - expect(result.returnDisplay).toHaveProperty('type', 'subagent_execution'); + expect(result.returnDisplay).toHaveProperty('type', 'task_execution'); expect(result.returnDisplay).toHaveProperty( 'subagentName', 'file-search', @@ -499,9 +514,7 @@ describe('TaskTool', () => { ).createInvocation(params); const description = invocation.getDescription(); - expect(description).toBe( - 'file-search subagent: "Search files"', - ); + expect(description).toBe('file-search subagent: "Search files"'); }); }); }); diff --git a/packages/core/src/tools/task.ts b/packages/core/src/tools/task.ts index d3bc4abb..365d6dd1 100644 --- a/packages/core/src/tools/task.ts +++ b/packages/core/src/tools/task.ts @@ -21,6 +21,8 @@ import { SubAgentToolCallEvent, SubAgentToolResultEvent, SubAgentFinishEvent, + SubAgentEventType, + SubAgentErrorEvent, } from '../subagents/subagent-events.js'; import { ChatRecordingService } from '../services/chatRecordingService.js'; @@ -30,14 +32,6 @@ export interface TaskParams { subagent_type: string; } -export interface TaskResult { - success: boolean; - output?: string; - error?: string; - subagent_name?: string; - execution_summary?: string; -} - /** * Task tool that enables primary agents to delegate tasks to specialized subagents. * The tool dynamically loads available subagents and includes them in its description @@ -107,21 +101,6 @@ export class TaskTool extends BaseDeclarativeTool { * Updates the tool's description and schema based on available subagents. */ private updateDescriptionAndSchema(): void { - // Generate dynamic description - const baseDescription = `Delegate tasks to specialized subagents. This tool allows you to offload specific tasks to agents optimized for particular domains, reducing context usage and improving task completion. - -## When to Use This Tool - -Use this tool proactively when: -- The task matches a specialized agent's description -- You want to reduce context usage for file searches or analysis -- The task requires domain-specific expertise -- You need to perform focused work that doesn't require the full conversation context - -## Available Subagents - -`; - let subagentDescriptions = ''; if (this.availableSubagents.length === 0) { subagentDescriptions = @@ -132,6 +111,63 @@ Use this tool proactively when: .join('\n'); } + const baseDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. + +Available agent types and the tools they have access to: +${subagentDescriptions} + +When using the Task tool, you must specify a subagent_type parameter to select which agent type to use. + +When NOT to use the Agent tool: +- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly +- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly +- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly +- Other tasks that are not related to the agent descriptions above + +Usage notes: +1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses +2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. +3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. +4. The agent's outputs should generally be trusted +5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent +6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. + +Example usage: + +"code-reviewer": use this agent after you are done writing a signficant piece of code +"greeting-responder": use this agent when to respond to user greetings with a friendly joke + + + +user: "Please write a function that checks if a number is prime" +assistant: Sure let me write a function that checks if a number is prime +assistant: First let me use the Write tool to write a function that checks if a number is prime +assistant: I'm going to use the Write tool to write the following code: + +function isPrime(n) { + if (n <= 1) return false + for (let i = 2; i * i <= n; i++) { + if (n % i === 0) return false + } + return true +} + + +Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code + +assistant: Now let me use the code-reviewer agent to review the code +assistant: Uses the Task tool to launch the with the code-reviewer agent + + + +user: "Hello" + +Since the user is greeting, use the greeting-responder agent to respond with a friendly joke + +assistant: "I'm going to use the Task tool to launch the with the greeting-responder agent" + +`; + // Update description using object property assignment since it's readonly (this as { description: string }).description = baseDescription + subagentDescriptions; @@ -211,14 +247,7 @@ Use this tool proactively when: class TaskToolInvocation extends BaseToolInvocation { private readonly _eventEmitter: SubAgentEventEmitter; private currentDisplay: TaskResultDisplay | null = null; - private currentToolCalls: Array<{ - name: string; - status: 'executing' | 'success' | 'failed'; - error?: string; - args?: Record; - result?: string; - returnDisplay?: string; - }> = []; + private currentToolCalls: TaskResultDisplay['toolCalls'] = []; constructor( private readonly config: Config, @@ -258,72 +287,75 @@ class TaskToolInvocation extends BaseToolInvocation { private setupEventListeners( updateOutput?: (output: ToolResultDisplay) => void, ): void { - this.eventEmitter.on('start', () => { + this.eventEmitter.on(SubAgentEventType.START, () => { this.updateDisplay({ status: 'running' }, updateOutput); }); - this.eventEmitter.on('model_text', (..._args: unknown[]) => { - // Model text events are no longer displayed as currentStep - // Keep the listener for potential future use - }); - - this.eventEmitter.on('tool_call', (...args: unknown[]) => { + this.eventEmitter.on(SubAgentEventType.TOOL_CALL, (...args: unknown[]) => { const event = args[0] as SubAgentToolCallEvent; const newToolCall = { + callId: event.callId, name: event.name, status: 'executing' as const, args: event.args, + description: event.description, }; - this.currentToolCalls.push(newToolCall); + this.currentToolCalls!.push(newToolCall); this.updateDisplay( { - progress: { - toolCalls: [...this.currentToolCalls], - }, + toolCalls: [...this.currentToolCalls!], }, updateOutput, ); }); - this.eventEmitter.on('tool_result', (...args: unknown[]) => { - const event = args[0] as SubAgentToolResultEvent; - const toolCallIndex = this.currentToolCalls.findIndex( - (call) => call.name === event.name, - ); - if (toolCallIndex >= 0) { - this.currentToolCalls[toolCallIndex] = { - ...this.currentToolCalls[toolCallIndex], - status: event.success ? 'success' : 'failed', - error: event.error, - // Note: result would need to be added to SubAgentToolResultEvent to be captured - }; - - this.updateDisplay( - { - progress: { - toolCalls: [...this.currentToolCalls], - }, - }, - updateOutput, + this.eventEmitter.on( + SubAgentEventType.TOOL_RESULT, + (...args: unknown[]) => { + const event = args[0] as SubAgentToolResultEvent; + const toolCallIndex = this.currentToolCalls!.findIndex( + (call) => call.callId === event.callId, ); - } - }); + if (toolCallIndex >= 0) { + this.currentToolCalls![toolCallIndex] = { + ...this.currentToolCalls![toolCallIndex], + status: event.success ? 'success' : 'failed', + error: event.error, + resultDisplay: event.resultDisplay, + }; - this.eventEmitter.on('finish', (...args: unknown[]) => { + this.updateDisplay( + { + toolCalls: [...this.currentToolCalls!], + }, + updateOutput, + ); + } + }, + ); + + this.eventEmitter.on(SubAgentEventType.FINISH, (...args: unknown[]) => { const event = args[0] as SubAgentFinishEvent; this.updateDisplay( { status: event.terminate_reason === 'GOAL' ? 'completed' : 'failed', terminateReason: event.terminate_reason, - // Keep progress data including tool calls for final display + // Keep toolCalls data for final display }, updateOutput, ); }); - this.eventEmitter.on('error', () => { - this.updateDisplay({ status: 'failed' }, updateOutput); + this.eventEmitter.on(SubAgentEventType.ERROR, (...args: unknown[]) => { + const event = args[0] as SubAgentErrorEvent; + this.updateDisplay( + { + status: 'failed', + terminateReason: event.error, + }, + updateOutput, + ); }); } @@ -348,38 +380,50 @@ class TaskToolInvocation extends BaseToolInvocation { if (!subagentConfig) { const errorDisplay = { - type: 'subagent_execution' as const, + type: 'task_execution' as const, subagentName: this.params.subagent_type, taskDescription: this.params.description, + taskPrompt: this.params.prompt, status: 'failed' as const, terminateReason: 'ERROR', result: `Subagent "${this.params.subagent_type}" not found`, + subagentColor: undefined, }; return { - llmContent: [ - { - text: JSON.stringify({ - success: false, - error: `Subagent "${this.params.subagent_type}" not found`, - }), - }, - ], + llmContent: `Subagent "${this.params.subagent_type}" not found`, returnDisplay: errorDisplay, }; } // Initialize the current display state this.currentDisplay = { - type: 'subagent_execution' as const, + type: 'task_execution' as const, subagentName: subagentConfig.name, taskDescription: this.params.description, + taskPrompt: this.params.prompt, status: 'running' as const, + subagentColor: subagentConfig.color, }; // Set up event listeners for real-time updates this.setupEventListeners(updateOutput); + if (signal) { + signal.addEventListener('abort', () => { + if (this.currentDisplay) { + this.updateDisplay( + { + status: 'failed', + terminateReason: 'CANCELLED', + result: 'Task was cancelled by user', + }, + updateOutput, + ); + } + }); + } + // Send initial display if (updateOutput) { updateOutput(this.currentDisplay); @@ -432,25 +476,7 @@ class TaskToolInvocation extends BaseToolInvocation { const finalText = subagentScope.getFinalText(); const terminateReason = subagentScope.output.terminate_reason; const success = terminateReason === 'GOAL'; - - // Format the results based on description (iflow-like switch) - const wantDetailed = /\b(stats|statistics|detailed)\b/i.test( - this.params.description, - ); - const executionSummary = wantDetailed - ? subagentScope.formatDetailedResult(this.params.description) - : subagentScope.formatCompactResult(this.params.description); - - const result: TaskResult = { - success, - output: finalText, - subagent_name: subagentConfig.name, - execution_summary: executionSummary, - }; - - if (!success) { - result.error = `Task did not complete successfully. Termination reason: ${terminateReason}`; - } + const executionSummary = subagentScope.getExecutionSummary(); // Update the final display state this.updateDisplay( @@ -459,38 +485,28 @@ class TaskToolInvocation extends BaseToolInvocation { terminateReason, result: finalText, executionSummary, - // Keep progress data including tool calls for final display }, updateOutput, ); return { - llmContent: [{ text: JSON.stringify(result) }], + llmContent: [{ text: finalText }], returnDisplay: this.currentDisplay!, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[TaskTool] Error starting subagent: ${errorMessage}`); + console.error(`[TaskTool] Error running subagent: ${errorMessage}`); - const errorDisplay = { - type: 'subagent_execution' as const, - subagentName: this.params.subagent_type, - taskDescription: this.params.description, + const errorDisplay: TaskResultDisplay = { + ...this.currentDisplay!, status: 'failed' as const, terminateReason: 'ERROR', - result: `Failed to start subagent: ${errorMessage}`, + result: `Failed to run subagent: ${errorMessage}`, }; return { - llmContent: [ - { - text: JSON.stringify({ - success: false, - error: `Failed to start subagent: ${errorMessage}`, - }), - }, - ], + llmContent: `Failed to run subagent: ${errorMessage}`, returnDisplay: errorDisplay, }; } diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index cf7cfb98..abd2980b 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -8,6 +8,7 @@ import { FunctionDeclaration, PartListUnion } from '@google/genai'; import { ToolErrorType } from './tool-error.js'; import { DiffUpdateResult } from '../ide/ideContext.js'; import { SchemaValidator } from '../utils/schemaValidator.js'; +import { SubagentStatsSummary } from '../subagents/subagent-statistics.js'; /** * Represents a validated and ready-to-execute tool call. @@ -422,23 +423,25 @@ export function hasCycleInSchema(schema: object): boolean { } export interface TaskResultDisplay { - type: 'subagent_execution'; + type: 'task_execution'; subagentName: string; + subagentColor?: string; taskDescription: string; + taskPrompt: string; status: 'running' | 'completed' | 'failed'; terminateReason?: string; result?: string; - executionSummary?: string; - progress?: { - toolCalls?: Array<{ - name: string; - status: 'executing' | 'success' | 'failed'; - error?: string; - args?: Record; - result?: string; - returnDisplay?: string; - }>; - }; + executionSummary?: SubagentStatsSummary; + toolCalls?: Array<{ + callId: string; + name: string; + status: 'executing' | 'success' | 'failed'; + error?: string; + args?: Record; + result?: string; + resultDisplay?: string; + description?: string; + }>; } export type ToolResultDisplay =