mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent runtime & CLI display - done
This commit is contained in:
@@ -196,7 +196,7 @@ describe('<ToolMessage />', () => {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<ToolMessageProps> = ({
|
||||
{displayRenderer.type === 'todo' && (
|
||||
<TodoResultRenderer data={displayRenderer.data} />
|
||||
)}
|
||||
{displayRenderer.type === 'subagent_execution' && (
|
||||
{displayRenderer.type === 'task' && (
|
||||
<SubagentExecutionRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
|
||||
@@ -48,12 +48,12 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(agent.backgroundColor) && (
|
||||
{shouldShowColor(agent.color) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(agent.backgroundColor)}>
|
||||
<Text color="black">{` ${agent.name} `}</Text>
|
||||
</Box>
|
||||
<Text
|
||||
color={getColorForDisplay(agent.color)}
|
||||
>{` ${agent.name} `}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ export function AgentsManagerDialog({
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<ColorSelector
|
||||
backgroundColor={selectedAgent?.backgroundColor || 'auto'}
|
||||
color={selectedAgent?.color || 'auto'}
|
||||
agentName={selectedAgent?.name || 'Agent'}
|
||||
onSelect={async (color) => {
|
||||
// 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
|
||||
|
||||
@@ -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<string>(backgroundColor);
|
||||
const [selectedColor, setSelectedColor] = useState<string>(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({
|
||||
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>Preview:</Text>
|
||||
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||
<Text color="black">{` ${agentName} `}</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color={currentColor.value}>{` ${agentName} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -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({
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(state.backgroundColor) && (
|
||||
{shouldShowColor(state.color) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(state.backgroundColor)}>
|
||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
<Text
|
||||
color={getColorForDisplay(state.color)}
|
||||
>{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export function SubagentCreationWizard({
|
||||
case WIZARD_STEPS.COLOR_SELECTION:
|
||||
return (
|
||||
<ColorSelector
|
||||
backgroundColor={state.backgroundColor}
|
||||
color={state.color}
|
||||
agentName={state.generatedName}
|
||||
onSelect={(color) => {
|
||||
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,
|
||||
|
||||
@@ -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 }) => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{/* Header with subagent name and status */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<StatusDot status={data.status} />
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
{data.subagentName}
|
||||
</Text>
|
||||
<Text color={Colors.Gray}> • </Text>
|
||||
<StatusIndicator status={data.status} />
|
||||
> = ({ data }) => {
|
||||
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('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 (
|
||||
<Box flexDirection="column" paddingX={1} gap={1}>
|
||||
{/* Header with subagent name and status */}
|
||||
<Box flexDirection="row">
|
||||
<Text bold color={agentColor}>
|
||||
{data.subagentName}
|
||||
</Text>
|
||||
<StatusDot status={data.status} />
|
||||
<StatusIndicator status={data.status} />
|
||||
</Box>
|
||||
|
||||
{/* Task description */}
|
||||
<TaskPromptSection
|
||||
taskPrompt={data.taskPrompt}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
|
||||
{/* Progress section for running tasks */}
|
||||
{data.status === 'running' &&
|
||||
data.toolCalls &&
|
||||
data.toolCalls.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<ToolCallsList
|
||||
toolCalls={data.toolCalls}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results section for completed/failed tasks */}
|
||||
{(data.status === 'completed' || data.status === 'failed') && (
|
||||
<ResultsSection data={data} displayMode={displayMode} />
|
||||
)}
|
||||
|
||||
{/* Footer with keyboard shortcuts */}
|
||||
{footerText && (
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>{footerText}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Task description */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text color={Colors.Gray}>Task: </Text>
|
||||
<Text wrap="wrap">{data.taskDescription}</Text>
|
||||
/**
|
||||
* 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 (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.text.primary}>Task Detail: </Text>
|
||||
{shouldTruncate && displayMode === 'default' && (
|
||||
<Text color={Colors.Gray}> Showing the first 10 lines.</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingLeft={1}>
|
||||
<Text wrap="wrap">
|
||||
{displayLines.join('\n') + (shouldTruncate && !showFull ? '...' : '')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Progress section for running tasks */}
|
||||
{data.status === 'running' && (
|
||||
<ProgressSection progress={data.progress || { toolCalls: [] }} />
|
||||
)}
|
||||
|
||||
{/* Results section for completed/failed tasks */}
|
||||
{(data.status === 'completed' || data.status === 'failed') && (
|
||||
<ResultsSection data={data} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Box marginRight={1}>
|
||||
<Text color={color}>●</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}> = ({ status }) => (
|
||||
<Box marginLeft={1} marginRight={1}>
|
||||
<Text color={getStatusColor(status)}>●</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Status indicator component
|
||||
@@ -82,109 +193,95 @@ const StatusDot: React.FC<{
|
||||
const StatusIndicator: React.FC<{
|
||||
status: TaskResultDisplay['status'];
|
||||
}> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Text color={Colors.AccentYellow}>Running</Text>;
|
||||
case 'completed':
|
||||
return <Text color={Colors.AccentGreen}>Completed</Text>;
|
||||
case 'failed':
|
||||
return <Text color={Colors.AccentRed}>Failed</Text>;
|
||||
default:
|
||||
return <Text color={Colors.Gray}>Unknown</Text>;
|
||||
}
|
||||
const color = getStatusColor(status);
|
||||
const text = getStatusText(status);
|
||||
return <Text color={color}>{text}</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<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
};
|
||||
}> = ({ progress }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{progress.toolCalls && progress.toolCalls.length > 0 && (
|
||||
<CleanToolCallsList toolCalls={progress.toolCalls} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
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<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
}> = ({ toolCalls }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text bold>Tools:</Text>
|
||||
// Reverse the order to show most recent first
|
||||
const reversedDisplayCalls = [...displayCalls].reverse();
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text color={theme.text.primary}>Tools:</Text>
|
||||
{shouldTruncate && displayMode === 'default' && (
|
||||
<Text color={Colors.Gray}>
|
||||
{' '}
|
||||
Showing the last 5 of {calls.length} tools.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{reversedDisplayCalls.map((toolCall, index) => (
|
||||
<ToolCallItem key={`${toolCall.name}-${index}`} toolCall={toolCall} />
|
||||
))}
|
||||
</Box>
|
||||
{toolCalls.map((toolCall, index) => (
|
||||
<CleanToolCallItem
|
||||
key={`${toolCall.name}-${index}`}
|
||||
toolCall={toolCall}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
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 <Text color={Colors.AccentYellow}>⊷</Text>; // Using same as ToolMessage
|
||||
return <Text color={color}>⊷</Text>; // Using same as ToolMessage
|
||||
case 'success':
|
||||
return <Text color={Colors.AccentGreen}>✔</Text>;
|
||||
return <Text color={color}>✔</Text>;
|
||||
case 'failed':
|
||||
return (
|
||||
<Text color={Colors.AccentRed} bold>
|
||||
<Text color={color} bold>
|
||||
x
|
||||
</Text>
|
||||
);
|
||||
default:
|
||||
return <Text color={Colors.Gray}>o</Text>;
|
||||
return <Text color={color}>o</Text>;
|
||||
}
|
||||
}, [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 (
|
||||
<Box flexDirection="column" paddingLeft={1} marginBottom={0}>
|
||||
@@ -197,7 +294,7 @@ const CleanToolCallItem: React.FC<{
|
||||
</Text>{' '}
|
||||
<Text color={Colors.Gray}>{description}</Text>
|
||||
{toolCall.error && (
|
||||
<Text color={Colors.AccentRed}> - {toolCall.error}</Text>
|
||||
<Text color={theme.status.error}> - {toolCall.error}</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -212,59 +309,16 @@ const CleanToolCallItem: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get tool description from args
|
||||
*/
|
||||
const getToolDescription = (toolCall: {
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
}): 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 (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text color={Colors.Gray}>• No summary available</Text>
|
||||
@@ -275,13 +329,13 @@ const ExecutionSummaryDetails: React.FC<{
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text>
|
||||
• <Text bold>Duration:</Text> {summaryData.duration}
|
||||
• <Text>Duration: {fmtDuration(stats.totalDurationMs)}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Rounds:</Text> {summaryData.rounds}
|
||||
• <Text>Rounds: {stats.rounds}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Tokens:</Text> {summaryData.tokens}
|
||||
• <Text>Tokens: {stats.totalTokens.toLocaleString()}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -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<string, unknown>;
|
||||
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 (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text color={Colors.Gray}>• No tool usage data available</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text>
|
||||
• <Text bold>Total Calls:</Text> {stats.total}
|
||||
• <Text bold>Total Calls:</Text> {executionSummary.totalToolCalls}
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Success Rate:</Text>{' '}
|
||||
<Text color={Colors.AccentGreen}>{stats.successRate}%</Text> (
|
||||
<Text color={Colors.AccentGreen}>{stats.successful} success</Text>,{' '}
|
||||
<Text color={Colors.AccentRed}>{stats.failed} failed</Text>)
|
||||
<Text color={Colors.AccentGreen}>
|
||||
{executionSummary.successRate.toFixed(1)}%
|
||||
</Text>{' '}
|
||||
(
|
||||
<Text color={Colors.AccentGreen}>
|
||||
{executionSummary.successfulToolCalls} success
|
||||
</Text>
|
||||
,{' '}
|
||||
<Text color={Colors.AccentRed}>
|
||||
{executionSummary.failedToolCalls} failed
|
||||
</Text>
|
||||
)
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -332,47 +384,39 @@ const ToolUsageStats: React.FC<{
|
||||
*/
|
||||
const ResultsSection: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
}> = ({ data }) => (
|
||||
<Box flexDirection="column">
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ data, displayMode }) => (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{/* Tool calls section - clean list format */}
|
||||
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||
<CleanToolCallsList toolCalls={data.progress.toolCalls} />
|
||||
{data.toolCalls && data.toolCalls.length > 0 && (
|
||||
<ToolCallsList toolCalls={data.toolCalls} displayMode={displayMode} />
|
||||
)}
|
||||
|
||||
{/* Task Completed section */}
|
||||
<Box flexDirection="row" marginTop={1} marginBottom={1}>
|
||||
<Text>📄 </Text>
|
||||
<Text bold>Task Completed: </Text>
|
||||
<Text>{data.taskDescription}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Execution Summary section */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text>📊 </Text>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
<Text color={theme.text.primary}>
|
||||
Execution Summary:
|
||||
</Text>
|
||||
</Box>
|
||||
<ExecutionSummaryDetails data={data} />
|
||||
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
|
||||
</Box>
|
||||
|
||||
{/* Tool Usage section */}
|
||||
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||
{data.executionSummary && (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text>🔧 </Text>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
<Text color={theme.text.primary}>
|
||||
Tool Usage:
|
||||
</Text>
|
||||
</Box>
|
||||
<ToolUsageStats toolCalls={data.progress.toolCalls} />
|
||||
<ToolUsageStats executionSummary={data.executionSummary} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error reason for failed tasks */}
|
||||
{data.status === 'failed' && data.terminateReason && (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
||||
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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();
|
||||
|
||||
|
||||
@@ -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<string, unknown>
|
||||
| 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<import('./subagent.js').RunConfig>,
|
||||
backgroundColor,
|
||||
modelConfig: modelConfig as Partial<ModelConfig>,
|
||||
runConfig: runConfig as Partial<RunConfig>,
|
||||
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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string | FunctionDeclaration>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Content[]> {
|
||||
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, unknown>,
|
||||
): 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.
|
||||
|
||||
@@ -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<RunConfig>;
|
||||
|
||||
/**
|
||||
* 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<string | FunctionDeclaration>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -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<import('./subagent.js').ModelConfig>,
|
||||
): 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<import('./subagent.js').RunConfig>,
|
||||
): ValidationResult {
|
||||
validateRunConfig(runConfig: RunConfig): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
|
||||
@@ -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<boolean>;
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TaskParams, ToolResult> {
|
||||
* 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:
|
||||
<example_agent_descriptions>
|
||||
"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
|
||||
</example_agent_description>
|
||||
|
||||
<example>
|
||||
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:
|
||||
<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
|
||||
}
|
||||
</code>
|
||||
<commentary>
|
||||
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
||||
</commentary>
|
||||
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
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "Hello"
|
||||
<commentary>
|
||||
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
||||
</commentary>
|
||||
assistant: "I'm going to use the Task tool to launch the with the greeting-responder agent"
|
||||
</example>
|
||||
`;
|
||||
|
||||
// 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<TaskParams, ToolResult> {
|
||||
private readonly _eventEmitter: SubAgentEventEmitter;
|
||||
private currentDisplay: TaskResultDisplay | null = null;
|
||||
private currentToolCalls: Array<{
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}> = [];
|
||||
private currentToolCalls: TaskResultDisplay['toolCalls'] = [];
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
@@ -258,72 +287,75 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
||||
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<TaskParams, ToolResult> {
|
||||
|
||||
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<TaskParams, ToolResult> {
|
||||
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<TaskParams, ToolResult> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
};
|
||||
executionSummary?: SubagentStatsSummary;
|
||||
toolCalls?: Array<{
|
||||
callId: string;
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
resultDisplay?: string;
|
||||
description?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type ToolResultDisplay =
|
||||
|
||||
Reference in New Issue
Block a user