mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent runtime & CLI display - wip
This commit is contained in:
@@ -39,6 +39,19 @@ vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
|||||||
return <Text>MockMarkdown:{text}</Text>;
|
return <Text>MockMarkdown:{text}</Text>;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
vi.mock('../subagents/index.js', () => ({
|
||||||
|
SubagentExecutionDisplay: function MockSubagentExecutionDisplay({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: { subagentName: string; taskDescription: string };
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
🤖 {data.subagentName} • Task: {data.taskDescription}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// Helper to render with context
|
// Helper to render with context
|
||||||
const renderWithContext = (
|
const renderWithContext = (
|
||||||
@@ -180,4 +193,33 @@ describe('<ToolMessage />', () => {
|
|||||||
// We can at least ensure it doesn't have the high emphasis indicator.
|
// We can at least ensure it doesn't have the high emphasis indicator.
|
||||||
expect(lowEmphasisFrame()).not.toContain('←');
|
expect(lowEmphasisFrame()).not.toContain('←');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows subagent execution display for task tool with proper result display', () => {
|
||||||
|
const subagentResultDisplay = {
|
||||||
|
type: 'subagent_execution' as const,
|
||||||
|
subagentName: 'file-search',
|
||||||
|
taskDescription: 'Search for files matching pattern',
|
||||||
|
status: 'running' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: ToolMessageProps = {
|
||||||
|
name: 'task',
|
||||||
|
description: 'Delegate task to subagent',
|
||||||
|
resultDisplay: subagentResultDisplay,
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
terminalWidth: 80,
|
||||||
|
callId: 'test-call-id-2',
|
||||||
|
confirmationDetails: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithContext(
|
||||||
|
<ToolMessage {...props} />,
|
||||||
|
StreamingState.Responding,
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('🤖'); // Subagent execution display should show
|
||||||
|
expect(output).toContain('file-search'); // Actual subagent name
|
||||||
|
expect(output).toContain('Search for files matching pattern'); // Actual task description
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
|||||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||||
import { TodoDisplay } from '../TodoDisplay.js';
|
import { TodoDisplay } from '../TodoDisplay.js';
|
||||||
import { TodoResultDisplay } from '@qwen-code/qwen-code-core';
|
import {
|
||||||
|
TodoResultDisplay,
|
||||||
|
TaskResultDisplay,
|
||||||
|
} from '@qwen-code/qwen-code-core';
|
||||||
|
import { SubagentExecutionDisplay } from '../subagents/index.js';
|
||||||
|
|
||||||
const STATIC_HEIGHT = 1;
|
const STATIC_HEIGHT = 1;
|
||||||
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||||
@@ -29,7 +33,8 @@ type DisplayRendererResult =
|
|||||||
| { type: 'none' }
|
| { type: 'none' }
|
||||||
| { type: 'todo'; data: TodoResultDisplay }
|
| { type: 'todo'; data: TodoResultDisplay }
|
||||||
| { type: 'string'; data: string }
|
| { type: 'string'; data: string }
|
||||||
| { type: 'diff'; data: { fileDiff: string; fileName: string } };
|
| { type: 'diff'; data: { fileDiff: string; fileName: string } }
|
||||||
|
| { type: 'subagent_execution'; data: TaskResultDisplay };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook to determine the type of result display and return appropriate rendering info
|
* Custom hook to determine the type of result display and return appropriate rendering info
|
||||||
@@ -55,6 +60,19 @@ const useResultDisplayRenderer = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for SubagentExecutionResultDisplay (for non-task tools)
|
||||||
|
if (
|
||||||
|
typeof resultDisplay === 'object' &&
|
||||||
|
resultDisplay !== null &&
|
||||||
|
'type' in resultDisplay &&
|
||||||
|
resultDisplay.type === 'subagent_execution'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: 'subagent_execution',
|
||||||
|
data: resultDisplay as TaskResultDisplay,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Check for FileDiff
|
// Check for FileDiff
|
||||||
if (
|
if (
|
||||||
typeof resultDisplay === 'object' &&
|
typeof resultDisplay === 'object' &&
|
||||||
@@ -81,6 +99,15 @@ const TodoResultRenderer: React.FC<{ data: TodoResultDisplay }> = ({
|
|||||||
data,
|
data,
|
||||||
}) => <TodoDisplay todos={data.todos} />;
|
}) => <TodoDisplay todos={data.todos} />;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render subagent execution results
|
||||||
|
*/
|
||||||
|
const SubagentExecutionRenderer: React.FC<{
|
||||||
|
data: TaskResultDisplay;
|
||||||
|
availableHeight?: number;
|
||||||
|
childWidth: number;
|
||||||
|
}> = ({ data }) => <SubagentExecutionDisplay data={data} />;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render string results (markdown or plain text)
|
* Component to render string results (markdown or plain text)
|
||||||
*/
|
*/
|
||||||
@@ -189,6 +216,13 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
{displayRenderer.type === 'todo' && (
|
{displayRenderer.type === 'todo' && (
|
||||||
<TodoResultRenderer data={displayRenderer.data} />
|
<TodoResultRenderer data={displayRenderer.data} />
|
||||||
)}
|
)}
|
||||||
|
{displayRenderer.type === 'subagent_execution' && (
|
||||||
|
<SubagentExecutionRenderer
|
||||||
|
data={displayRenderer.data}
|
||||||
|
availableHeight={availableHeight}
|
||||||
|
childWidth={childWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{displayRenderer.type === 'string' && (
|
{displayRenderer.type === 'string' && (
|
||||||
<StringResultRenderer
|
<StringResultRenderer
|
||||||
data={displayRenderer.data}
|
data={displayRenderer.data}
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import { Colors } from '../../colors.js';
|
||||||
|
import { TaskResultDisplay } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
|
export interface SubagentExecutionDisplayProps {
|
||||||
|
data: TaskResultDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display subagent execution progress and results.
|
||||||
|
* This is now a pure component that renders the provided SubagentExecutionResultDisplay data.
|
||||||
|
* Real-time updates are handled by the parent component updating the data prop.
|
||||||
|
*/
|
||||||
|
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} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Task description */}
|
||||||
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
|
<Text color={Colors.Gray}>Task: </Text>
|
||||||
|
<Text wrap="wrap">{data.taskDescription}</Text>
|
||||||
|
</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 indicator component
|
||||||
|
*/
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress section for running executions
|
||||||
|
*/
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
</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<{
|
||||||
|
toolCall: {
|
||||||
|
name: string;
|
||||||
|
status: 'executing' | 'success' | 'failed';
|
||||||
|
error?: string;
|
||||||
|
args?: Record<string, unknown>;
|
||||||
|
result?: string;
|
||||||
|
returnDisplay?: string;
|
||||||
|
};
|
||||||
|
}> = ({ toolCall }) => {
|
||||||
|
const STATUS_INDICATOR_WIDTH = 3;
|
||||||
|
|
||||||
|
// Map subagent status to ToolCallStatus-like display
|
||||||
|
const statusIcon = React.useMemo(() => {
|
||||||
|
switch (toolCall.status) {
|
||||||
|
case 'executing':
|
||||||
|
return <Text color={Colors.AccentYellow}>⊷</Text>; // Using same as ToolMessage
|
||||||
|
case 'success':
|
||||||
|
return <Text color={Colors.AccentGreen}>✔</Text>;
|
||||||
|
case 'failed':
|
||||||
|
return (
|
||||||
|
<Text color={Colors.AccentRed} bold>
|
||||||
|
x
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Text color={Colors.Gray}>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];
|
||||||
|
return firstLine.length > 80
|
||||||
|
? firstLine.substring(0, 80) + '...'
|
||||||
|
: firstLine;
|
||||||
|
}, [toolCall.returnDisplay]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingLeft={1} marginBottom={0}>
|
||||||
|
{/* First line: status icon + tool name + description (consistent with ToolInfo) */}
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Box minWidth={STATUS_INDICATOR_WIDTH}>{statusIcon}</Box>
|
||||||
|
<Text wrap="truncate-end">
|
||||||
|
<Text color={Colors.Foreground} bold>
|
||||||
|
{toolCall.name}
|
||||||
|
</Text>{' '}
|
||||||
|
<Text color={Colors.Gray}>{description}</Text>
|
||||||
|
{toolCall.error && (
|
||||||
|
<Text color={Colors.AccentRed}> - {toolCall.error}</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Second line: truncated returnDisplay output */}
|
||||||
|
{truncatedOutput && (
|
||||||
|
<Box flexDirection="row" paddingLeft={STATUS_INDICATOR_WIDTH}>
|
||||||
|
<Text color={Colors.Gray}>{truncatedOutput}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingLeft={1}>
|
||||||
|
<Text color={Colors.Gray}>• No summary available</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingLeft={1}>
|
||||||
|
<Text>
|
||||||
|
• <Text bold>Duration:</Text> {summaryData.duration}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
• <Text bold>Rounds:</Text> {summaryData.rounds}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
• <Text bold>Tokens:</Text> {summaryData.tokens}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingLeft={1}>
|
||||||
|
<Text>
|
||||||
|
• <Text bold>Total Calls:</Text> {stats.total}
|
||||||
|
</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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Results section for completed executions - matches the clean layout from the image
|
||||||
|
*/
|
||||||
|
const ResultsSection: React.FC<{
|
||||||
|
data: TaskResultDisplay;
|
||||||
|
}> = ({ data }) => (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{/* Tool calls section - clean list format */}
|
||||||
|
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||||
|
<CleanToolCallsList toolCalls={data.progress.toolCalls} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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="row" marginBottom={1}>
|
||||||
|
<Text>📊 </Text>
|
||||||
|
<Text bold color={Colors.AccentBlue}>
|
||||||
|
Execution Summary:
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<ExecutionSummaryDetails data={data} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Tool Usage section */}
|
||||||
|
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
|
<Text>🔧 </Text>
|
||||||
|
<Text bold color={Colors.AccentBlue}>
|
||||||
|
Tool Usage:
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<ToolUsageStats toolCalls={data.progress.toolCalls} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error reason for failed tasks */}
|
||||||
|
{data.status === 'failed' && data.terminateReason && (
|
||||||
|
<Box flexDirection="row" marginTop={1}>
|
||||||
|
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
||||||
|
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
@@ -20,6 +20,9 @@ export { ActionSelectionStep } from './ActionSelectionStep.js';
|
|||||||
export { AgentViewerStep } from './AgentViewerStep.js';
|
export { AgentViewerStep } from './AgentViewerStep.js';
|
||||||
export { AgentDeleteStep } from './AgentDeleteStep.js';
|
export { AgentDeleteStep } from './AgentDeleteStep.js';
|
||||||
|
|
||||||
|
// Execution Display Components
|
||||||
|
export { SubagentExecutionDisplay } from './SubagentExecutionDisplay.js';
|
||||||
|
|
||||||
// Creation Wizard Types and State
|
// Creation Wizard Types and State
|
||||||
export type {
|
export type {
|
||||||
CreationWizardState,
|
CreationWizardState,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export interface IndividualToolCallDisplay {
|
|||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
resultDisplay: ToolResultDisplay | undefined;
|
resultDisplay: ToolResultDisplay | string | object | undefined;
|
||||||
status: ToolCallStatus;
|
status: ToolCallStatus;
|
||||||
confirmationDetails: ToolCallConfirmationDetails | undefined;
|
confirmationDetails: ToolCallConfirmationDetails | undefined;
|
||||||
renderOutputAsMarkdown?: boolean;
|
renderOutputAsMarkdown?: boolean;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
GEMINI_CONFIG_DIR as GEMINI_DIR,
|
GEMINI_CONFIG_DIR as GEMINI_DIR,
|
||||||
} from '../tools/memoryTool.js';
|
} from '../tools/memoryTool.js';
|
||||||
import { TodoWriteTool } from '../tools/todoWrite.js';
|
import { TodoWriteTool } from '../tools/todoWrite.js';
|
||||||
|
import { TaskTool } from '../tools/task.js';
|
||||||
import { WebSearchTool } from '../tools/web-search.js';
|
import { WebSearchTool } from '../tools/web-search.js';
|
||||||
import { GeminiClient } from '../core/client.js';
|
import { GeminiClient } from '../core/client.js';
|
||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
@@ -237,6 +238,7 @@ export interface ConfigParameters {
|
|||||||
export class Config {
|
export class Config {
|
||||||
private toolRegistry!: ToolRegistry;
|
private toolRegistry!: ToolRegistry;
|
||||||
private promptRegistry!: PromptRegistry;
|
private promptRegistry!: PromptRegistry;
|
||||||
|
private subagentManager!: SubagentManager;
|
||||||
private sessionId: string;
|
private sessionId: string;
|
||||||
private fileSystemService: FileSystemService;
|
private fileSystemService: FileSystemService;
|
||||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||||
@@ -317,7 +319,6 @@ export class Config {
|
|||||||
private readonly shouldUseNodePtyShell: boolean;
|
private readonly shouldUseNodePtyShell: boolean;
|
||||||
private readonly skipNextSpeakerCheck: boolean;
|
private readonly skipNextSpeakerCheck: boolean;
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
private subagentManager: SubagentManager | null = null;
|
|
||||||
|
|
||||||
constructor(params: ConfigParameters) {
|
constructor(params: ConfigParameters) {
|
||||||
this.sessionId = params.sessionId;
|
this.sessionId = params.sessionId;
|
||||||
@@ -427,6 +428,7 @@ export class Config {
|
|||||||
await this.getGitService();
|
await this.getGitService();
|
||||||
}
|
}
|
||||||
this.promptRegistry = new PromptRegistry();
|
this.promptRegistry = new PromptRegistry();
|
||||||
|
this.subagentManager = new SubagentManager(this);
|
||||||
this.toolRegistry = await this.createToolRegistry();
|
this.toolRegistry = await this.createToolRegistry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,9 +870,6 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubagentManager(): SubagentManager {
|
getSubagentManager(): SubagentManager {
|
||||||
if (!this.subagentManager) {
|
|
||||||
this.subagentManager = new SubagentManager(this.targetDir);
|
|
||||||
}
|
|
||||||
return this.subagentManager;
|
return this.subagentManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,6 +909,7 @@ export class Config {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registerCoreTool(TaskTool, this);
|
||||||
registerCoreTool(LSTool, this);
|
registerCoreTool(LSTool, this);
|
||||||
registerCoreTool(ReadFileTool, this);
|
registerCoreTool(ReadFileTool, this);
|
||||||
registerCoreTool(GrepTool, this);
|
registerCoreTool(GrepTool, this);
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -414,6 +415,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -717,6 +719,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -1005,6 +1008,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -1293,6 +1297,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -1581,6 +1586,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -1869,6 +1875,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -2157,6 +2164,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
@@ -2445,6 +2453,7 @@ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the 'todo_write' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the 'task' tool in order to reduce context usage. You should proactively use the 'task' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the 'save_memory' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export type ExecutingToolCall = {
|
|||||||
request: ToolCallRequestInfo;
|
request: ToolCallRequestInfo;
|
||||||
tool: AnyDeclarativeTool;
|
tool: AnyDeclarativeTool;
|
||||||
invocation: AnyToolInvocation;
|
invocation: AnyToolInvocation;
|
||||||
liveOutput?: string;
|
liveOutput?: ToolResultDisplay;
|
||||||
startTime?: number;
|
startTime?: number;
|
||||||
outcome?: ToolConfirmationOutcome;
|
outcome?: ToolConfirmationOutcome;
|
||||||
};
|
};
|
||||||
@@ -120,7 +120,7 @@ export type ConfirmHandler = (
|
|||||||
|
|
||||||
export type OutputUpdateHandler = (
|
export type OutputUpdateHandler = (
|
||||||
toolCallId: string,
|
toolCallId: string,
|
||||||
outputChunk: string,
|
outputChunk: ToolResultDisplay,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export type AllToolCallsCompleteHandler = (
|
export type AllToolCallsCompleteHandler = (
|
||||||
@@ -818,7 +818,7 @@ export class CoreToolScheduler {
|
|||||||
|
|
||||||
const liveOutputCallback =
|
const liveOutputCallback =
|
||||||
scheduledCall.tool.canUpdateOutput && this.outputUpdateHandler
|
scheduledCall.tool.canUpdateOutput && this.outputUpdateHandler
|
||||||
? (outputChunk: string) => {
|
? (outputChunk: ToolResultDisplay) => {
|
||||||
if (this.outputUpdateHandler) {
|
if (this.outputUpdateHandler) {
|
||||||
this.outputUpdateHandler(callId, outputChunk);
|
this.outputUpdateHandler(callId, outputChunk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import process from 'node:process';
|
|||||||
import { isGitRepository } from '../utils/gitUtils.js';
|
import { isGitRepository } from '../utils/gitUtils.js';
|
||||||
import { MemoryTool, GEMINI_CONFIG_DIR } from '../tools/memoryTool.js';
|
import { MemoryTool, GEMINI_CONFIG_DIR } from '../tools/memoryTool.js';
|
||||||
import { TodoWriteTool } from '../tools/todoWrite.js';
|
import { TodoWriteTool } from '../tools/todoWrite.js';
|
||||||
|
import { TaskTool } from '../tools/task.js';
|
||||||
import { GenerateContentConfig } from '@google/genai';
|
import { GenerateContentConfig } from '@google/genai';
|
||||||
|
|
||||||
export interface ModelTemplateMapping {
|
export interface ModelTemplateMapping {
|
||||||
@@ -284,6 +285,7 @@ IMPORTANT: Always use the ${TodoWriteTool.Name} tool to plan and track tasks thr
|
|||||||
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
- **Background Processes:** Use background processes (via \`&\`) for commands that are unlikely to stop on their own, e.g. \`node server.js &\`. If unsure, ask the user.
|
||||||
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
- **Interactive Commands:** Try to avoid shell commands that are likely to require user interaction (e.g. \`git rebase -i\`). Use non-interactive versions of commands (e.g. \`npm init -y\` instead of \`npm init\`) when available, and otherwise remind the user that interactive shell commands are not supported and may cause hangs until canceled by the user.
|
||||||
- **Task Management:** Use the '${TodoWriteTool.Name}' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
- **Task Management:** Use the '${TodoWriteTool.Name}' tool proactively for complex, multi-step tasks to track progress and provide visibility to users. This tool helps organize work systematically and ensures no requirements are missed.
|
||||||
|
- **Subagent Delegation:** When doing file search, prefer to use the '${TaskTool.Name}' tool in order to reduce context usage. You should proactively use the '${TaskTool.Name}' tool with specialized agents when the task at hand matches the agent's description.
|
||||||
- **Remembering Facts:** Use the '${MemoryTool.Name}' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
- **Remembering Facts:** Use the '${MemoryTool.Name}' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information. If unsure whether to save something, you can ask the user, "Should I remember that for you?"
|
||||||
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
- **Respect User Confirmations:** Most tool calls (also denoted as 'function calls') will first require confirmation from the user, where they will either approve or cancel the function call. If a user cancels a function call, respect their choice and do _not_ try to make the function call again. It is okay to request the tool call again _only_ if the user requests that same tool call on a subsequent prompt. When a user cancels a function call, assume best intentions from the user and consider inquiring if they prefer any alternative paths forward.
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ export type {
|
|||||||
ToolConfig,
|
ToolConfig,
|
||||||
SubagentTerminateMode,
|
SubagentTerminateMode,
|
||||||
OutputObject,
|
OutputObject,
|
||||||
} from '../core/subagent.js';
|
} from './subagent.js';
|
||||||
|
|
||||||
export { SubAgentScope } from '../core/subagent.js';
|
export { SubAgentScope } from './subagent.js';
|
||||||
|
|
||||||
|
// Event system for UI integration
|
||||||
|
export type {
|
||||||
|
SubAgentEvent,
|
||||||
|
SubAgentStartEvent,
|
||||||
|
SubAgentFinishEvent,
|
||||||
|
SubAgentRoundEvent,
|
||||||
|
SubAgentToolCallEvent,
|
||||||
|
SubAgentToolResultEvent,
|
||||||
|
SubAgentModelTextEvent,
|
||||||
|
} from './subagent-events.js';
|
||||||
|
|
||||||
|
export { SubAgentEventEmitter } from './subagent-events.js';
|
||||||
|
|||||||
89
packages/core/src/subagents/subagent-events.ts
Normal file
89
packages/core/src/subagents/subagent-events.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export type SubAgentEvent =
|
||||||
|
| 'start'
|
||||||
|
| 'round_start'
|
||||||
|
| 'round_end'
|
||||||
|
| 'model_text'
|
||||||
|
| 'tool_call'
|
||||||
|
| 'tool_result'
|
||||||
|
| 'finish'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
export interface SubAgentModelTextEvent {
|
||||||
|
subagentId: string;
|
||||||
|
round: number;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentStartEvent {
|
||||||
|
subagentId: string;
|
||||||
|
name: string;
|
||||||
|
model?: string;
|
||||||
|
tools: string[];
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentRoundEvent {
|
||||||
|
subagentId: string;
|
||||||
|
round: number;
|
||||||
|
promptId: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentToolCallEvent {
|
||||||
|
subagentId: string;
|
||||||
|
round: number;
|
||||||
|
callId: string;
|
||||||
|
name: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentToolResultEvent {
|
||||||
|
subagentId: string;
|
||||||
|
round: number;
|
||||||
|
callId: string;
|
||||||
|
name: string;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentFinishEvent {
|
||||||
|
subagentId: string;
|
||||||
|
terminate_reason: string;
|
||||||
|
timestamp: number;
|
||||||
|
rounds?: number;
|
||||||
|
totalDurationMs?: number;
|
||||||
|
totalToolCalls?: number;
|
||||||
|
successfulToolCalls?: number;
|
||||||
|
failedToolCalls?: number;
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubAgentEventEmitter {
|
||||||
|
private ee = new EventEmitter();
|
||||||
|
|
||||||
|
on(event: SubAgentEvent, listener: (...args: unknown[]) => void) {
|
||||||
|
this.ee.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: SubAgentEvent, listener: (...args: unknown[]) => void) {
|
||||||
|
this.ee.off(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: SubAgentEvent, payload: unknown) {
|
||||||
|
this.ee.emit(event, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/core/src/subagents/subagent-hooks.ts
Normal file
33
packages/core/src/subagents/subagent-hooks.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PreToolUsePayload {
|
||||||
|
subagentId: string;
|
||||||
|
name: string; // subagent name
|
||||||
|
toolName: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostToolUsePayload extends PreToolUsePayload {
|
||||||
|
success: boolean;
|
||||||
|
durationMs: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentStopPayload {
|
||||||
|
subagentId: string;
|
||||||
|
name: string; // subagent name
|
||||||
|
terminateReason: string;
|
||||||
|
summary: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentHooks {
|
||||||
|
preToolUse?(payload: PreToolUsePayload): Promise<void> | void;
|
||||||
|
postToolUse?(payload: PostToolUsePayload): Promise<void> | void;
|
||||||
|
onStop?(payload: SubagentStopPayload): Promise<void> | void;
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import * as os from 'os';
|
|||||||
import { SubagentManager } from './subagent-manager.js';
|
import { SubagentManager } from './subagent-manager.js';
|
||||||
import { SubagentConfig, SubagentError } from './types.js';
|
import { SubagentConfig, SubagentError } from './types.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
|
import { Config } from '../config/config.js';
|
||||||
|
import { makeFakeConfig } from '../test-utils/config.js';
|
||||||
|
|
||||||
// Mock file system operations
|
// Mock file system operations
|
||||||
vi.mock('fs/promises');
|
vi.mock('fs/promises');
|
||||||
@@ -36,15 +38,30 @@ vi.mock('./validation.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../core/subagent.js');
|
vi.mock('./subagent.js');
|
||||||
|
|
||||||
describe('SubagentManager', () => {
|
describe('SubagentManager', () => {
|
||||||
let manager: SubagentManager;
|
let manager: SubagentManager;
|
||||||
let mockToolRegistry: ToolRegistry;
|
let mockToolRegistry: ToolRegistry;
|
||||||
const projectRoot = '/test/project';
|
let mockConfig: Config;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockToolRegistry = {} as ToolRegistry;
|
mockToolRegistry = {
|
||||||
|
getAllTools: vi.fn().mockReturnValue([
|
||||||
|
{ name: 'read_file', displayName: 'Read File' },
|
||||||
|
{ name: 'write_file', displayName: 'Write File' },
|
||||||
|
{ name: 'grep', displayName: 'Search Files' },
|
||||||
|
]),
|
||||||
|
} as unknown as ToolRegistry;
|
||||||
|
|
||||||
|
// Create mock Config object using test utility
|
||||||
|
mockConfig = makeFakeConfig({
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the tool registry and project root methods
|
||||||
|
vi.spyOn(mockConfig, 'getToolRegistry').mockReturnValue(mockToolRegistry);
|
||||||
|
vi.spyOn(mockConfig, 'getProjectRoot').mockReturnValue('/test/project');
|
||||||
|
|
||||||
// Mock os.homedir
|
// Mock os.homedir
|
||||||
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
||||||
@@ -134,7 +151,7 @@ describe('SubagentManager', () => {
|
|||||||
return yaml.trim();
|
return yaml.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
manager = new SubagentManager(projectRoot, mockToolRegistry);
|
manager = new SubagentManager(mockConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -739,6 +756,25 @@ System prompt 3`);
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should transform display names to tool names in tool configuration', () => {
|
||||||
|
const configWithDisplayNames: SubagentConfig = {
|
||||||
|
...validConfig,
|
||||||
|
tools: ['Read File', 'write_file', 'Search Files', 'unknown_tool'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtimeConfig = manager.convertToRuntimeConfig(
|
||||||
|
configWithDisplayNames,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(runtimeConfig.toolConfig).toBeDefined();
|
||||||
|
expect(runtimeConfig.toolConfig!.tools).toEqual([
|
||||||
|
'read_file', // 'Read File' -> 'read_file' (display name match)
|
||||||
|
'write_file', // 'write_file' -> 'write_file' (exact name match)
|
||||||
|
'grep', // 'Search Files' -> 'grep' (display name match)
|
||||||
|
'unknown_tool', // 'unknown_tool' -> 'unknown_tool' (preserved as-is)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should merge custom model and run configurations', () => {
|
it('should merge custom model and run configurations', () => {
|
||||||
const configWithCustom: SubagentConfig = {
|
const configWithCustom: SubagentConfig = {
|
||||||
...validConfig,
|
...validConfig,
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ import {
|
|||||||
ModelConfig,
|
ModelConfig,
|
||||||
RunConfig,
|
RunConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
} from '../core/subagent.js';
|
} from './subagent.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
|
||||||
|
|
||||||
const QWEN_CONFIG_DIR = '.qwen';
|
const QWEN_CONFIG_DIR = '.qwen';
|
||||||
const AGENT_CONFIG_DIR = 'agents';
|
const AGENT_CONFIG_DIR = 'agents';
|
||||||
@@ -43,11 +42,8 @@ const AGENT_CONFIG_DIR = 'agents';
|
|||||||
export class SubagentManager {
|
export class SubagentManager {
|
||||||
private readonly validator: SubagentValidator;
|
private readonly validator: SubagentValidator;
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly config: Config) {
|
||||||
private readonly projectRoot: string,
|
this.validator = new SubagentValidator();
|
||||||
private readonly toolRegistry?: ToolRegistry,
|
|
||||||
) {
|
|
||||||
this.validator = new SubagentValidator(toolRegistry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +57,6 @@ export class SubagentManager {
|
|||||||
config: SubagentConfig,
|
config: SubagentConfig,
|
||||||
options: CreateSubagentOptions,
|
options: CreateSubagentOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Validate the configuration
|
|
||||||
this.validator.validateOrThrow(config);
|
this.validator.validateOrThrow(config);
|
||||||
|
|
||||||
// Determine file path
|
// Determine file path
|
||||||
@@ -381,7 +376,7 @@ export class SubagentManager {
|
|||||||
// Determine level from file path
|
// Determine level from file path
|
||||||
// Project level paths contain the project root, user level paths are in home directory
|
// Project level paths contain the project root, user level paths are in home directory
|
||||||
const isProjectLevel =
|
const isProjectLevel =
|
||||||
filePath.includes(this.projectRoot) &&
|
filePath.includes(this.config.getProjectRoot()) &&
|
||||||
filePath.includes(`/${QWEN_CONFIG_DIR}/${AGENT_CONFIG_DIR}/`);
|
filePath.includes(`/${QWEN_CONFIG_DIR}/${AGENT_CONFIG_DIR}/`);
|
||||||
const level: SubagentLevel = isProjectLevel ? 'project' : 'user';
|
const level: SubagentLevel = isProjectLevel ? 'project' : 'user';
|
||||||
|
|
||||||
@@ -393,11 +388,9 @@ export class SubagentManager {
|
|||||||
level,
|
level,
|
||||||
filePath,
|
filePath,
|
||||||
modelConfig: modelConfig as Partial<
|
modelConfig: modelConfig as Partial<
|
||||||
import('../core/subagent.js').ModelConfig
|
import('./subagent.js').ModelConfig
|
||||||
>,
|
|
||||||
runConfig: runConfig as Partial<
|
|
||||||
import('../core/subagent.js').RunConfig
|
|
||||||
>,
|
>,
|
||||||
|
runConfig: runConfig as Partial<import('./subagent.js').RunConfig>,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -433,6 +426,8 @@ export class SubagentManager {
|
|||||||
frontmatter['tools'] = config.tools;
|
frontmatter['tools'] = config.tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No outputs section
|
||||||
|
|
||||||
if (config.modelConfig) {
|
if (config.modelConfig) {
|
||||||
frontmatter['modelConfig'] = config.modelConfig;
|
frontmatter['modelConfig'] = config.modelConfig;
|
||||||
}
|
}
|
||||||
@@ -465,6 +460,10 @@ export class SubagentManager {
|
|||||||
async createSubagentScope(
|
async createSubagentScope(
|
||||||
config: SubagentConfig,
|
config: SubagentConfig,
|
||||||
runtimeContext: Config,
|
runtimeContext: Config,
|
||||||
|
options?: {
|
||||||
|
eventEmitter?: import('./subagent-events.js').SubAgentEventEmitter;
|
||||||
|
hooks?: import('./subagent-hooks.js').SubagentHooks;
|
||||||
|
},
|
||||||
): Promise<SubAgentScope> {
|
): Promise<SubAgentScope> {
|
||||||
try {
|
try {
|
||||||
const runtimeConfig = this.convertToRuntimeConfig(config);
|
const runtimeConfig = this.convertToRuntimeConfig(config);
|
||||||
@@ -476,6 +475,8 @@ export class SubagentManager {
|
|||||||
runtimeConfig.modelConfig,
|
runtimeConfig.modelConfig,
|
||||||
runtimeConfig.runConfig,
|
runtimeConfig.runConfig,
|
||||||
runtimeConfig.toolConfig,
|
runtimeConfig.toolConfig,
|
||||||
|
options?.eventEmitter,
|
||||||
|
options?.hooks,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -515,8 +516,10 @@ export class SubagentManager {
|
|||||||
// Build tool configuration if tools are specified
|
// Build tool configuration if tools are specified
|
||||||
let toolConfig: ToolConfig | undefined;
|
let toolConfig: ToolConfig | undefined;
|
||||||
if (config.tools && config.tools.length > 0) {
|
if (config.tools && config.tools.length > 0) {
|
||||||
|
// Transform tools array to ensure all entries are tool names (not display names)
|
||||||
|
const toolNames = this.transformToToolNames(config.tools);
|
||||||
toolConfig = {
|
toolConfig = {
|
||||||
tools: config.tools,
|
tools: toolNames,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +531,53 @@ export class SubagentManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a tools array that may contain tool names or display names
|
||||||
|
* into an array containing only tool names.
|
||||||
|
*
|
||||||
|
* @param tools - Array of tool names or display names
|
||||||
|
* @returns Array of tool names
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private transformToToolNames(tools: string[]): string[] {
|
||||||
|
const toolRegistry = this.config.getToolRegistry();
|
||||||
|
if (!toolRegistry) {
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTools = toolRegistry.getAllTools();
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const toolIdentifier of tools) {
|
||||||
|
// First, try to find an exact match by tool name (highest priority)
|
||||||
|
const exactNameMatch = allTools.find(
|
||||||
|
(tool) => tool.name === toolIdentifier,
|
||||||
|
);
|
||||||
|
if (exactNameMatch) {
|
||||||
|
result.push(exactNameMatch.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact name match, try to find by display name
|
||||||
|
const displayNameMatch = allTools.find(
|
||||||
|
(tool) => tool.displayName === toolIdentifier,
|
||||||
|
);
|
||||||
|
if (displayNameMatch) {
|
||||||
|
result.push(displayNameMatch.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match found, preserve the original identifier as-is
|
||||||
|
// This allows for tools that might not be registered yet or custom tools
|
||||||
|
result.push(toolIdentifier);
|
||||||
|
console.warn(
|
||||||
|
`Tool "${toolIdentifier}" not found in tool registry, preserving as-is`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges partial configurations with defaults, useful for updating
|
* Merges partial configurations with defaults, useful for updating
|
||||||
* existing configurations.
|
* existing configurations.
|
||||||
@@ -563,7 +613,11 @@ export class SubagentManager {
|
|||||||
getSubagentPath(name: string, level: SubagentLevel): string {
|
getSubagentPath(name: string, level: SubagentLevel): string {
|
||||||
const baseDir =
|
const baseDir =
|
||||||
level === 'project'
|
level === 'project'
|
||||||
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
? path.join(
|
||||||
|
this.config.getProjectRoot(),
|
||||||
|
QWEN_CONFIG_DIR,
|
||||||
|
AGENT_CONFIG_DIR,
|
||||||
|
)
|
||||||
: path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR);
|
: path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR);
|
||||||
|
|
||||||
return path.join(baseDir, `${name}.md`);
|
return path.join(baseDir, `${name}.md`);
|
||||||
@@ -580,7 +634,11 @@ export class SubagentManager {
|
|||||||
): Promise<SubagentConfig[]> {
|
): Promise<SubagentConfig[]> {
|
||||||
const baseDir =
|
const baseDir =
|
||||||
level === 'project'
|
level === 'project'
|
||||||
? path.join(this.projectRoot, QWEN_CONFIG_DIR, AGENT_CONFIG_DIR)
|
? path.join(
|
||||||
|
this.config.getProjectRoot(),
|
||||||
|
QWEN_CONFIG_DIR,
|
||||||
|
AGENT_CONFIG_DIR,
|
||||||
|
)
|
||||||
: path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR);
|
: path.join(os.homedir(), QWEN_CONFIG_DIR, AGENT_CONFIG_DIR);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -630,20 +688,4 @@ export class SubagentManager {
|
|||||||
|
|
||||||
return false; // Name is already in use
|
return false; // Name is already in use
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets available tools from the tool registry.
|
|
||||||
* Useful for validation and UI purposes.
|
|
||||||
*
|
|
||||||
* @returns Array of available tool names
|
|
||||||
*/
|
|
||||||
getAvailableTools(): string[] {
|
|
||||||
if (!this.toolRegistry) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This would need to be implemented in ToolRegistry
|
|
||||||
// For now, return empty array
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
186
packages/core/src/subagents/subagent-result-format.ts
Normal file
186
packages/core/src/subagents/subagent-result-format.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCompact(
|
||||||
|
stats: SubAgentBasicStats,
|
||||||
|
taskDesc: string,
|
||||||
|
): string {
|
||||||
|
const sr =
|
||||||
|
stats.totalToolCalls > 0
|
||||||
|
? (stats.successRate ??
|
||||||
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
||||||
|
: 0;
|
||||||
|
const lines = [
|
||||||
|
`📋 Task Completed: ${taskDesc}`,
|
||||||
|
`🔧 Tool Usage: ${stats.totalToolCalls} calls${stats.totalToolCalls ? `, ${sr.toFixed(1)}% success` : ''}`,
|
||||||
|
`⏱️ Duration: ${fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`,
|
||||||
|
];
|
||||||
|
if (typeof stats.totalTokens === 'number') {
|
||||||
|
lines.push(
|
||||||
|
`🔢 Tokens: ${stats.totalTokens.toLocaleString()}${stats.inputTokens || stats.outputTokens ? ` (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})` : ''}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDetailed(
|
||||||
|
stats: SubAgentBasicStats & {
|
||||||
|
toolUsage?: Array<{
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
success: number;
|
||||||
|
failure: number;
|
||||||
|
lastError?: string;
|
||||||
|
averageDurationMs?: number;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
taskDesc: string,
|
||||||
|
): string {
|
||||||
|
const sr =
|
||||||
|
stats.totalToolCalls > 0
|
||||||
|
? (stats.successRate ??
|
||||||
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
||||||
|
: 0;
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`📋 Task Completed: ${taskDesc}`);
|
||||||
|
lines.push(
|
||||||
|
`⏱️ Duration: ${fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`,
|
||||||
|
);
|
||||||
|
// Quality indicator
|
||||||
|
let quality = 'Poor execution';
|
||||||
|
if (sr >= 95) quality = 'Excellent execution';
|
||||||
|
else if (sr >= 85) quality = 'Good execution';
|
||||||
|
else if (sr >= 70) quality = 'Fair execution';
|
||||||
|
lines.push(`✅ Quality: ${quality} (${sr.toFixed(1)}% tool success)`);
|
||||||
|
// Speed category
|
||||||
|
const d = stats.totalDurationMs;
|
||||||
|
let speed = 'Long execution - consider breaking down tasks';
|
||||||
|
if (d < 10_000) speed = 'Fast completion - under 10 seconds';
|
||||||
|
else if (d < 60_000) speed = 'Good speed - under a minute';
|
||||||
|
else if (d < 300_000) speed = 'Moderate duration - a few minutes';
|
||||||
|
lines.push(`🚀 Speed: ${speed}`);
|
||||||
|
lines.push(
|
||||||
|
`🔧 Tools: ${stats.totalToolCalls} calls, ${sr.toFixed(1)}% success (${stats.successfulToolCalls} ok, ${stats.failedToolCalls} failed)`,
|
||||||
|
);
|
||||||
|
if (typeof stats.totalTokens === 'number') {
|
||||||
|
lines.push(
|
||||||
|
`🔢 Tokens: ${stats.totalTokens.toLocaleString()} (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stats.toolUsage && stats.toolUsage.length) {
|
||||||
|
const sorted = [...stats.toolUsage]
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.slice(0, 5);
|
||||||
|
lines.push('\nTop tools:');
|
||||||
|
for (const t of sorted) {
|
||||||
|
const avg =
|
||||||
|
typeof t.averageDurationMs === 'number'
|
||||||
|
? `, avg ${fmtDuration(Math.round(t.averageDurationMs))}`
|
||||||
|
: '';
|
||||||
|
lines.push(
|
||||||
|
` - ${t.name}: ${t.count} calls (${t.success} ok, ${t.failure} fail${avg}${t.lastError ? `, last error: ${t.lastError}` : ''})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tips = generatePerformanceTips(stats);
|
||||||
|
if (tips.length) {
|
||||||
|
lines.push('\n💡 Performance Insights:');
|
||||||
|
for (const tip of tips.slice(0, 3)) lines.push(` - ${tip}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePerformanceTips(
|
||||||
|
stats: SubAgentBasicStats & {
|
||||||
|
toolUsage?: Array<{
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
success: number;
|
||||||
|
failure: number;
|
||||||
|
lastError?: string;
|
||||||
|
averageDurationMs?: number;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
): string[] {
|
||||||
|
const tips: string[] = [];
|
||||||
|
const totalCalls = stats.totalToolCalls;
|
||||||
|
const sr =
|
||||||
|
stats.totalToolCalls > 0
|
||||||
|
? (stats.successRate ??
|
||||||
|
(stats.successfulToolCalls / stats.totalToolCalls) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// High failure rate
|
||||||
|
if (sr < 80)
|
||||||
|
tips.push('Low tool success rate - review inputs and error messages');
|
||||||
|
|
||||||
|
// Long duration
|
||||||
|
if (stats.totalDurationMs > 60_000)
|
||||||
|
tips.push('Long execution time - consider breaking down complex tasks');
|
||||||
|
|
||||||
|
// Token usage
|
||||||
|
if (typeof stats.totalTokens === 'number' && stats.totalTokens > 100_000) {
|
||||||
|
tips.push(
|
||||||
|
'High token usage - consider optimizing prompts or narrowing scope',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof stats.totalTokens === 'number' && totalCalls > 0) {
|
||||||
|
const avgTokPerCall = stats.totalTokens / totalCalls;
|
||||||
|
if (avgTokPerCall > 5_000)
|
||||||
|
tips.push(
|
||||||
|
`High token usage per tool call (~${Math.round(avgTokPerCall)} tokens/call)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network failures
|
||||||
|
const isNetworkTool = (name: string) => /web|fetch|search/i.test(name);
|
||||||
|
const hadNetworkFailure = (stats.toolUsage || []).some(
|
||||||
|
(t) =>
|
||||||
|
isNetworkTool(t.name) &&
|
||||||
|
t.lastError &&
|
||||||
|
/timeout|network/i.test(t.lastError),
|
||||||
|
);
|
||||||
|
if (hadNetworkFailure)
|
||||||
|
tips.push(
|
||||||
|
'Network operations had failures - consider increasing timeout or checking connectivity',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slow tools
|
||||||
|
const slow = (stats.toolUsage || [])
|
||||||
|
.filter((t) => (t.averageDurationMs ?? 0) > 10_000)
|
||||||
|
.sort((a, b) => (b.averageDurationMs ?? 0) - (a.averageDurationMs ?? 0));
|
||||||
|
if (slow.length)
|
||||||
|
tips.push(
|
||||||
|
`Consider optimizing ${slow[0].name} operations (avg ${fmtDuration(Math.round(slow[0].averageDurationMs!))})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
105
packages/core/src/subagents/subagent-statistics.ts
Normal file
105
packages/core/src/subagents/subagent-statistics.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ToolUsageStats {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
success: number;
|
||||||
|
failure: number;
|
||||||
|
lastError?: string;
|
||||||
|
totalDurationMs: number;
|
||||||
|
averageDurationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubagentSummary {
|
||||||
|
rounds: number;
|
||||||
|
totalDurationMs: number;
|
||||||
|
totalToolCalls: number;
|
||||||
|
successfulToolCalls: number;
|
||||||
|
failedToolCalls: number;
|
||||||
|
successRate: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
estimatedCost: number;
|
||||||
|
toolUsage: ToolUsageStats[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubagentStatistics {
|
||||||
|
private startTimeMs = 0;
|
||||||
|
private rounds = 0;
|
||||||
|
private totalToolCalls = 0;
|
||||||
|
private successfulToolCalls = 0;
|
||||||
|
private failedToolCalls = 0;
|
||||||
|
private inputTokens = 0;
|
||||||
|
private outputTokens = 0;
|
||||||
|
private toolUsage = new Map<string, ToolUsageStats>();
|
||||||
|
|
||||||
|
start(now = Date.now()) {
|
||||||
|
this.startTimeMs = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRounds(rounds: number) {
|
||||||
|
this.rounds = rounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
recordToolCall(
|
||||||
|
name: string,
|
||||||
|
success: boolean,
|
||||||
|
durationMs: number,
|
||||||
|
lastError?: string,
|
||||||
|
) {
|
||||||
|
this.totalToolCalls += 1;
|
||||||
|
if (success) this.successfulToolCalls += 1;
|
||||||
|
else this.failedToolCalls += 1;
|
||||||
|
|
||||||
|
const tu = this.toolUsage.get(name) || {
|
||||||
|
name,
|
||||||
|
count: 0,
|
||||||
|
success: 0,
|
||||||
|
failure: 0,
|
||||||
|
lastError: undefined,
|
||||||
|
totalDurationMs: 0,
|
||||||
|
averageDurationMs: 0,
|
||||||
|
};
|
||||||
|
tu.count += 1;
|
||||||
|
if (success) tu.success += 1;
|
||||||
|
else tu.failure += 1;
|
||||||
|
if (lastError) tu.lastError = lastError;
|
||||||
|
tu.totalDurationMs += Math.max(0, durationMs || 0);
|
||||||
|
tu.averageDurationMs = tu.count > 0 ? tu.totalDurationMs / tu.count : 0;
|
||||||
|
this.toolUsage.set(name, tu);
|
||||||
|
}
|
||||||
|
|
||||||
|
recordTokens(input: number, output: number) {
|
||||||
|
this.inputTokens += Math.max(0, input || 0);
|
||||||
|
this.outputTokens += Math.max(0, output || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSummary(now = Date.now()): SubagentSummary {
|
||||||
|
const totalDurationMs = this.startTimeMs ? now - this.startTimeMs : 0;
|
||||||
|
const totalToolCalls = this.totalToolCalls;
|
||||||
|
const successRate =
|
||||||
|
totalToolCalls > 0
|
||||||
|
? (this.successfulToolCalls / totalToolCalls) * 100
|
||||||
|
: 0;
|
||||||
|
const totalTokens = this.inputTokens + this.outputTokens;
|
||||||
|
const estimatedCost = this.inputTokens * 3e-5 + this.outputTokens * 6e-5;
|
||||||
|
return {
|
||||||
|
rounds: this.rounds,
|
||||||
|
totalDurationMs,
|
||||||
|
totalToolCalls,
|
||||||
|
successfulToolCalls: this.successfulToolCalls,
|
||||||
|
failedToolCalls: this.failedToolCalls,
|
||||||
|
successRate,
|
||||||
|
inputTokens: this.inputTokens,
|
||||||
|
outputTokens: this.outputTokens,
|
||||||
|
totalTokens,
|
||||||
|
estimatedCost,
|
||||||
|
toolUsage: Array.from(this.toolUsage.values()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,13 @@ import {
|
|||||||
PromptConfig,
|
PromptConfig,
|
||||||
ModelConfig,
|
ModelConfig,
|
||||||
RunConfig,
|
RunConfig,
|
||||||
OutputConfig,
|
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
} from './subagent.js';
|
} from './subagent.js';
|
||||||
import { Config, ConfigParameters } from '../config/config.js';
|
import { Config, ConfigParameters } from '../config/config.js';
|
||||||
import { GeminiChat } from './geminiChat.js';
|
import { GeminiChat } from '../core/geminiChat.js';
|
||||||
import { createContentGenerator } from './contentGenerator.js';
|
import { createContentGenerator } from '../core/contentGenerator.js';
|
||||||
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
||||||
import { executeToolCall } from './nonInteractiveToolExecutor.js';
|
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||||
import {
|
import {
|
||||||
@@ -31,10 +30,10 @@ import {
|
|||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { ToolErrorType } from '../tools/tool-error.js';
|
import { ToolErrorType } from '../tools/tool-error.js';
|
||||||
|
|
||||||
vi.mock('./geminiChat.js');
|
vi.mock('../core/geminiChat.js');
|
||||||
vi.mock('./contentGenerator.js');
|
vi.mock('../core/contentGenerator.js');
|
||||||
vi.mock('../utils/environmentContext.js');
|
vi.mock('../utils/environmentContext.js');
|
||||||
vi.mock('./nonInteractiveToolExecutor.js');
|
vi.mock('../core/nonInteractiveToolExecutor.js');
|
||||||
vi.mock('../ide/ide-client.js');
|
vi.mock('../ide/ide-client.js');
|
||||||
|
|
||||||
async function createMockConfig(
|
async function createMockConfig(
|
||||||
@@ -55,6 +54,7 @@ async function createMockConfig(
|
|||||||
// Mock ToolRegistry
|
// Mock ToolRegistry
|
||||||
const mockToolRegistry = {
|
const mockToolRegistry = {
|
||||||
getTool: vi.fn(),
|
getTool: vi.fn(),
|
||||||
|
getFunctionDeclarations: vi.fn().mockReturnValue([]),
|
||||||
getFunctionDeclarationsFiltered: vi.fn().mockReturnValue([]),
|
getFunctionDeclarationsFiltered: vi.fn().mockReturnValue([]),
|
||||||
...toolRegistryMocks,
|
...toolRegistryMocks,
|
||||||
} as unknown as ToolRegistry;
|
} as unknown as ToolRegistry;
|
||||||
@@ -74,11 +74,27 @@ const createMockStream = (
|
|||||||
return (async function* () {
|
return (async function* () {
|
||||||
if (response === 'stop') {
|
if (response === 'stop') {
|
||||||
// When stopping, the model might return text, but the subagent logic primarily cares about the absence of functionCalls.
|
// When stopping, the model might return text, but the subagent logic primarily cares about the absence of functionCalls.
|
||||||
yield { text: 'Done.' };
|
yield {
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
parts: [{ text: 'Done.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
} else if (response.length > 0) {
|
} else if (response.length > 0) {
|
||||||
yield { functionCalls: response };
|
yield { functionCalls: response };
|
||||||
} else {
|
} else {
|
||||||
yield { text: 'Done.' }; // Handle empty array also as stop
|
yield {
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
parts: [{ text: 'Done.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}; // Handle empty array also as stop
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@@ -134,6 +150,15 @@ describe('subagent.ts', () => {
|
|||||||
sendMessageStream: mockSendMessageStream,
|
sendMessageStream: mockSendMessageStream,
|
||||||
}) as unknown as GeminiChat,
|
}) as unknown as GeminiChat,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Default mock for executeToolCall
|
||||||
|
vi.mocked(executeToolCall).mockResolvedValue({
|
||||||
|
callId: 'default-call',
|
||||||
|
responseParts: 'default response',
|
||||||
|
resultDisplay: 'Default tool result',
|
||||||
|
error: undefined,
|
||||||
|
errorType: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -329,45 +354,6 @@ describe('subagent.ts', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include output instructions in the system prompt when outputs are defined', async () => {
|
|
||||||
const { config } = await createMockConfig();
|
|
||||||
vi.mocked(GeminiChat).mockClear();
|
|
||||||
|
|
||||||
const promptConfig: PromptConfig = { systemPrompt: 'Do the task.' };
|
|
||||||
const outputConfig: OutputConfig = {
|
|
||||||
outputs: {
|
|
||||||
result1: 'The first result',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const context = new ContextState();
|
|
||||||
|
|
||||||
// Model stops immediately
|
|
||||||
mockSendMessageStream.mockImplementation(createMockStream(['stop']));
|
|
||||||
|
|
||||||
const scope = await SubAgentScope.create(
|
|
||||||
'test-agent',
|
|
||||||
config,
|
|
||||||
promptConfig,
|
|
||||||
defaultModelConfig,
|
|
||||||
defaultRunConfig,
|
|
||||||
undefined, // ToolConfig
|
|
||||||
outputConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
await scope.runNonInteractive(context);
|
|
||||||
|
|
||||||
const generationConfig = getGenerationConfigFromMock();
|
|
||||||
const systemInstruction = generationConfig.systemInstruction as string;
|
|
||||||
|
|
||||||
expect(systemInstruction).toContain('Do the task.');
|
|
||||||
expect(systemInstruction).toContain(
|
|
||||||
'you MUST emit the required output variables',
|
|
||||||
);
|
|
||||||
expect(systemInstruction).toContain(
|
|
||||||
"Use 'self.emitvalue' to emit the 'result1' key",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use initialMessages instead of systemPrompt if provided', async () => {
|
it('should use initialMessages instead of systemPrompt if provided', async () => {
|
||||||
const { config } = await createMockConfig();
|
const { config } = await createMockConfig();
|
||||||
vi.mocked(GeminiChat).mockClear();
|
vi.mocked(GeminiChat).mockClear();
|
||||||
@@ -473,7 +459,7 @@ describe('subagent.ts', () => {
|
|||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
||||||
expect(scope.output.emitted_vars).toEqual({});
|
expect(scope.output.result).toBe('Done.');
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||||
// Check the initial message
|
// Check the initial message
|
||||||
expect(mockSendMessageStream.mock.calls[0][0].message).toEqual([
|
expect(mockSendMessageStream.mock.calls[0][0].message).toEqual([
|
||||||
@@ -481,28 +467,11 @@ describe('subagent.ts', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle self.emitvalue and terminate with GOAL when outputs are met', async () => {
|
it('should terminate with GOAL when model provides final text', async () => {
|
||||||
const { config } = await createMockConfig();
|
const { config } = await createMockConfig();
|
||||||
const outputConfig: OutputConfig = {
|
|
||||||
outputs: { result: 'The final result' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Turn 1: Model responds with emitvalue call
|
// Model stops immediately with text response
|
||||||
// Turn 2: Model stops after receiving the tool response
|
mockSendMessageStream.mockImplementation(createMockStream(['stop']));
|
||||||
mockSendMessageStream.mockImplementation(
|
|
||||||
createMockStream([
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'self.emitvalue',
|
|
||||||
args: {
|
|
||||||
emit_variable_name: 'result',
|
|
||||||
emit_variable_value: 'Success!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'stop',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const scope = await SubAgentScope.create(
|
const scope = await SubAgentScope.create(
|
||||||
'test-agent',
|
'test-agent',
|
||||||
@@ -510,21 +479,13 @@ describe('subagent.ts', () => {
|
|||||||
promptConfig,
|
promptConfig,
|
||||||
defaultModelConfig,
|
defaultModelConfig,
|
||||||
defaultRunConfig,
|
defaultRunConfig,
|
||||||
undefined,
|
|
||||||
outputConfig,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
||||||
expect(scope.output.emitted_vars).toEqual({ result: 'Success!' });
|
expect(scope.output.result).toBe('Done.');
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
// Check the tool response sent back in the second call
|
|
||||||
const secondCallArgs = mockSendMessageStream.mock.calls[1][0];
|
|
||||||
expect(secondCallArgs.message).toEqual([
|
|
||||||
{ text: 'Emitted variable result successfully' },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute external tools and provide the response to the model', async () => {
|
it('should execute external tools and provide the response to the model', async () => {
|
||||||
@@ -640,59 +601,6 @@ describe('subagent.ts', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should nudge the model if it stops before emitting all required variables', async () => {
|
|
||||||
const { config } = await createMockConfig();
|
|
||||||
const outputConfig: OutputConfig = {
|
|
||||||
outputs: { required_var: 'Must be present' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Turn 1: Model stops prematurely
|
|
||||||
// Turn 2: Model responds to the nudge and emits the variable
|
|
||||||
// Turn 3: Model stops
|
|
||||||
mockSendMessageStream.mockImplementation(
|
|
||||||
createMockStream([
|
|
||||||
'stop',
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'self.emitvalue',
|
|
||||||
args: {
|
|
||||||
emit_variable_name: 'required_var',
|
|
||||||
emit_variable_value: 'Here it is',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'stop',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const scope = await SubAgentScope.create(
|
|
||||||
'test-agent',
|
|
||||||
config,
|
|
||||||
promptConfig,
|
|
||||||
defaultModelConfig,
|
|
||||||
defaultRunConfig,
|
|
||||||
undefined,
|
|
||||||
outputConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
|
||||||
|
|
||||||
// Check the nudge message sent in Turn 2
|
|
||||||
const secondCallArgs = mockSendMessageStream.mock.calls[1][0];
|
|
||||||
|
|
||||||
// We check that the message contains the required variable name and the nudge phrasing.
|
|
||||||
expect(secondCallArgs.message[0].text).toContain('required_var');
|
|
||||||
expect(secondCallArgs.message[0].text).toContain(
|
|
||||||
'You have stopped calling tools',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
|
||||||
expect(scope.output.emitted_vars).toEqual({
|
|
||||||
required_var: 'Here it is',
|
|
||||||
});
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runNonInteractive - Termination and Recovery', () => {
|
describe('runNonInteractive - Termination and Recovery', () => {
|
||||||
@@ -702,26 +610,26 @@ describe('subagent.ts', () => {
|
|||||||
const { config } = await createMockConfig();
|
const { config } = await createMockConfig();
|
||||||
const runConfig: RunConfig = { ...defaultRunConfig, max_turns: 2 };
|
const runConfig: RunConfig = { ...defaultRunConfig, max_turns: 2 };
|
||||||
|
|
||||||
// Model keeps looping by calling emitvalue repeatedly
|
// Model keeps calling tools repeatedly
|
||||||
mockSendMessageStream.mockImplementation(
|
mockSendMessageStream.mockImplementation(
|
||||||
createMockStream([
|
createMockStream([
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'self.emitvalue',
|
name: 'list_files',
|
||||||
args: { emit_variable_name: 'loop', emit_variable_value: 'v1' },
|
args: { path: '/test' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'self.emitvalue',
|
name: 'list_files',
|
||||||
args: { emit_variable_name: 'loop', emit_variable_value: 'v2' },
|
args: { path: '/test2' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// This turn should not happen
|
// This turn should not happen
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'self.emitvalue',
|
name: 'list_files',
|
||||||
args: { emit_variable_name: 'loop', emit_variable_value: 'v3' },
|
args: { path: '/test3' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import { reportError } from '../utils/errorReporting.js';
|
import { reportError } from '../utils/errorReporting.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
import { ToolCallRequestInfo } from './turn.js';
|
import { ToolCallRequestInfo } from '../core/turn.js';
|
||||||
import { executeToolCall } from './nonInteractiveToolExecutor.js';
|
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
|
||||||
import { createContentGenerator } from './contentGenerator.js';
|
import { createContentGenerator } from '../core/contentGenerator.js';
|
||||||
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
||||||
import {
|
import {
|
||||||
Content,
|
Content,
|
||||||
@@ -16,9 +16,15 @@ import {
|
|||||||
FunctionCall,
|
FunctionCall,
|
||||||
GenerateContentConfig,
|
GenerateContentConfig,
|
||||||
FunctionDeclaration,
|
FunctionDeclaration,
|
||||||
Type,
|
GenerateContentResponseUsageMetadata,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { GeminiChat } from './geminiChat.js';
|
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 { SubagentHooks } from './subagent-hooks.js';
|
||||||
|
import { logSubagentExecution } from '../telemetry/loggers.js';
|
||||||
|
import { SubagentExecutionEvent } from '../telemetry/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview Defines the configuration interfaces for a subagent.
|
* @fileoverview Defines the configuration interfaces for a subagent.
|
||||||
@@ -27,6 +33,19 @@ import { GeminiChat } from './geminiChat.js';
|
|||||||
* the model parameters, and the execution settings.
|
* the model parameters, and the execution settings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
interface ExecutionStats {
|
||||||
|
startTimeMs: number;
|
||||||
|
totalDurationMs: number;
|
||||||
|
rounds: number;
|
||||||
|
totalToolCalls: number;
|
||||||
|
successfulToolCalls: number;
|
||||||
|
failedToolCalls: number;
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
estimatedCost?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the possible termination modes for a subagent.
|
* Describes the possible termination modes for a subagent.
|
||||||
* This enum provides a clear indication of why a subagent's execution might have ended.
|
* This enum provides a clear indication of why a subagent's execution might have ended.
|
||||||
@@ -53,14 +72,14 @@ export enum SubagentTerminateMode {
|
|||||||
/**
|
/**
|
||||||
* Represents the output structure of a subagent's execution.
|
* Represents the output structure of a subagent's execution.
|
||||||
* This interface defines the data that a subagent will return upon completion,
|
* This interface defines the data that a subagent will return upon completion,
|
||||||
* including any emitted variables and the reason for its termination.
|
* including the final result and the reason for its termination.
|
||||||
*/
|
*/
|
||||||
export interface OutputObject {
|
export interface OutputObject {
|
||||||
/**
|
/**
|
||||||
* A record of key-value pairs representing variables emitted by the subagent
|
* The final result text returned by the subagent upon completion.
|
||||||
* during its execution. These variables can be used by the calling agent.
|
* This contains the direct output from the model's final response.
|
||||||
*/
|
*/
|
||||||
emitted_vars: Record<string, string>;
|
result: string;
|
||||||
/**
|
/**
|
||||||
* The reason for the subagent's termination, indicating whether it completed
|
* The reason for the subagent's termination, indicating whether it completed
|
||||||
* successfully, timed out, or encountered an error.
|
* successfully, timed out, or encountered an error.
|
||||||
@@ -96,17 +115,6 @@ export interface ToolConfig {
|
|||||||
tools: Array<string | FunctionDeclaration>;
|
tools: Array<string | FunctionDeclaration>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the expected outputs for the subagent.
|
|
||||||
*/
|
|
||||||
export interface OutputConfig {
|
|
||||||
/**
|
|
||||||
* A record describing the variables the subagent is expected to emit.
|
|
||||||
* The subagent will be prompted to generate these values before terminating.
|
|
||||||
*/
|
|
||||||
outputs: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the generative model parameters for the subagent.
|
* Configures the generative model parameters for the subagent.
|
||||||
* This interface specifies the model to be used and its associated generation settings,
|
* This interface specifies the model to be used and its associated generation settings,
|
||||||
@@ -232,8 +240,35 @@ function templateString(template: string, context: ContextState): string {
|
|||||||
export class SubAgentScope {
|
export class SubAgentScope {
|
||||||
output: OutputObject = {
|
output: OutputObject = {
|
||||||
terminate_reason: SubagentTerminateMode.ERROR,
|
terminate_reason: SubagentTerminateMode.ERROR,
|
||||||
emitted_vars: {},
|
result: '',
|
||||||
};
|
};
|
||||||
|
executionStats: ExecutionStats = {
|
||||||
|
startTimeMs: 0,
|
||||||
|
totalDurationMs: 0,
|
||||||
|
rounds: 0,
|
||||||
|
totalToolCalls: 0,
|
||||||
|
successfulToolCalls: 0,
|
||||||
|
failedToolCalls: 0,
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
estimatedCost: 0,
|
||||||
|
};
|
||||||
|
private toolUsage = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
count: number;
|
||||||
|
success: number;
|
||||||
|
failure: number;
|
||||||
|
lastError?: string;
|
||||||
|
totalDurationMs?: number;
|
||||||
|
averageDurationMs?: number;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
private eventEmitter?: SubAgentEventEmitter;
|
||||||
|
private finalText: string = '';
|
||||||
|
private readonly stats = new SubagentStatistics();
|
||||||
|
private hooks?: SubagentHooks;
|
||||||
private readonly subagentId: string;
|
private readonly subagentId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,7 +279,6 @@ export class SubAgentScope {
|
|||||||
* @param modelConfig - Configuration for the generative model parameters.
|
* @param modelConfig - Configuration for the generative model parameters.
|
||||||
* @param runConfig - Configuration for the subagent's execution environment.
|
* @param runConfig - Configuration for the subagent's execution environment.
|
||||||
* @param toolConfig - Optional configuration for tools available to the subagent.
|
* @param toolConfig - Optional configuration for tools available to the subagent.
|
||||||
* @param outputConfig - Optional configuration for the subagent's expected outputs.
|
|
||||||
*/
|
*/
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
@@ -253,10 +287,13 @@ export class SubAgentScope {
|
|||||||
private readonly modelConfig: ModelConfig,
|
private readonly modelConfig: ModelConfig,
|
||||||
private readonly runConfig: RunConfig,
|
private readonly runConfig: RunConfig,
|
||||||
private readonly toolConfig?: ToolConfig,
|
private readonly toolConfig?: ToolConfig,
|
||||||
private readonly outputConfig?: OutputConfig,
|
eventEmitter?: SubAgentEventEmitter,
|
||||||
|
hooks?: SubagentHooks,
|
||||||
) {
|
) {
|
||||||
const randomPart = Math.random().toString(36).slice(2, 8);
|
const randomPart = Math.random().toString(36).slice(2, 8);
|
||||||
this.subagentId = `${this.name}-${randomPart}`;
|
this.subagentId = `${this.name}-${randomPart}`;
|
||||||
|
this.eventEmitter = eventEmitter;
|
||||||
|
this.hooks = hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,7 +306,6 @@ export class SubAgentScope {
|
|||||||
* @param {ModelConfig} modelConfig - Configuration for the generative model parameters.
|
* @param {ModelConfig} modelConfig - Configuration for the generative model parameters.
|
||||||
* @param {RunConfig} runConfig - Configuration for the subagent's execution environment.
|
* @param {RunConfig} runConfig - Configuration for the subagent's execution environment.
|
||||||
* @param {ToolConfig} [toolConfig] - Optional configuration for tools.
|
* @param {ToolConfig} [toolConfig] - Optional configuration for tools.
|
||||||
* @param {OutputConfig} [outputConfig] - Optional configuration for expected outputs.
|
|
||||||
* @returns {Promise<SubAgentScope>} A promise that resolves to a valid SubAgentScope instance.
|
* @returns {Promise<SubAgentScope>} A promise that resolves to a valid SubAgentScope instance.
|
||||||
* @throws {Error} If any tool requires user confirmation.
|
* @throws {Error} If any tool requires user confirmation.
|
||||||
*/
|
*/
|
||||||
@@ -280,43 +316,60 @@ export class SubAgentScope {
|
|||||||
modelConfig: ModelConfig,
|
modelConfig: ModelConfig,
|
||||||
runConfig: RunConfig,
|
runConfig: RunConfig,
|
||||||
toolConfig?: ToolConfig,
|
toolConfig?: ToolConfig,
|
||||||
outputConfig?: OutputConfig,
|
eventEmitter?: SubAgentEventEmitter,
|
||||||
|
hooks?: SubagentHooks,
|
||||||
): Promise<SubAgentScope> {
|
): Promise<SubAgentScope> {
|
||||||
if (toolConfig) {
|
// Validate tools for non-interactive use
|
||||||
|
if (toolConfig?.tools) {
|
||||||
const toolRegistry = runtimeContext.getToolRegistry();
|
const toolRegistry = runtimeContext.getToolRegistry();
|
||||||
const toolsToLoad: string[] = [];
|
|
||||||
for (const tool of toolConfig.tools) {
|
for (const toolItem of toolConfig.tools) {
|
||||||
if (typeof tool === 'string') {
|
if (typeof toolItem !== 'string') {
|
||||||
toolsToLoad.push(tool);
|
continue; // Skip inline function declarations
|
||||||
|
}
|
||||||
|
const tool = toolRegistry.getTool(toolItem);
|
||||||
|
if (!tool) {
|
||||||
|
continue; // Skip unknown tools
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const toolName of toolsToLoad) {
|
// Check if tool has required parameters
|
||||||
const tool = toolRegistry.getTool(toolName);
|
const hasRequiredParams =
|
||||||
if (tool) {
|
tool.schema?.parameters?.required &&
|
||||||
const requiredParams = tool.schema.parameters?.required ?? [];
|
Array.isArray(tool.schema.parameters.required) &&
|
||||||
if (requiredParams.length > 0) {
|
tool.schema.parameters.required.length > 0;
|
||||||
// This check is imperfect. A tool might require parameters but still
|
|
||||||
// be interactive (e.g., `delete_file(path)`). However, we cannot
|
|
||||||
// build a generic invocation without knowing what dummy parameters
|
|
||||||
// to provide. Crashing here because `build({})` fails is worse
|
|
||||||
// than allowing a potential hang later if an interactive tool is
|
|
||||||
// used. This is a best-effort check.
|
|
||||||
console.warn(
|
|
||||||
`Cannot check tool "${toolName}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invocation = tool.build({});
|
if (hasRequiredParams) {
|
||||||
const confirmationDetails = await invocation.shouldConfirmExecute(
|
// Can't check interactivity without parameters, log warning and continue
|
||||||
|
console.warn(
|
||||||
|
`Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to build the tool to check if it requires confirmation
|
||||||
|
try {
|
||||||
|
const toolInstance = tool.build({});
|
||||||
|
const confirmationDetails = await toolInstance.shouldConfirmExecute(
|
||||||
new AbortController().signal,
|
new AbortController().signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmationDetails) {
|
if (confirmationDetails) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Tool "${toolName}" requires user confirmation and cannot be used in a non-interactive subagent.`,
|
`Tool "${toolItem}" requires user confirmation and cannot be used in a non-interactive subagent.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't build the tool, assume it's safe
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes('requires user confirmation')
|
||||||
|
) {
|
||||||
|
throw error; // Re-throw confirmation errors
|
||||||
|
}
|
||||||
|
// For other build errors, log warning and continue
|
||||||
|
console.warn(
|
||||||
|
`Cannot check tool "${toolItem}" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,7 +381,8 @@ export class SubAgentScope {
|
|||||||
modelConfig,
|
modelConfig,
|
||||||
runConfig,
|
runConfig,
|
||||||
toolConfig,
|
toolConfig,
|
||||||
outputConfig,
|
eventEmitter,
|
||||||
|
hooks,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +393,10 @@ export class SubAgentScope {
|
|||||||
* @param {ContextState} context - The current context state containing variables for prompt templating.
|
* @param {ContextState} context - The current context state containing variables for prompt templating.
|
||||||
* @returns {Promise<void>} A promise that resolves when the subagent has completed its execution.
|
* @returns {Promise<void>} A promise that resolves when the subagent has completed its execution.
|
||||||
*/
|
*/
|
||||||
async runNonInteractive(context: ContextState): Promise<void> {
|
async runNonInteractive(
|
||||||
|
context: ContextState,
|
||||||
|
externalSignal?: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
const chat = await this.createChatObject(context);
|
const chat = await this.createChatObject(context);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
@@ -348,35 +405,64 @@ export class SubAgentScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
const onAbort = () => abortController.abort();
|
||||||
|
if (externalSignal) {
|
||||||
|
if (externalSignal.aborted) abortController.abort();
|
||||||
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
}
|
||||||
const toolRegistry = this.runtimeContext.getToolRegistry();
|
const toolRegistry = this.runtimeContext.getToolRegistry();
|
||||||
|
|
||||||
// Prepare the list of tools available to the subagent.
|
// Prepare the list of tools available to the subagent.
|
||||||
|
// If no explicit toolConfig or it contains "*" or is empty, inherit all tools.
|
||||||
const toolsList: FunctionDeclaration[] = [];
|
const toolsList: FunctionDeclaration[] = [];
|
||||||
if (this.toolConfig) {
|
if (this.toolConfig) {
|
||||||
const toolsToLoad: string[] = [];
|
const asStrings = this.toolConfig.tools.filter(
|
||||||
for (const tool of this.toolConfig.tools) {
|
(t): t is string => typeof t === 'string',
|
||||||
if (typeof tool === 'string') {
|
|
||||||
toolsToLoad.push(tool);
|
|
||||||
} else {
|
|
||||||
toolsList.push(tool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toolsList.push(
|
|
||||||
...toolRegistry.getFunctionDeclarationsFiltered(toolsToLoad),
|
|
||||||
);
|
);
|
||||||
}
|
const hasWildcard = asStrings.includes('*');
|
||||||
// Add local scope functions if outputs are expected.
|
const onlyInlineDecls = this.toolConfig.tools.filter(
|
||||||
if (this.outputConfig && this.outputConfig.outputs) {
|
(t): t is FunctionDeclaration => typeof t !== 'string',
|
||||||
toolsList.push(...this.getScopeLocalFuncDefs());
|
);
|
||||||
|
|
||||||
|
if (hasWildcard || asStrings.length === 0) {
|
||||||
|
toolsList.push(...toolRegistry.getFunctionDeclarations());
|
||||||
|
} else {
|
||||||
|
toolsList.push(
|
||||||
|
...toolRegistry.getFunctionDeclarationsFiltered(asStrings),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
toolsList.push(...onlyInlineDecls);
|
||||||
|
} else {
|
||||||
|
// Inherit all available tools by default when not specified.
|
||||||
|
toolsList.push(...toolRegistry.getFunctionDeclarations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialTaskText = String(
|
||||||
|
(context.get('task_prompt') as string) ?? 'Get Started!',
|
||||||
|
);
|
||||||
let currentMessages: Content[] = [
|
let currentMessages: Content[] = [
|
||||||
{ role: 'user', parts: [{ text: 'Get Started!' }] },
|
{ role: 'user', parts: [{ text: initialTaskText }] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
this.executionStats.startTimeMs = startTime;
|
||||||
|
this.stats.start(startTime);
|
||||||
let turnCounter = 0;
|
let turnCounter = 0;
|
||||||
try {
|
try {
|
||||||
|
// Emit start event
|
||||||
|
this.eventEmitter?.emit('start', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
name: this.name,
|
||||||
|
model: this.modelConfig.model,
|
||||||
|
tools: (this.toolConfig?.tools || ['*']).map((t) =>
|
||||||
|
typeof t === 'string' ? t : t.name,
|
||||||
|
),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log telemetry for subagent start
|
||||||
|
const startEvent = new SubagentExecutionEvent(this.name, 'started');
|
||||||
|
logSubagentExecution(this.runtimeContext, startEvent);
|
||||||
while (true) {
|
while (true) {
|
||||||
// Check termination conditions.
|
// Check termination conditions.
|
||||||
if (
|
if (
|
||||||
@@ -408,12 +494,37 @@ export class SubAgentScope {
|
|||||||
messageParams,
|
messageParams,
|
||||||
promptId,
|
promptId,
|
||||||
);
|
);
|
||||||
|
this.eventEmitter?.emit('round_start', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
round: turnCounter,
|
||||||
|
promptId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
const functionCalls: FunctionCall[] = [];
|
const functionCalls: FunctionCall[] = [];
|
||||||
|
let roundText = '';
|
||||||
|
let lastUsage: GenerateContentResponseUsageMetadata | undefined =
|
||||||
|
undefined;
|
||||||
for await (const resp of responseStream) {
|
for await (const resp of responseStream) {
|
||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) return;
|
||||||
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
|
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
|
||||||
|
const content = resp.candidates?.[0]?.content;
|
||||||
|
const parts = content?.parts || [];
|
||||||
|
for (const p of parts) {
|
||||||
|
const txt = (p as Part & { text?: string }).text;
|
||||||
|
if (txt) roundText += txt;
|
||||||
|
if (txt)
|
||||||
|
this.eventEmitter?.emit('model_text', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
round: turnCounter,
|
||||||
|
text: txt,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resp.usageMetadata) lastUsage = resp.usageMetadata;
|
||||||
}
|
}
|
||||||
|
this.executionStats.rounds = turnCounter;
|
||||||
|
this.stats.setRounds(turnCounter);
|
||||||
|
|
||||||
durationMin = (Date.now() - startTime) / (1000 * 60);
|
durationMin = (Date.now() - startTime) / (1000 * 60);
|
||||||
if (
|
if (
|
||||||
@@ -424,6 +535,31 @@ export class SubAgentScope {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update token usage if available
|
||||||
|
if (lastUsage) {
|
||||||
|
const inTok = Number(lastUsage.promptTokenCount || 0);
|
||||||
|
const outTok = Number(lastUsage.candidatesTokenCount || 0);
|
||||||
|
if (isFinite(inTok) || isFinite(outTok)) {
|
||||||
|
this.stats.recordTokens(
|
||||||
|
isFinite(inTok) ? inTok : 0,
|
||||||
|
isFinite(outTok) ? outTok : 0,
|
||||||
|
);
|
||||||
|
// mirror legacy fields for compatibility
|
||||||
|
this.executionStats.inputTokens =
|
||||||
|
(this.executionStats.inputTokens || 0) +
|
||||||
|
(isFinite(inTok) ? inTok : 0);
|
||||||
|
this.executionStats.outputTokens =
|
||||||
|
(this.executionStats.outputTokens || 0) +
|
||||||
|
(isFinite(outTok) ? outTok : 0);
|
||||||
|
this.executionStats.totalTokens =
|
||||||
|
(this.executionStats.inputTokens || 0) +
|
||||||
|
(this.executionStats.outputTokens || 0);
|
||||||
|
this.executionStats.estimatedCost =
|
||||||
|
(this.executionStats.inputTokens || 0) * 3e-5 +
|
||||||
|
(this.executionStats.outputTokens || 0) * 6e-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (functionCalls.length > 0) {
|
if (functionCalls.length > 0) {
|
||||||
currentMessages = await this.processFunctionCalls(
|
currentMessages = await this.processFunctionCalls(
|
||||||
functionCalls,
|
functionCalls,
|
||||||
@@ -431,42 +567,90 @@ export class SubAgentScope {
|
|||||||
promptId,
|
promptId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Model stopped calling tools. Check if goal is met.
|
// No tool calls — treat this as the model's final answer.
|
||||||
if (
|
if (roundText && roundText.trim().length > 0) {
|
||||||
!this.outputConfig ||
|
this.finalText = roundText.trim();
|
||||||
Object.keys(this.outputConfig.outputs).length === 0
|
this.output.result = this.finalText;
|
||||||
) {
|
|
||||||
this.output.terminate_reason = SubagentTerminateMode.GOAL;
|
this.output.terminate_reason = SubagentTerminateMode.GOAL;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Otherwise, nudge the model to finalize a result.
|
||||||
const remainingVars = Object.keys(this.outputConfig.outputs).filter(
|
|
||||||
(key) => !(key in this.output.emitted_vars),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remainingVars.length === 0) {
|
|
||||||
this.output.terminate_reason = SubagentTerminateMode.GOAL;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nudgeMessage = `You have stopped calling tools but have not emitted the following required variables: ${remainingVars.join(
|
|
||||||
', ',
|
|
||||||
)}. Please use the 'self.emitvalue' tool to emit them now, or continue working if necessary.`;
|
|
||||||
|
|
||||||
console.debug(nudgeMessage);
|
|
||||||
|
|
||||||
currentMessages = [
|
currentMessages = [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
parts: [{ text: nudgeMessage }],
|
parts: [
|
||||||
|
{
|
||||||
|
text: 'Please provide the final result now and stop calling tools.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
this.eventEmitter?.emit('round_end', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
round: turnCounter,
|
||||||
|
promptId,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during subagent execution:', error);
|
console.error('Error during subagent execution:', error);
|
||||||
this.output.terminate_reason = SubagentTerminateMode.ERROR;
|
this.output.terminate_reason = SubagentTerminateMode.ERROR;
|
||||||
|
this.eventEmitter?.emit('error', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log telemetry for subagent error
|
||||||
|
const errorEvent = new SubagentExecutionEvent(this.name, 'failed', {
|
||||||
|
terminate_reason: SubagentTerminateMode.ERROR,
|
||||||
|
result: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
logSubagentExecution(this.runtimeContext, errorEvent);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
||||||
|
this.executionStats.totalDurationMs = Date.now() - startTime;
|
||||||
|
const summary = this.stats.getSummary(Date.now());
|
||||||
|
this.eventEmitter?.emit('finish', {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
terminate_reason: this.output.terminate_reason,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
rounds: summary.rounds,
|
||||||
|
totalDurationMs: summary.totalDurationMs,
|
||||||
|
totalToolCalls: summary.totalToolCalls,
|
||||||
|
successfulToolCalls: summary.successfulToolCalls,
|
||||||
|
failedToolCalls: summary.failedToolCalls,
|
||||||
|
inputTokens: summary.inputTokens,
|
||||||
|
outputTokens: summary.outputTokens,
|
||||||
|
totalTokens: summary.totalTokens,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log telemetry for subagent completion
|
||||||
|
const completionEvent = new SubagentExecutionEvent(
|
||||||
|
this.name,
|
||||||
|
this.output.terminate_reason === SubagentTerminateMode.GOAL
|
||||||
|
? 'completed'
|
||||||
|
: 'failed',
|
||||||
|
{
|
||||||
|
terminate_reason: this.output.terminate_reason,
|
||||||
|
result: this.finalText,
|
||||||
|
execution_summary: this.formatCompactResult(
|
||||||
|
'Subagent execution completed',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
logSubagentExecution(this.runtimeContext, completionEvent);
|
||||||
|
|
||||||
|
await this.hooks?.onStop?.({
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
name: this.name,
|
||||||
|
terminateReason: this.output.terminate_reason,
|
||||||
|
summary: summary as unknown as Record<string, unknown>,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +673,7 @@ export class SubAgentScope {
|
|||||||
const toolResponseParts: Part[] = [];
|
const toolResponseParts: Part[] = [];
|
||||||
|
|
||||||
for (const functionCall of functionCalls) {
|
for (const functionCall of functionCalls) {
|
||||||
|
const toolName = String(functionCall.name || 'unknown');
|
||||||
const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`;
|
const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`;
|
||||||
const requestInfo: ToolCallRequestInfo = {
|
const requestInfo: ToolCallRequestInfo = {
|
||||||
callId,
|
callId,
|
||||||
@@ -498,28 +683,112 @@ export class SubAgentScope {
|
|||||||
prompt_id: promptId,
|
prompt_id: promptId,
|
||||||
};
|
};
|
||||||
|
|
||||||
let toolResponse;
|
// Execute tools with timing and hooks
|
||||||
|
const start = Date.now();
|
||||||
// Handle scope-local tools first.
|
await this.hooks?.preToolUse?.({
|
||||||
if (functionCall.name === 'self.emitvalue') {
|
subagentId: this.subagentId,
|
||||||
const valName = String(requestInfo.args['emit_variable_name']);
|
name: this.name,
|
||||||
const valVal = String(requestInfo.args['emit_variable_value']);
|
toolName,
|
||||||
this.output.emitted_vars[valName] = valVal;
|
args: requestInfo.args,
|
||||||
|
timestamp: Date.now(),
|
||||||
toolResponse = {
|
});
|
||||||
callId,
|
const toolResponse = await executeToolCall(
|
||||||
responseParts: `Emitted variable ${valName} successfully`,
|
this.runtimeContext,
|
||||||
resultDisplay: `Emitted variable ${valName} successfully`,
|
requestInfo,
|
||||||
error: undefined,
|
abortController.signal,
|
||||||
};
|
);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
// Update tool call stats
|
||||||
|
this.executionStats.totalToolCalls += 1;
|
||||||
|
if (toolResponse.error) {
|
||||||
|
this.executionStats.failedToolCalls += 1;
|
||||||
} else {
|
} else {
|
||||||
toolResponse = await executeToolCall(
|
this.executionStats.successfulToolCalls += 1;
|
||||||
this.runtimeContext,
|
|
||||||
requestInfo,
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update per-tool usage
|
||||||
|
const tu = this.toolUsage.get(toolName) || {
|
||||||
|
count: 0,
|
||||||
|
success: 0,
|
||||||
|
failure: 0,
|
||||||
|
totalDurationMs: 0,
|
||||||
|
averageDurationMs: 0,
|
||||||
|
};
|
||||||
|
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';
|
||||||
|
} else {
|
||||||
|
tu.success += 1;
|
||||||
|
}
|
||||||
|
if (typeof tu.totalDurationMs === 'number') {
|
||||||
|
tu.totalDurationMs += duration;
|
||||||
|
tu.averageDurationMs =
|
||||||
|
tu.count > 0 ? tu.totalDurationMs / tu.count : tu.totalDurationMs;
|
||||||
|
} else {
|
||||||
|
tu.totalDurationMs = duration;
|
||||||
|
tu.averageDurationMs = duration;
|
||||||
|
}
|
||||||
|
this.toolUsage.set(toolName, tu);
|
||||||
|
|
||||||
|
// Emit tool call/result events
|
||||||
|
this.eventEmitter?.emit('tool_call', {
|
||||||
|
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,
|
||||||
|
callId,
|
||||||
|
name: toolName,
|
||||||
|
success: !toolResponse?.error,
|
||||||
|
error: toolResponse?.error
|
||||||
|
? typeof toolResponse.resultDisplay === 'string'
|
||||||
|
? toolResponse.resultDisplay
|
||||||
|
: toolResponse.resultDisplay
|
||||||
|
? JSON.stringify(toolResponse.resultDisplay)
|
||||||
|
: toolResponse.error.message
|
||||||
|
: undefined,
|
||||||
|
durationMs: duration,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update statistics service
|
||||||
|
this.stats.recordToolCall(
|
||||||
|
toolName,
|
||||||
|
!toolResponse?.error,
|
||||||
|
duration,
|
||||||
|
this.toolUsage.get(toolName)?.lastError,
|
||||||
|
);
|
||||||
|
|
||||||
|
// post-tool hook
|
||||||
|
await this.hooks?.postToolUse?.({
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
name: this.name,
|
||||||
|
toolName,
|
||||||
|
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,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
if (toolResponse.error) {
|
if (toolResponse.error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`,
|
`Error executing tool ${functionCall.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`,
|
||||||
@@ -549,6 +818,65 @@ export class SubAgentScope {
|
|||||||
return [{ role: 'user', parts: toolResponseParts }];
|
return [{ role: 'user', parts: toolResponseParts }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEventEmitter() {
|
||||||
|
return this.eventEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatistics() {
|
||||||
|
const total = this.executionStats.totalToolCalls;
|
||||||
|
const successRate =
|
||||||
|
total > 0 ? (this.executionStats.successfulToolCalls / total) * 100 : 0;
|
||||||
|
return {
|
||||||
|
...this.executionStats,
|
||||||
|
successRate,
|
||||||
|
toolUsage: Array.from(this.toolUsage.entries()).map(([name, v]) => ({
|
||||||
|
name,
|
||||||
|
...v,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private async createChatObject(context: ContextState) {
|
||||||
if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
|
if (!this.promptConfig.systemPrompt && !this.promptConfig.initialMessages) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -616,43 +944,6 @@ export class SubAgentScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of FunctionDeclaration objects for tools that are local to the subagent's scope.
|
|
||||||
* Currently, this includes the `self.emitvalue` tool for emitting variables.
|
|
||||||
* @returns An array of `FunctionDeclaration` objects.
|
|
||||||
*/
|
|
||||||
private getScopeLocalFuncDefs() {
|
|
||||||
const emitValueTool: FunctionDeclaration = {
|
|
||||||
name: 'self.emitvalue',
|
|
||||||
description: `* This tool emits A SINGLE return value from this execution, such that it can be collected and presented to the calling function.
|
|
||||||
* You can only emit ONE VALUE each time you call this tool. You are expected to call this tool MULTIPLE TIMES if you have MULTIPLE OUTPUTS.`,
|
|
||||||
parameters: {
|
|
||||||
type: Type.OBJECT,
|
|
||||||
properties: {
|
|
||||||
emit_variable_name: {
|
|
||||||
description: 'This is the name of the variable to be returned.',
|
|
||||||
type: Type.STRING,
|
|
||||||
},
|
|
||||||
emit_variable_value: {
|
|
||||||
description:
|
|
||||||
'This is the _value_ to be returned for this variable.',
|
|
||||||
type: Type.STRING,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['emit_variable_name', 'emit_variable_value'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return [emitValueTool];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the system prompt for the chat based on the provided configurations.
|
|
||||||
* It templates the base system prompt and appends instructions for emitting
|
|
||||||
* variables if an `OutputConfig` is provided.
|
|
||||||
* @param {ContextState} context - The context for templating.
|
|
||||||
* @returns {string} The complete system prompt.
|
|
||||||
*/
|
|
||||||
private buildChatSystemPrompt(context: ContextState): string {
|
private buildChatSystemPrompt(context: ContextState): string {
|
||||||
if (!this.promptConfig.systemPrompt) {
|
if (!this.promptConfig.systemPrompt) {
|
||||||
// This should ideally be caught in createChatObject, but serves as a safeguard.
|
// This should ideally be caught in createChatObject, but serves as a safeguard.
|
||||||
@@ -661,23 +952,13 @@ export class SubAgentScope {
|
|||||||
|
|
||||||
let finalPrompt = templateString(this.promptConfig.systemPrompt, context);
|
let finalPrompt = templateString(this.promptConfig.systemPrompt, context);
|
||||||
|
|
||||||
// Add instructions for emitting variables if needed.
|
|
||||||
if (this.outputConfig && this.outputConfig.outputs) {
|
|
||||||
let outputInstructions =
|
|
||||||
'\n\nAfter you have achieved all other goals, you MUST emit the required output variables. For each expected output, make one final call to the `self.emitvalue` tool.';
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(this.outputConfig.outputs)) {
|
|
||||||
outputInstructions += `\n* Use 'self.emitvalue' to emit the '${key}' key, with a value described as: '${value}'`;
|
|
||||||
}
|
|
||||||
finalPrompt += outputInstructions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add general non-interactive instructions.
|
// Add general non-interactive instructions.
|
||||||
finalPrompt += `
|
finalPrompt += `
|
||||||
|
|
||||||
Important Rules:
|
Important Rules:
|
||||||
* You are running in a non-interactive mode. You CANNOT ask the user for input or clarification. You must proceed with the information you have.
|
- You operate in non-interactive mode: do not ask the user questions; proceed with available context.
|
||||||
* Once you believe all goals have been met and all required outputs have been emitted, stop calling tools.`;
|
- Use tools only when necessary to obtain facts or make changes.
|
||||||
|
- When the task is complete, return the final result as a normal model response (not a tool call) and stop.`;
|
||||||
|
|
||||||
return finalPrompt;
|
return finalPrompt;
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ModelConfig,
|
ModelConfig,
|
||||||
RunConfig,
|
RunConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
} from '../core/subagent.js';
|
} from './subagent.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the storage level for a subagent configuration.
|
* Represents the storage level for a subagent configuration.
|
||||||
|
|||||||
@@ -4,21 +4,15 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { SubagentValidator } from './validation.js';
|
import { SubagentValidator } from './validation.js';
|
||||||
import { SubagentConfig, SubagentError } from './types.js';
|
import { SubagentConfig, SubagentError } from './types.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
|
||||||
|
|
||||||
describe('SubagentValidator', () => {
|
describe('SubagentValidator', () => {
|
||||||
let validator: SubagentValidator;
|
let validator: SubagentValidator;
|
||||||
let mockToolRegistry: ToolRegistry;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockToolRegistry = {
|
validator = new SubagentValidator();
|
||||||
getTool: vi.fn(),
|
|
||||||
} as unknown as ToolRegistry;
|
|
||||||
|
|
||||||
validator = new SubagentValidator(mockToolRegistry);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateName', () => {
|
describe('validateName', () => {
|
||||||
@@ -191,9 +185,6 @@ describe('SubagentValidator', () => {
|
|||||||
|
|
||||||
describe('validateTools', () => {
|
describe('validateTools', () => {
|
||||||
it('should accept valid tool arrays', () => {
|
it('should accept valid tool arrays', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue({} as any);
|
|
||||||
|
|
||||||
const result = validator.validateTools(['read_file', 'write_file']);
|
const result = validator.validateTools(['read_file', 'write_file']);
|
||||||
expect(result.isValid).toBe(true);
|
expect(result.isValid).toBe(true);
|
||||||
expect(result.errors).toHaveLength(0);
|
expect(result.errors).toHaveLength(0);
|
||||||
@@ -215,9 +206,6 @@ describe('SubagentValidator', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should warn about duplicate tools', () => {
|
it('should warn about duplicate tools', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue({} as any);
|
|
||||||
|
|
||||||
const result = validator.validateTools([
|
const result = validator.validateTools([
|
||||||
'read_file',
|
'read_file',
|
||||||
'read_file',
|
'read_file',
|
||||||
@@ -243,16 +231,6 @@ describe('SubagentValidator', () => {
|
|||||||
expect(result.isValid).toBe(false);
|
expect(result.isValid).toBe(false);
|
||||||
expect(result.errors).toContain('Tool name cannot be empty');
|
expect(result.errors).toContain('Tool name cannot be empty');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject unknown tools when registry is available', () => {
|
|
||||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const result = validator.validateTools(['unknown_tool']);
|
|
||||||
expect(result.isValid).toBe(false);
|
|
||||||
expect(result.errors).toContain(
|
|
||||||
'Tool "unknown_tool" not found in tool registry',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateModelConfig', () => {
|
describe('validateModelConfig', () => {
|
||||||
|
|||||||
@@ -10,15 +10,12 @@ import {
|
|||||||
SubagentError,
|
SubagentError,
|
||||||
SubagentErrorCode,
|
SubagentErrorCode,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates subagent configurations to ensure they are well-formed
|
* Validates subagent configurations to ensure they are well-formed
|
||||||
* and compatible with the runtime system.
|
* and compatible with the runtime system.
|
||||||
*/
|
*/
|
||||||
export class SubagentValidator {
|
export class SubagentValidator {
|
||||||
constructor(private readonly toolRegistry?: ToolRegistry) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a complete subagent configuration.
|
* Validates a complete subagent configuration.
|
||||||
*
|
*
|
||||||
@@ -238,14 +235,6 @@ export class SubagentValidator {
|
|||||||
errors.push('Tool name cannot be empty');
|
errors.push('Tool name cannot be empty');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if tool exists in registry (if available)
|
|
||||||
if (this.toolRegistry) {
|
|
||||||
const toolInstance = this.toolRegistry.getTool(tool);
|
|
||||||
if (!toolInstance) {
|
|
||||||
errors.push(`Tool "${tool}" not found in tool registry`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -262,7 +251,7 @@ export class SubagentValidator {
|
|||||||
* @returns ValidationResult
|
* @returns ValidationResult
|
||||||
*/
|
*/
|
||||||
validateModelConfig(
|
validateModelConfig(
|
||||||
modelConfig: Partial<import('../core/subagent.js').ModelConfig>,
|
modelConfig: Partial<import('./subagent.js').ModelConfig>,
|
||||||
): ValidationResult {
|
): ValidationResult {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
@@ -310,7 +299,7 @@ export class SubagentValidator {
|
|||||||
* @returns ValidationResult
|
* @returns ValidationResult
|
||||||
*/
|
*/
|
||||||
validateRunConfig(
|
validateRunConfig(
|
||||||
runConfig: Partial<import('../core/subagent.js').RunConfig>,
|
runConfig: Partial<import('./subagent.js').RunConfig>,
|
||||||
): ValidationResult {
|
): ValidationResult {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const EVENT_INVALID_CHUNK = 'qwen-code.chat.invalid_chunk';
|
|||||||
export const EVENT_CONTENT_RETRY = 'qwen-code.chat.content_retry';
|
export const EVENT_CONTENT_RETRY = 'qwen-code.chat.content_retry';
|
||||||
export const EVENT_CONTENT_RETRY_FAILURE =
|
export const EVENT_CONTENT_RETRY_FAILURE =
|
||||||
'qwen-code.chat.content_retry_failure';
|
'qwen-code.chat.content_retry_failure';
|
||||||
|
export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution';
|
||||||
|
|
||||||
export const METRIC_TOOL_CALL_COUNT = 'qwen-code.tool.call.count';
|
export const METRIC_TOOL_CALL_COUNT = 'qwen-code.tool.call.count';
|
||||||
export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency';
|
export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency';
|
||||||
@@ -33,3 +34,5 @@ export const METRIC_INVALID_CHUNK_COUNT = 'qwen-code.chat.invalid_chunk.count';
|
|||||||
export const METRIC_CONTENT_RETRY_COUNT = 'qwen-code.chat.content_retry.count';
|
export const METRIC_CONTENT_RETRY_COUNT = 'qwen-code.chat.content_retry.count';
|
||||||
export const METRIC_CONTENT_RETRY_FAILURE_COUNT =
|
export const METRIC_CONTENT_RETRY_FAILURE_COUNT =
|
||||||
'qwen-code.chat.content_retry_failure.count';
|
'qwen-code.chat.content_retry_failure.count';
|
||||||
|
export const METRIC_SUBAGENT_EXECUTION_COUNT =
|
||||||
|
'qwen-code.subagent.execution.count';
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
EVENT_INVALID_CHUNK,
|
EVENT_INVALID_CHUNK,
|
||||||
EVENT_CONTENT_RETRY,
|
EVENT_CONTENT_RETRY,
|
||||||
EVENT_CONTENT_RETRY_FAILURE,
|
EVENT_CONTENT_RETRY_FAILURE,
|
||||||
|
EVENT_SUBAGENT_EXECUTION,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
import {
|
import {
|
||||||
ApiErrorEvent,
|
ApiErrorEvent,
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
InvalidChunkEvent,
|
InvalidChunkEvent,
|
||||||
ContentRetryEvent,
|
ContentRetryEvent,
|
||||||
ContentRetryFailureEvent,
|
ContentRetryFailureEvent,
|
||||||
|
SubagentExecutionEvent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
recordApiErrorMetrics,
|
recordApiErrorMetrics,
|
||||||
@@ -51,6 +53,7 @@ import {
|
|||||||
recordInvalidChunk,
|
recordInvalidChunk,
|
||||||
recordContentRetry,
|
recordContentRetry,
|
||||||
recordContentRetryFailure,
|
recordContentRetryFailure,
|
||||||
|
recordSubagentExecutionMetrics,
|
||||||
} from './metrics.js';
|
} from './metrics.js';
|
||||||
import { QwenLogger } from './qwen-logger/qwen-logger.js';
|
import { QwenLogger } from './qwen-logger/qwen-logger.js';
|
||||||
import { isTelemetrySdkInitialized } from './sdk.js';
|
import { isTelemetrySdkInitialized } from './sdk.js';
|
||||||
@@ -504,3 +507,31 @@ export function logContentRetryFailure(
|
|||||||
logger.emit(logRecord);
|
logger.emit(logRecord);
|
||||||
recordContentRetryFailure(config);
|
recordContentRetryFailure(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logSubagentExecution(
|
||||||
|
config: Config,
|
||||||
|
event: SubagentExecutionEvent,
|
||||||
|
): void {
|
||||||
|
QwenLogger.getInstance(config)?.logSubagentExecutionEvent(event);
|
||||||
|
if (!isTelemetrySdkInitialized()) return;
|
||||||
|
|
||||||
|
const attributes: LogAttributes = {
|
||||||
|
...getCommonAttributes(config),
|
||||||
|
...event,
|
||||||
|
'event.name': EVENT_SUBAGENT_EXECUTION,
|
||||||
|
'event.timestamp': new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = logs.getLogger(SERVICE_NAME);
|
||||||
|
const logRecord: LogRecord = {
|
||||||
|
body: `Subagent execution: ${event.subagent_name}.`,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
logger.emit(logRecord);
|
||||||
|
recordSubagentExecutionMetrics(
|
||||||
|
config,
|
||||||
|
event.subagent_name,
|
||||||
|
event.status,
|
||||||
|
event.terminate_reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
METRIC_INVALID_CHUNK_COUNT,
|
METRIC_INVALID_CHUNK_COUNT,
|
||||||
METRIC_CONTENT_RETRY_COUNT,
|
METRIC_CONTENT_RETRY_COUNT,
|
||||||
METRIC_CONTENT_RETRY_FAILURE_COUNT,
|
METRIC_CONTENT_RETRY_FAILURE_COUNT,
|
||||||
|
METRIC_SUBAGENT_EXECUTION_COUNT,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
import { DiffStat } from '../tools/tools.js';
|
import { DiffStat } from '../tools/tools.js';
|
||||||
@@ -46,6 +47,7 @@ let chatCompressionCounter: Counter | undefined;
|
|||||||
let invalidChunkCounter: Counter | undefined;
|
let invalidChunkCounter: Counter | undefined;
|
||||||
let contentRetryCounter: Counter | undefined;
|
let contentRetryCounter: Counter | undefined;
|
||||||
let contentRetryFailureCounter: Counter | undefined;
|
let contentRetryFailureCounter: Counter | undefined;
|
||||||
|
let subagentExecutionCounter: Counter | undefined;
|
||||||
let isMetricsInitialized = false;
|
let isMetricsInitialized = false;
|
||||||
|
|
||||||
function getCommonAttributes(config: Config): Attributes {
|
function getCommonAttributes(config: Config): Attributes {
|
||||||
@@ -117,6 +119,14 @@ export function initializeMetrics(config: Config): void {
|
|||||||
valueType: ValueType.INT,
|
valueType: ValueType.INT,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
subagentExecutionCounter = meter.createCounter(
|
||||||
|
METRIC_SUBAGENT_EXECUTION_COUNT,
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Counts subagent execution events, tagged by status and subagent name.',
|
||||||
|
valueType: ValueType.INT,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const sessionCounter = meter.createCounter(METRIC_SESSION_COUNT, {
|
const sessionCounter = meter.createCounter(METRIC_SESSION_COUNT, {
|
||||||
description: 'Count of CLI sessions started.',
|
description: 'Count of CLI sessions started.',
|
||||||
@@ -277,3 +287,27 @@ export function recordContentRetryFailure(config: Config): void {
|
|||||||
if (!contentRetryFailureCounter || !isMetricsInitialized) return;
|
if (!contentRetryFailureCounter || !isMetricsInitialized) return;
|
||||||
contentRetryFailureCounter.add(1, getCommonAttributes(config));
|
contentRetryFailureCounter.add(1, getCommonAttributes(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a metric for subagent execution events.
|
||||||
|
*/
|
||||||
|
export function recordSubagentExecutionMetrics(
|
||||||
|
config: Config,
|
||||||
|
subagentName: string,
|
||||||
|
status: 'started' | 'progress' | 'completed' | 'failed',
|
||||||
|
terminateReason?: string,
|
||||||
|
): void {
|
||||||
|
if (!subagentExecutionCounter || !isMetricsInitialized) return;
|
||||||
|
|
||||||
|
const attributes: Attributes = {
|
||||||
|
...getCommonAttributes(config),
|
||||||
|
subagent_name: subagentName,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (terminateReason) {
|
||||||
|
attributes['terminate_reason'] = terminateReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
subagentExecutionCounter.add(1, attributes);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
InvalidChunkEvent,
|
InvalidChunkEvent,
|
||||||
ContentRetryEvent,
|
ContentRetryEvent,
|
||||||
ContentRetryFailureEvent,
|
ContentRetryFailureEvent,
|
||||||
|
SubagentExecutionEvent,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import {
|
import {
|
||||||
RumEvent,
|
RumEvent,
|
||||||
@@ -628,6 +629,20 @@ export class QwenLogger {
|
|||||||
this.flushIfNeeded();
|
this.flushIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logSubagentExecutionEvent(event: SubagentExecutionEvent): void {
|
||||||
|
const rumEvent = this.createActionEvent('subagent', 'subagent_execution', {
|
||||||
|
snapshots: JSON.stringify({
|
||||||
|
subagent_name: event.subagent_name,
|
||||||
|
status: event.status,
|
||||||
|
terminate_reason: event.terminate_reason,
|
||||||
|
execution_summary: event.execution_summary,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enqueueLogEvent(rumEvent);
|
||||||
|
this.flushIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
logEndSessionEvent(_event: EndSessionEvent): void {
|
logEndSessionEvent(_event: EndSessionEvent): void {
|
||||||
const applicationEvent = this.createViewEvent('session', 'session_end', {});
|
const applicationEvent = this.createViewEvent('session', 'session_end', {});
|
||||||
|
|
||||||
|
|||||||
@@ -444,6 +444,34 @@ export class ContentRetryFailureEvent implements BaseTelemetryEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SubagentExecutionEvent implements BaseTelemetryEvent {
|
||||||
|
'event.name': 'subagent_execution';
|
||||||
|
'event.timestamp': string;
|
||||||
|
subagent_name: string;
|
||||||
|
status: 'started' | 'progress' | 'completed' | 'failed';
|
||||||
|
terminate_reason?: string;
|
||||||
|
result?: string;
|
||||||
|
execution_summary?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
subagent_name: string,
|
||||||
|
status: 'started' | 'progress' | 'completed' | 'failed',
|
||||||
|
options?: {
|
||||||
|
terminate_reason?: string;
|
||||||
|
result?: string;
|
||||||
|
execution_summary?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this['event.name'] = 'subagent_execution';
|
||||||
|
this['event.timestamp'] = new Date().toISOString();
|
||||||
|
this.subagent_name = subagent_name;
|
||||||
|
this.status = status;
|
||||||
|
this.terminate_reason = options?.terminate_reason;
|
||||||
|
this.result = options?.result;
|
||||||
|
this.execution_summary = options?.execution_summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type TelemetryEvent =
|
export type TelemetryEvent =
|
||||||
| StartSessionEvent
|
| StartSessionEvent
|
||||||
| EndSessionEvent
|
| EndSessionEvent
|
||||||
@@ -461,4 +489,5 @@ export type TelemetryEvent =
|
|||||||
| SlashCommandEvent
|
| SlashCommandEvent
|
||||||
| InvalidChunkEvent
|
| InvalidChunkEvent
|
||||||
| ContentRetryEvent
|
| ContentRetryEvent
|
||||||
| ContentRetryFailureEvent;
|
| ContentRetryFailureEvent
|
||||||
|
| SubagentExecutionEvent;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
BaseToolInvocation,
|
BaseToolInvocation,
|
||||||
ToolInvocation,
|
ToolInvocation,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
|
ToolResultDisplay,
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolExecuteConfirmationDetails,
|
ToolExecuteConfirmationDetails,
|
||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
@@ -100,7 +101,7 @@ class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
updateOutput?: (output: string) => void,
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
terminalColumns?: number,
|
terminalColumns?: number,
|
||||||
terminalRows?: number,
|
terminalRows?: number,
|
||||||
): Promise<ToolResult> {
|
): Promise<ToolResult> {
|
||||||
|
|||||||
507
packages/core/src/tools/task.test.ts
Normal file
507
packages/core/src/tools/task.test.ts
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { TaskTool, TaskParams } from './task.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 { partToString } from '../utils/partUtils.js';
|
||||||
|
|
||||||
|
// Type for accessing protected methods in tests
|
||||||
|
type TaskToolWithProtectedMethods = TaskTool & {
|
||||||
|
createInvocation: (params: TaskParams) => {
|
||||||
|
execute: (
|
||||||
|
signal?: AbortSignal,
|
||||||
|
liveOutputCallback?: (chunk: string) => void,
|
||||||
|
) => Promise<{
|
||||||
|
llmContent: string;
|
||||||
|
returnDisplay: unknown;
|
||||||
|
}>;
|
||||||
|
getDescription: () => string;
|
||||||
|
shouldConfirmExecute: () => Promise<boolean>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../subagents/subagent-manager.js');
|
||||||
|
vi.mock('../subagents/subagent.js');
|
||||||
|
|
||||||
|
const MockedSubagentManager = vi.mocked(SubagentManager);
|
||||||
|
const MockedContextState = vi.mocked(ContextState);
|
||||||
|
|
||||||
|
describe('TaskTool', () => {
|
||||||
|
let config: Config;
|
||||||
|
let taskTool: TaskTool;
|
||||||
|
let mockSubagentManager: SubagentManager;
|
||||||
|
|
||||||
|
const mockSubagents: SubagentConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'file-search',
|
||||||
|
description: 'Specialized agent for searching and analyzing files',
|
||||||
|
systemPrompt: 'You are a file search specialist.',
|
||||||
|
level: 'project',
|
||||||
|
filePath: '/project/.qwen/agents/file-search.md',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'code-review',
|
||||||
|
description: 'Agent for reviewing code quality and best practices',
|
||||||
|
systemPrompt: 'You are a code review specialist.',
|
||||||
|
level: 'user',
|
||||||
|
filePath: '/home/user/.qwen/agents/code-review.md',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup fake timers
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
// Create mock config
|
||||||
|
config = {
|
||||||
|
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
|
||||||
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
getSubagentManager: vi.fn(),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
// Setup SubagentManager mock
|
||||||
|
mockSubagentManager = {
|
||||||
|
listSubagents: vi.fn().mockResolvedValue(mockSubagents),
|
||||||
|
loadSubagent: vi.fn(),
|
||||||
|
createSubagentScope: vi.fn(),
|
||||||
|
} as unknown as SubagentManager;
|
||||||
|
|
||||||
|
MockedSubagentManager.mockImplementation(() => mockSubagentManager);
|
||||||
|
|
||||||
|
// Make config return the mock SubagentManager
|
||||||
|
vi.mocked(config.getSubagentManager).mockReturnValue(mockSubagentManager);
|
||||||
|
|
||||||
|
// Create TaskTool instance
|
||||||
|
taskTool = new TaskTool(config);
|
||||||
|
|
||||||
|
// Allow async initialization to complete
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('should initialize with correct name and properties', () => {
|
||||||
|
expect(taskTool.name).toBe('task');
|
||||||
|
expect(taskTool.displayName).toBe('Task');
|
||||||
|
expect(taskTool.kind).toBe('execute');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load available subagents during initialization', () => {
|
||||||
|
expect(mockSubagentManager.listSubagents).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update description with available subagents', () => {
|
||||||
|
expect(taskTool.description).toContain('file-search');
|
||||||
|
expect(taskTool.description).toContain(
|
||||||
|
'Specialized agent for searching and analyzing files',
|
||||||
|
);
|
||||||
|
expect(taskTool.description).toContain('code-review');
|
||||||
|
expect(taskTool.description).toContain(
|
||||||
|
'Agent for reviewing code quality and best practices',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty subagents list gracefully', async () => {
|
||||||
|
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const emptyTaskTool = new TaskTool(config);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
|
expect(emptyTaskTool.description).toContain(
|
||||||
|
'No subagents are currently configured',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle subagent loading errors gracefully', async () => {
|
||||||
|
vi.mocked(mockSubagentManager.listSubagents).mockRejectedValue(
|
||||||
|
new Error('Loading failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
new TaskTool(config);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Failed to load subagents for Task tool:',
|
||||||
|
expect.any(Error),
|
||||||
|
);
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('schema generation', () => {
|
||||||
|
it('should generate schema with subagent names as enum', () => {
|
||||||
|
const schema = taskTool.schema;
|
||||||
|
const properties = schema.parametersJsonSchema as {
|
||||||
|
properties: {
|
||||||
|
subagent_type: {
|
||||||
|
enum?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expect(properties.properties.subagent_type.enum).toEqual([
|
||||||
|
'file-search',
|
||||||
|
'code-review',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate schema without enum when no subagents available', async () => {
|
||||||
|
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const emptyTaskTool = new TaskTool(config);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
|
const schema = emptyTaskTool.schema;
|
||||||
|
const properties = schema.parametersJsonSchema as {
|
||||||
|
properties: {
|
||||||
|
subagent_type: {
|
||||||
|
enum?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expect(properties.properties.subagent_type.enum).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateToolParams', () => {
|
||||||
|
const validParams: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files in the project',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should validate valid parameters', async () => {
|
||||||
|
const result = taskTool.validateToolParams(validParams);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject empty description', async () => {
|
||||||
|
const result = taskTool.validateToolParams({
|
||||||
|
...validParams,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
expect(result).toBe(
|
||||||
|
'Parameter "description" must be a non-empty string.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject empty prompt', async () => {
|
||||||
|
const result = taskTool.validateToolParams({
|
||||||
|
...validParams,
|
||||||
|
prompt: '',
|
||||||
|
});
|
||||||
|
expect(result).toBe('Parameter "prompt" must be a non-empty string.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject empty subagent_type', async () => {
|
||||||
|
const result = taskTool.validateToolParams({
|
||||||
|
...validParams,
|
||||||
|
subagent_type: '',
|
||||||
|
});
|
||||||
|
expect(result).toBe(
|
||||||
|
'Parameter "subagent_type" must be a non-empty string.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject non-existent subagent', async () => {
|
||||||
|
const result = taskTool.validateToolParams({
|
||||||
|
...validParams,
|
||||||
|
subagent_type: 'non-existent',
|
||||||
|
});
|
||||||
|
expect(result).toBe(
|
||||||
|
'Subagent "non-existent" not found. Available subagents: file-search, code-review',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('refreshSubagents', () => {
|
||||||
|
it('should refresh available subagents and update description', async () => {
|
||||||
|
const newSubagents: SubagentConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'test-agent',
|
||||||
|
description: 'A test agent',
|
||||||
|
systemPrompt: 'Test prompt',
|
||||||
|
level: 'project',
|
||||||
|
filePath: '/project/.qwen/agents/test-agent.md',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue(
|
||||||
|
newSubagents,
|
||||||
|
);
|
||||||
|
|
||||||
|
await taskTool.refreshSubagents();
|
||||||
|
|
||||||
|
expect(taskTool.description).toContain('test-agent');
|
||||||
|
expect(taskTool.description).toContain('A test agent');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TaskToolInvocation', () => {
|
||||||
|
let mockSubagentScope: SubAgentScope;
|
||||||
|
let mockContextState: ContextState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSubagentScope = {
|
||||||
|
runNonInteractive: vi.fn().mockResolvedValue(undefined),
|
||||||
|
output: {
|
||||||
|
result: 'Task completed successfully',
|
||||||
|
terminate_reason: SubagentTerminateMode.GOAL,
|
||||||
|
},
|
||||||
|
getFinalText: vi.fn().mockReturnValue('Task completed successfully'),
|
||||||
|
formatCompactResult: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(
|
||||||
|
'✅ Success: Search files completed with GOAL termination',
|
||||||
|
),
|
||||||
|
getStatistics: vi.fn().mockReturnValue({
|
||||||
|
rounds: 2,
|
||||||
|
totalDurationMs: 1500,
|
||||||
|
totalToolCalls: 3,
|
||||||
|
successfulToolCalls: 3,
|
||||||
|
failedToolCalls: 0,
|
||||||
|
}),
|
||||||
|
} as unknown as SubAgentScope;
|
||||||
|
|
||||||
|
mockContextState = {
|
||||||
|
set: vi.fn(),
|
||||||
|
} as unknown as ContextState;
|
||||||
|
|
||||||
|
MockedContextState.mockImplementation(() => mockContextState);
|
||||||
|
|
||||||
|
vi.mocked(mockSubagentManager.loadSubagent).mockResolvedValue(
|
||||||
|
mockSubagents[0],
|
||||||
|
);
|
||||||
|
vi.mocked(mockSubagentManager.createSubagentScope).mockResolvedValue(
|
||||||
|
mockSubagentScope,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute subagent successfully', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
const result = await invocation.execute();
|
||||||
|
|
||||||
|
expect(mockSubagentManager.loadSubagent).toHaveBeenCalledWith(
|
||||||
|
'file-search',
|
||||||
|
);
|
||||||
|
expect(mockSubagentManager.createSubagentScope).toHaveBeenCalledWith(
|
||||||
|
mockSubagents[0],
|
||||||
|
config,
|
||||||
|
expect.any(Object), // eventEmitter parameter
|
||||||
|
);
|
||||||
|
expect(mockSubagentScope.runNonInteractive).toHaveBeenCalledWith(
|
||||||
|
mockContextState,
|
||||||
|
undefined, // signal parameter (undefined when not provided)
|
||||||
|
);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle subagent not found error', async () => {
|
||||||
|
vi.mocked(mockSubagentManager.loadSubagent).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'non-existent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).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('Subagent "non-existent" not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle subagent execution failure', async () => {
|
||||||
|
mockSubagentScope.output.terminate_reason = SubagentTerminateMode.ERROR;
|
||||||
|
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle execution errors gracefully', async () => {
|
||||||
|
vi.mocked(mockSubagentManager.createSubagentScope).mockRejectedValue(
|
||||||
|
new Error('Creation failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).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('Failed to start subagent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute subagent without live output callback', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
const result = await invocation.execute();
|
||||||
|
|
||||||
|
// Verify that the task completed successfully
|
||||||
|
expect(result.llmContent).toBeDefined();
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set context variables correctly', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
await invocation.execute();
|
||||||
|
|
||||||
|
expect(mockContextState.set).toHaveBeenCalledWith(
|
||||||
|
'task_prompt',
|
||||||
|
'Find all TypeScript files',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return structured display object', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
const result = await invocation.execute();
|
||||||
|
|
||||||
|
expect(typeof result.returnDisplay).toBe('object');
|
||||||
|
expect(result.returnDisplay).toHaveProperty('type', 'subagent_execution');
|
||||||
|
expect(result.returnDisplay).toHaveProperty(
|
||||||
|
'subagentName',
|
||||||
|
'file-search',
|
||||||
|
);
|
||||||
|
expect(result.returnDisplay).toHaveProperty(
|
||||||
|
'taskDescription',
|
||||||
|
'Search files',
|
||||||
|
);
|
||||||
|
expect(result.returnDisplay).toHaveProperty('status', 'completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not require confirmation', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
const shouldConfirm = await invocation.shouldConfirmExecute();
|
||||||
|
|
||||||
|
expect(shouldConfirm).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide correct description', async () => {
|
||||||
|
const params: TaskParams = {
|
||||||
|
description: 'Search files',
|
||||||
|
prompt: 'Find all TypeScript files',
|
||||||
|
subagent_type: 'file-search',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invocation = (
|
||||||
|
taskTool as TaskToolWithProtectedMethods
|
||||||
|
).createInvocation(params);
|
||||||
|
const description = invocation.getDescription();
|
||||||
|
|
||||||
|
expect(description).toBe(
|
||||||
|
'file-search subagent: "Search files"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
498
packages/core/src/tools/task.ts
Normal file
498
packages/core/src/tools/task.ts
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
BaseDeclarativeTool,
|
||||||
|
BaseToolInvocation,
|
||||||
|
Kind,
|
||||||
|
ToolResult,
|
||||||
|
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 { ContextState } from '../subagents/subagent.js';
|
||||||
|
import {
|
||||||
|
SubAgentEventEmitter,
|
||||||
|
SubAgentToolCallEvent,
|
||||||
|
SubAgentToolResultEvent,
|
||||||
|
SubAgentFinishEvent,
|
||||||
|
} from '../subagents/subagent-events.js';
|
||||||
|
import { ChatRecordingService } from '../services/chatRecordingService.js';
|
||||||
|
|
||||||
|
export interface TaskParams {
|
||||||
|
description: string;
|
||||||
|
prompt: string;
|
||||||
|
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
|
||||||
|
* for the model to choose from.
|
||||||
|
*/
|
||||||
|
export class TaskTool extends BaseDeclarativeTool<TaskParams, ToolResult> {
|
||||||
|
static readonly Name: string = 'task';
|
||||||
|
|
||||||
|
private subagentManager: SubagentManager;
|
||||||
|
private availableSubagents: SubagentConfig[] = [];
|
||||||
|
|
||||||
|
constructor(private readonly config: Config) {
|
||||||
|
// Initialize with a basic schema first
|
||||||
|
const initialSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'A short (3-5 word) description of the task',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The task for the agent to perform',
|
||||||
|
},
|
||||||
|
subagent_type: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The type of specialized agent to use for this task',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['description', 'prompt', 'subagent_type'],
|
||||||
|
additionalProperties: false,
|
||||||
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
};
|
||||||
|
|
||||||
|
super(
|
||||||
|
TaskTool.Name,
|
||||||
|
'Task',
|
||||||
|
'Delegate tasks to specialized subagents. Loading available subagents...', // Initial description
|
||||||
|
Kind.Execute,
|
||||||
|
initialSchema,
|
||||||
|
true, // isOutputMarkdown
|
||||||
|
true, // canUpdateOutput - Enable live output updates for real-time progress
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subagentManager = config.getSubagentManager();
|
||||||
|
|
||||||
|
// Initialize the tool asynchronously
|
||||||
|
this.initializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously initializes the tool by loading available subagents
|
||||||
|
* and updating the description and schema.
|
||||||
|
*/
|
||||||
|
private async initializeAsync(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.availableSubagents = await this.subagentManager.listSubagents();
|
||||||
|
this.updateDescriptionAndSchema();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load subagents for Task tool:', error);
|
||||||
|
this.availableSubagents = [];
|
||||||
|
this.updateDescriptionAndSchema();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 =
|
||||||
|
'No subagents are currently configured. You can create subagents using the /agents command.';
|
||||||
|
} else {
|
||||||
|
subagentDescriptions = this.availableSubagents
|
||||||
|
.map((subagent) => `- **${subagent.name}**: ${subagent.description}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update description using object property assignment since it's readonly
|
||||||
|
(this as { description: string }).description =
|
||||||
|
baseDescription + subagentDescriptions;
|
||||||
|
|
||||||
|
// Generate dynamic schema with enum of available subagent names
|
||||||
|
const subagentNames = this.availableSubagents.map((s) => s.name);
|
||||||
|
|
||||||
|
// Update the parameter schema by modifying the existing object
|
||||||
|
const schema = this.parameterSchema as {
|
||||||
|
properties?: {
|
||||||
|
subagent_type?: {
|
||||||
|
enum?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (schema.properties && schema.properties.subagent_type) {
|
||||||
|
if (subagentNames.length > 0) {
|
||||||
|
schema.properties.subagent_type.enum = subagentNames;
|
||||||
|
} else {
|
||||||
|
delete schema.properties.subagent_type.enum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the available subagents and updates the tool description.
|
||||||
|
* This can be called when subagents are added or removed.
|
||||||
|
*/
|
||||||
|
async refreshSubagents(): Promise<void> {
|
||||||
|
await this.initializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
override validateToolParams(params: TaskParams): string | null {
|
||||||
|
// Validate required fields
|
||||||
|
if (
|
||||||
|
!params.description ||
|
||||||
|
typeof params.description !== 'string' ||
|
||||||
|
params.description.trim() === ''
|
||||||
|
) {
|
||||||
|
return 'Parameter "description" must be a non-empty string.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!params.prompt ||
|
||||||
|
typeof params.prompt !== 'string' ||
|
||||||
|
params.prompt.trim() === ''
|
||||||
|
) {
|
||||||
|
return 'Parameter "prompt" must be a non-empty string.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!params.subagent_type ||
|
||||||
|
typeof params.subagent_type !== 'string' ||
|
||||||
|
params.subagent_type.trim() === ''
|
||||||
|
) {
|
||||||
|
return 'Parameter "subagent_type" must be a non-empty string.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the subagent exists
|
||||||
|
const subagentExists = this.availableSubagents.some(
|
||||||
|
(subagent) => subagent.name === params.subagent_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subagentExists) {
|
||||||
|
const availableNames = this.availableSubagents.map((s) => s.name);
|
||||||
|
return `Subagent "${params.subagent_type}" not found. Available subagents: ${availableNames.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createInvocation(params: TaskParams) {
|
||||||
|
return new TaskToolInvocation(this.config, this.subagentManager, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly config: Config,
|
||||||
|
private readonly subagentManager: SubagentManager,
|
||||||
|
params: TaskParams,
|
||||||
|
) {
|
||||||
|
super(params);
|
||||||
|
this._eventEmitter = new SubAgentEventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventEmitter(): SubAgentEventEmitter {
|
||||||
|
return this._eventEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current display state and calls updateOutput if provided
|
||||||
|
*/
|
||||||
|
private updateDisplay(
|
||||||
|
updates: Partial<TaskResultDisplay>,
|
||||||
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
|
): void {
|
||||||
|
if (!this.currentDisplay) return;
|
||||||
|
|
||||||
|
this.currentDisplay = {
|
||||||
|
...this.currentDisplay,
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updateOutput) {
|
||||||
|
updateOutput(this.currentDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up event listeners for real-time subagent progress updates
|
||||||
|
*/
|
||||||
|
private setupEventListeners(
|
||||||
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
|
): void {
|
||||||
|
this.eventEmitter.on('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[]) => {
|
||||||
|
const event = args[0] as SubAgentToolCallEvent;
|
||||||
|
const newToolCall = {
|
||||||
|
name: event.name,
|
||||||
|
status: 'executing' as const,
|
||||||
|
args: event.args,
|
||||||
|
};
|
||||||
|
this.currentToolCalls.push(newToolCall);
|
||||||
|
|
||||||
|
this.updateDisplay(
|
||||||
|
{
|
||||||
|
progress: {
|
||||||
|
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('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
|
||||||
|
},
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.on('error', () => {
|
||||||
|
this.updateDisplay({ status: 'failed' }, updateOutput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return `${this.params.subagent_type} subagent: "${this.params.description}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async shouldConfirmExecute(): Promise<false> {
|
||||||
|
// Task delegation should execute automatically without user confirmation
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
signal?: AbortSignal,
|
||||||
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
|
): Promise<ToolResult> {
|
||||||
|
try {
|
||||||
|
// Load the subagent configuration
|
||||||
|
const subagentConfig = await this.subagentManager.loadSubagent(
|
||||||
|
this.params.subagent_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!subagentConfig) {
|
||||||
|
const errorDisplay = {
|
||||||
|
type: 'subagent_execution' as const,
|
||||||
|
subagentName: this.params.subagent_type,
|
||||||
|
taskDescription: this.params.description,
|
||||||
|
status: 'failed' as const,
|
||||||
|
terminateReason: 'ERROR',
|
||||||
|
result: `Subagent "${this.params.subagent_type}" not found`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
llmContent: [
|
||||||
|
{
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `Subagent "${this.params.subagent_type}" not found`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returnDisplay: errorDisplay,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the current display state
|
||||||
|
this.currentDisplay = {
|
||||||
|
type: 'subagent_execution' as const,
|
||||||
|
subagentName: subagentConfig.name,
|
||||||
|
taskDescription: this.params.description,
|
||||||
|
status: 'running' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up event listeners for real-time updates
|
||||||
|
this.setupEventListeners(updateOutput);
|
||||||
|
|
||||||
|
// Send initial display
|
||||||
|
if (updateOutput) {
|
||||||
|
updateOutput(this.currentDisplay);
|
||||||
|
}
|
||||||
|
const chatRecorder = new ChatRecordingService(this.config);
|
||||||
|
try {
|
||||||
|
chatRecorder.initialize();
|
||||||
|
} catch {
|
||||||
|
// Initialization failed, continue without recording
|
||||||
|
}
|
||||||
|
const subagentScope = await this.subagentManager.createSubagentScope(
|
||||||
|
subagentConfig,
|
||||||
|
this.config,
|
||||||
|
{ eventEmitter: this.eventEmitter },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up basic event listeners for chat recording
|
||||||
|
this.eventEmitter.on('start', () => {
|
||||||
|
chatRecorder.recordMessage({
|
||||||
|
type: 'user',
|
||||||
|
content: `Subagent(${this.params.subagent_type}) Task: ${this.params.description}\n\n${this.params.prompt}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.on('finish', (e) => {
|
||||||
|
const finishEvent = e as {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
};
|
||||||
|
const text = subagentScope.getFinalText() || '';
|
||||||
|
chatRecorder.recordMessage({ type: 'gemini', content: text });
|
||||||
|
const input = finishEvent.inputTokens ?? 0;
|
||||||
|
const output = finishEvent.outputTokens ?? 0;
|
||||||
|
chatRecorder.recordMessageTokens({
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
cached: 0,
|
||||||
|
total: input + output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create context state with the task prompt
|
||||||
|
const contextState = new ContextState();
|
||||||
|
contextState.set('task_prompt', this.params.prompt);
|
||||||
|
|
||||||
|
// Execute the subagent (blocking)
|
||||||
|
await subagentScope.runNonInteractive(contextState, signal);
|
||||||
|
|
||||||
|
// Get the results
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the final display state
|
||||||
|
this.updateDisplay(
|
||||||
|
{
|
||||||
|
status: success ? 'completed' : 'failed',
|
||||||
|
terminateReason,
|
||||||
|
result: finalText,
|
||||||
|
executionSummary,
|
||||||
|
// Keep progress data including tool calls for final display
|
||||||
|
},
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
llmContent: [{ text: JSON.stringify(result) }],
|
||||||
|
returnDisplay: this.currentDisplay!,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[TaskTool] Error starting subagent: ${errorMessage}`);
|
||||||
|
|
||||||
|
const errorDisplay = {
|
||||||
|
type: 'subagent_execution' as const,
|
||||||
|
subagentName: this.params.subagent_type,
|
||||||
|
taskDescription: this.params.description,
|
||||||
|
status: 'failed' as const,
|
||||||
|
terminateReason: 'ERROR',
|
||||||
|
result: `Failed to start subagent: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
llmContent: [
|
||||||
|
{
|
||||||
|
text: JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to start subagent: ${errorMessage}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
returnDisplay: errorDisplay,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
AnyDeclarativeTool,
|
AnyDeclarativeTool,
|
||||||
Kind,
|
Kind,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
|
ToolResultDisplay,
|
||||||
BaseDeclarativeTool,
|
BaseDeclarativeTool,
|
||||||
BaseToolInvocation,
|
BaseToolInvocation,
|
||||||
ToolInvocation,
|
ToolInvocation,
|
||||||
@@ -41,7 +42,7 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
_signal: AbortSignal,
|
_signal: AbortSignal,
|
||||||
_updateOutput?: (output: string) => void,
|
_updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
): Promise<ToolResult> {
|
): Promise<ToolResult> {
|
||||||
const callCommand = this.config.getToolCallCommand()!;
|
const callCommand = this.config.getToolCallCommand()!;
|
||||||
const child = spawn(callCommand, [this.toolName]);
|
const child = spawn(callCommand, [this.toolName]);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export interface ToolInvocation<
|
|||||||
*/
|
*/
|
||||||
execute(
|
execute(
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
updateOutput?: (output: string) => void,
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
): Promise<TResult>;
|
): Promise<TResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export abstract class BaseToolInvocation<
|
|||||||
|
|
||||||
abstract execute(
|
abstract execute(
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
updateOutput?: (output: string) => void,
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
): Promise<TResult>;
|
): Promise<TResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ export abstract class DeclarativeTool<
|
|||||||
async buildAndExecute(
|
async buildAndExecute(
|
||||||
params: TParams,
|
params: TParams,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
updateOutput?: (output: string) => void,
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
): Promise<TResult> {
|
): Promise<TResult> {
|
||||||
const invocation = this.build(params);
|
const invocation = this.build(params);
|
||||||
return invocation.execute(signal, updateOutput);
|
return invocation.execute(signal, updateOutput);
|
||||||
@@ -421,7 +421,31 @@ export function hasCycleInSchema(schema: object): boolean {
|
|||||||
return traverse(schema, new Set<string>(), new Set<string>());
|
return traverse(schema, new Set<string>(), new Set<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolResultDisplay = string | FileDiff | TodoResultDisplay;
|
export interface TaskResultDisplay {
|
||||||
|
type: 'subagent_execution';
|
||||||
|
subagentName: string;
|
||||||
|
taskDescription: 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;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToolResultDisplay =
|
||||||
|
| string
|
||||||
|
| FileDiff
|
||||||
|
| TodoResultDisplay
|
||||||
|
| TaskResultDisplay;
|
||||||
|
|
||||||
export interface FileDiff {
|
export interface FileDiff {
|
||||||
fileDiff: string;
|
fileDiff: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user