mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent feature wip
This commit is contained in:
@@ -44,7 +44,7 @@ import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
|||||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||||
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
||||||
import {
|
import {
|
||||||
SubagentCreationWizard,
|
AgentCreationWizard,
|
||||||
AgentsManagerDialog,
|
AgentsManagerDialog,
|
||||||
} from './components/subagents/index.js';
|
} from './components/subagents/index.js';
|
||||||
import { Colors } from './colors.js';
|
import { Colors } from './colors.js';
|
||||||
@@ -1093,7 +1093,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
) : isSubagentCreateDialogOpen ? (
|
) : isSubagentCreateDialogOpen ? (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<SubagentCreationWizard
|
<AgentCreationWizard
|
||||||
onClose={closeSubagentCreateDialog}
|
onClose={closeSubagentCreateDialog}
|
||||||
config={config}
|
config={config}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock('../subagents/index.js', () => ({
|
vi.mock('../subagents/index.js', () => ({
|
||||||
SubagentExecutionDisplay: function MockSubagentExecutionDisplay({
|
AgentExecutionDisplay: function MockAgentExecutionDisplay({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
data: { subagentName: string; taskDescription: string };
|
data: { subagentName: string; taskDescription: string };
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
TodoResultDisplay,
|
TodoResultDisplay,
|
||||||
TaskResultDisplay,
|
TaskResultDisplay,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { SubagentExecutionDisplay } from '../subagents/index.js';
|
import { AgentExecutionDisplay } 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.
|
||||||
@@ -106,7 +106,7 @@ const SubagentExecutionRenderer: React.FC<{
|
|||||||
data: TaskResultDisplay;
|
data: TaskResultDisplay;
|
||||||
availableHeight?: number;
|
availableHeight?: number;
|
||||||
childWidth: number;
|
childWidth: number;
|
||||||
}> = ({ data }) => <SubagentExecutionDisplay data={data} />;
|
}> = ({ data }) => <AgentExecutionDisplay data={data} />;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render string results (markdown or plain text)
|
* Component to render string results (markdown or plain text)
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
|
|
||||||
import { useReducer, useCallback, useMemo } from 'react';
|
import { useReducer, useCallback, useMemo } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { wizardReducer, initialWizardState } from './reducers.js';
|
import { wizardReducer, initialWizardState } from '../reducers.js';
|
||||||
import { LocationSelector } from './LocationSelector.js';
|
import { LocationSelector } from './LocationSelector.js';
|
||||||
import { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
import { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
||||||
import { DescriptionInput } from './DescriptionInput.js';
|
import { DescriptionInput } from './DescriptionInput.js';
|
||||||
import { ToolSelector } from './ToolSelector.js';
|
import { ToolSelector } from './ToolSelector.js';
|
||||||
import { ColorSelector } from './ColorSelector.js';
|
import { ColorSelector } from './ColorSelector.js';
|
||||||
import { CreationSummary } from './CreationSummary.js';
|
import { CreationSummary } from './CreationSummary.js';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from '../types.js';
|
||||||
import { WIZARD_STEPS } from './constants.js';
|
import { WIZARD_STEPS } from '../constants.js';
|
||||||
import { Config } from '@qwen-code/qwen-code-core';
|
import { Config } from '@qwen-code/qwen-code-core';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
|
|
||||||
interface SubagentCreationWizardProps {
|
interface AgentCreationWizardProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
config: Config | null;
|
config: Config | null;
|
||||||
}
|
}
|
||||||
@@ -27,10 +27,10 @@ interface SubagentCreationWizardProps {
|
|||||||
/**
|
/**
|
||||||
* Main orchestrator component for the subagent creation wizard.
|
* Main orchestrator component for the subagent creation wizard.
|
||||||
*/
|
*/
|
||||||
export function SubagentCreationWizard({
|
export function AgentCreationWizard({
|
||||||
onClose,
|
onClose,
|
||||||
config,
|
config,
|
||||||
}: SubagentCreationWizardProps) {
|
}: AgentCreationWizardProps) {
|
||||||
const [state, dispatch] = useReducer(wizardReducer, initialWizardState);
|
const [state, dispatch] = useReducer(wizardReducer, initialWizardState);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { ColorOption } from './types.js';
|
import { ColorOption } from '../types.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
import { COLOR_OPTIONS } from './constants.js';
|
import { COLOR_OPTIONS } from '../constants.js';
|
||||||
|
|
||||||
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from '../types.js';
|
||||||
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentManager, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
import { shouldShowColor, getColorForDisplay } from '../utils.js';
|
||||||
import { useLaunchEditor } from './useLaunchEditor.js';
|
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 6: Final confirmation and actions.
|
* Step 6: Final confirmation and actions.
|
||||||
@@ -6,17 +6,17 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { WizardStepProps, WizardAction } from './types.js';
|
import { WizardStepProps, WizardAction } from '../types.js';
|
||||||
import { sanitizeInput } from './utils.js';
|
import { sanitizeInput } from '../utils.js';
|
||||||
import { Config, subagentGenerator } from '@qwen-code/qwen-code-core';
|
import { Config, subagentGenerator } from '@qwen-code/qwen-code-core';
|
||||||
import { useTextBuffer } from '../shared/text-buffer.js';
|
import { useTextBuffer } from '../../shared/text-buffer.js';
|
||||||
import { useKeypress, Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, Key } from '../../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { keyMatchers, Command } from '../../../keyMatchers.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { cpSlice, cpLen } from '../../utils/textUtils.js';
|
import { cpSlice, cpLen } from '../../../utils/textUtils.js';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import stringWidth from 'string-width';
|
import stringWidth from 'string-width';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 3: Description input with LLM generation.
|
* Step 3: Description input with LLM generation.
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from '../types.js';
|
||||||
|
|
||||||
interface GenerationOption {
|
interface GenerationOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { WizardStepProps } from './types.js';
|
import { WizardStepProps } from '../types.js';
|
||||||
|
|
||||||
interface LocationOption {
|
interface LocationOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { ToolCategory } from './types.js';
|
import { ToolCategory } from '../types.js';
|
||||||
import { Kind, Config } from '@qwen-code/qwen-code-core';
|
import { Kind, Config } from '@qwen-code/qwen-code-core';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
|
|
||||||
interface ToolOption {
|
interface ToolOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -4,33 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Creation Wizard Components
|
// Creation Wizard
|
||||||
export { SubagentCreationWizard } from './SubagentCreationWizard.js';
|
export { AgentCreationWizard } from './create/AgentCreationWizard.js';
|
||||||
export { LocationSelector } from './LocationSelector.js';
|
|
||||||
export { GenerationMethodSelector } from './GenerationMethodSelector.js';
|
|
||||||
export { DescriptionInput } from './DescriptionInput.js';
|
|
||||||
export { ToolSelector } from './ToolSelector.js';
|
|
||||||
export { ColorSelector } from './ColorSelector.js';
|
|
||||||
export { CreationSummary } from './CreationSummary.js';
|
|
||||||
|
|
||||||
// Management Dialog Components
|
// Management Dialog
|
||||||
export { AgentsManagerDialog } from './AgentsManagerDialog.js';
|
export { AgentsManagerDialog } from './view/AgentsManagerDialog.js';
|
||||||
export { AgentSelectionStep } from './AgentSelectionStep.js';
|
|
||||||
export { ActionSelectionStep } from './ActionSelectionStep.js';
|
|
||||||
export { AgentViewerStep } from './AgentViewerStep.js';
|
|
||||||
export { AgentDeleteStep } from './AgentDeleteStep.js';
|
|
||||||
|
|
||||||
// Execution Display Components
|
// Execution Display
|
||||||
export { SubagentExecutionDisplay } from './SubagentExecutionDisplay.js';
|
export { AgentExecutionDisplay } from './runtime/AgentExecutionDisplay.js';
|
||||||
|
|
||||||
// Creation Wizard Types and State
|
|
||||||
export type {
|
|
||||||
CreationWizardState,
|
|
||||||
WizardAction,
|
|
||||||
WizardStepProps,
|
|
||||||
WizardResult,
|
|
||||||
ToolCategory,
|
|
||||||
ColorOption,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
export { wizardReducer, initialWizardState } from './reducers.js';
|
|
||||||
|
|||||||
@@ -6,19 +6,19 @@
|
|||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
import {
|
import {
|
||||||
TaskResultDisplay,
|
TaskResultDisplay,
|
||||||
SubagentStatsSummary,
|
SubagentStatsSummary,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||||
import { COLOR_OPTIONS } from './constants.js';
|
import { COLOR_OPTIONS } from '../constants.js';
|
||||||
import { fmtDuration } from './utils.js';
|
import { fmtDuration } from '../utils.js';
|
||||||
|
|
||||||
export type DisplayMode = 'compact' | 'default' | 'verbose';
|
export type DisplayMode = 'default' | 'verbose';
|
||||||
|
|
||||||
export interface SubagentExecutionDisplayProps {
|
export interface AgentExecutionDisplayProps {
|
||||||
data: TaskResultDisplay;
|
data: TaskResultDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ const getStatusColor = (
|
|||||||
case 'completed':
|
case 'completed':
|
||||||
case 'success':
|
case 'success':
|
||||||
return theme.status.success;
|
return theme.status.success;
|
||||||
|
case 'cancelled':
|
||||||
|
return theme.status.warning;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return theme.status.error;
|
return theme.status.error;
|
||||||
default:
|
default:
|
||||||
@@ -45,6 +47,8 @@ const getStatusText = (status: TaskResultDisplay['status']) => {
|
|||||||
return 'Running';
|
return 'Running';
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return 'Completed';
|
return 'Completed';
|
||||||
|
case 'cancelled':
|
||||||
|
return 'User Cancelled';
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return 'Failed';
|
return 'Failed';
|
||||||
default:
|
default:
|
||||||
@@ -52,14 +56,17 @@ const getStatusText = (status: TaskResultDisplay['status']) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_TOOL_CALLS = 5;
|
||||||
|
const MAX_TASK_PROMPT_LINES = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display subagent execution progress and results.
|
* Component to display subagent execution progress and results.
|
||||||
* This is now a pure component that renders the provided SubagentExecutionResultDisplay data.
|
* 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.
|
* Real-time updates are handled by the parent component updating the data prop.
|
||||||
*/
|
*/
|
||||||
export const SubagentExecutionDisplay: React.FC<
|
export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
||||||
SubagentExecutionDisplayProps
|
data,
|
||||||
> = ({ data }) => {
|
}) => {
|
||||||
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('default');
|
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('default');
|
||||||
|
|
||||||
const agentColor = useMemo(() => {
|
const agentColor = useMemo(() => {
|
||||||
@@ -76,27 +83,25 @@ export const SubagentExecutionDisplay: React.FC<
|
|||||||
if (displayMode === 'verbose') return 'Press ctrl+r to show less.';
|
if (displayMode === 'verbose') return 'Press ctrl+r to show less.';
|
||||||
|
|
||||||
if (displayMode === 'default') {
|
if (displayMode === 'default') {
|
||||||
const hasMoreLines = data.taskPrompt.split('\n').length > 10;
|
const hasMoreLines =
|
||||||
const hasMoreToolCalls = data.toolCalls && data.toolCalls.length > 5;
|
data.taskPrompt.split('\n').length > MAX_TASK_PROMPT_LINES;
|
||||||
|
const hasMoreToolCalls =
|
||||||
|
data.toolCalls && data.toolCalls.length > MAX_TOOL_CALLS;
|
||||||
|
|
||||||
if (hasMoreToolCalls || hasMoreLines) {
|
if (hasMoreToolCalls || hasMoreLines) {
|
||||||
return 'Press ctrl+s to show more.';
|
return 'Press ctrl+r to show more.';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}, [displayMode, data.toolCalls, data.taskPrompt, data.status]);
|
}, [displayMode, data.toolCalls, data.taskPrompt, data.status]);
|
||||||
|
|
||||||
// Handle ctrl+s and ctrl+r keypresses to control display mode
|
// Handle ctrl+r keypresses to control display mode
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
if (key.ctrl && key.name === 's') {
|
if (key.ctrl && key.name === 'r') {
|
||||||
setDisplayMode((current) =>
|
setDisplayMode((current) =>
|
||||||
current === 'default' ? 'verbose' : 'verbose',
|
current === 'default' ? 'verbose' : 'default',
|
||||||
);
|
|
||||||
} else if (key.ctrl && key.name === 'r') {
|
|
||||||
setDisplayMode((current) =>
|
|
||||||
current === 'verbose' ? 'default' : 'default',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -133,7 +138,9 @@ export const SubagentExecutionDisplay: React.FC<
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results section for completed/failed tasks */}
|
{/* Results section for completed/failed tasks */}
|
||||||
{(data.status === 'completed' || data.status === 'failed') && (
|
{(data.status === 'completed' ||
|
||||||
|
data.status === 'failed' ||
|
||||||
|
data.status === 'cancelled') && (
|
||||||
<ResultsSection data={data} displayMode={displayMode} />
|
<ResultsSection data={data} displayMode={displayMode} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -157,7 +164,7 @@ const TaskPromptSection: React.FC<{
|
|||||||
const lines = taskPrompt.split('\n');
|
const lines = taskPrompt.split('\n');
|
||||||
const shouldTruncate = lines.length > 10;
|
const shouldTruncate = lines.length > 10;
|
||||||
const showFull = displayMode === 'verbose';
|
const showFull = displayMode === 'verbose';
|
||||||
const displayLines = showFull ? lines : lines.slice(0, 10);
|
const displayLines = showFull ? lines : lines.slice(0, MAX_TASK_PROMPT_LINES);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={1}>
|
<Box flexDirection="column" gap={1}>
|
||||||
@@ -206,9 +213,9 @@ const ToolCallsList: React.FC<{
|
|||||||
displayMode: DisplayMode;
|
displayMode: DisplayMode;
|
||||||
}> = ({ toolCalls, displayMode }) => {
|
}> = ({ toolCalls, displayMode }) => {
|
||||||
const calls = toolCalls || [];
|
const calls = toolCalls || [];
|
||||||
const shouldTruncate = calls.length > 5;
|
const shouldTruncate = calls.length > MAX_TOOL_CALLS;
|
||||||
const showAll = displayMode === 'verbose';
|
const showAll = displayMode === 'verbose';
|
||||||
const displayCalls = showAll ? calls : calls.slice(-5); // Show last 5
|
const displayCalls = showAll ? calls : calls.slice(-MAX_TOOL_CALLS); // Show last 5
|
||||||
|
|
||||||
// Reverse the order to show most recent first
|
// Reverse the order to show most recent first
|
||||||
const reversedDisplayCalls = [...displayCalls].reverse();
|
const reversedDisplayCalls = [...displayCalls].reverse();
|
||||||
@@ -220,7 +227,7 @@ const ToolCallsList: React.FC<{
|
|||||||
{shouldTruncate && displayMode === 'default' && (
|
{shouldTruncate && displayMode === 'default' && (
|
||||||
<Text color={Colors.Gray}>
|
<Text color={Colors.Gray}>
|
||||||
{' '}
|
{' '}
|
||||||
Showing the last 5 of {calls.length} tools.
|
Showing the last {MAX_TOOL_CALLS} of {calls.length} tools.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -390,16 +397,18 @@ const ResultsSection: React.FC<{
|
|||||||
<ToolCallsList toolCalls={data.toolCalls} displayMode={displayMode} />
|
<ToolCallsList toolCalls={data.toolCalls} displayMode={displayMode} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Execution Summary section */}
|
{/* Execution Summary section - hide when cancelled */}
|
||||||
<Box flexDirection="column">
|
{data.status !== 'cancelled' && (
|
||||||
<Box flexDirection="row" marginBottom={1}>
|
<Box flexDirection="column">
|
||||||
<Text color={theme.text.primary}>Execution Summary:</Text>
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
|
<Text color={theme.text.primary}>Execution Summary:</Text>
|
||||||
|
</Box>
|
||||||
|
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
|
||||||
</Box>
|
</Box>
|
||||||
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
|
)}
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Tool Usage section */}
|
{/* Tool Usage section - hide when cancelled */}
|
||||||
{data.executionSummary && (
|
{data.status !== 'cancelled' && data.executionSummary && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box flexDirection="row" marginBottom={1}>
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
<Text color={theme.text.primary}>Tool Usage:</Text>
|
<Text color={theme.text.primary}>Tool Usage:</Text>
|
||||||
@@ -409,11 +418,18 @@ const ResultsSection: React.FC<{
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error reason for failed tasks */}
|
{/* Error reason for failed tasks */}
|
||||||
{data.status === 'failed' && data.terminateReason && (
|
{data.status === 'cancelled' && (
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
<Text color={theme.status.warning}>⏹ User Cancelled</Text>
|
||||||
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{data.status === 'failed' &&
|
||||||
|
data.terminateReason &&
|
||||||
|
data.terminateReason !== 'CANCELLED' && (
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
||||||
|
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { MANAGEMENT_STEPS } from './types.js';
|
import { MANAGEMENT_STEPS } from '../types.js';
|
||||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface ActionSelectionStepProps {
|
interface ActionSelectionStepProps {
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
import { StepNavigationProps } from './types.js';
|
import { StepNavigationProps } from '../types.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||||
|
|
||||||
interface AgentDeleteStepProps extends StepNavigationProps {
|
interface AgentDeleteStepProps extends StepNavigationProps {
|
||||||
selectedAgent: SubagentConfig | null;
|
selectedAgent: SubagentConfig | null;
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||||
import { MANAGEMENT_STEPS } from './types.js';
|
import { MANAGEMENT_STEPS } from '../types.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { useLaunchEditor } from './useLaunchEditor.js';
|
import { useLaunchEditor } from '../../../hooks/useLaunchEditor.js';
|
||||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface EditOption {
|
interface EditOption {
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../../hooks/useKeypress.js';
|
||||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface NavigationState {
|
interface NavigationState {
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { shouldShowColor, getColorForDisplay } from './utils.js';
|
import { shouldShowColor, getColorForDisplay } from '../utils.js';
|
||||||
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface AgentViewerStepProps {
|
interface AgentViewerStepProps {
|
||||||
@@ -11,11 +11,11 @@ import { ActionSelectionStep } from './ActionSelectionStep.js';
|
|||||||
import { AgentViewerStep } from './AgentViewerStep.js';
|
import { AgentViewerStep } from './AgentViewerStep.js';
|
||||||
import { EditOptionsStep } from './AgentEditStep.js';
|
import { EditOptionsStep } from './AgentEditStep.js';
|
||||||
import { AgentDeleteStep } from './AgentDeleteStep.js';
|
import { AgentDeleteStep } from './AgentDeleteStep.js';
|
||||||
import { ToolSelector } from './ToolSelector.js';
|
import { ToolSelector } from '../create/ToolSelector.js';
|
||||||
import { ColorSelector } from './ColorSelector.js';
|
import { ColorSelector } from '../create/ColorSelector.js';
|
||||||
import { MANAGEMENT_STEPS } from './types.js';
|
import { MANAGEMENT_STEPS } from '../types.js';
|
||||||
import { Colors } from '../../colors.js';
|
import { Colors } from '../../../colors.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../../semantic-colors.js';
|
||||||
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
|
import { Config, SubagentConfig } from '@qwen-code/qwen-code-core';
|
||||||
|
|
||||||
interface AgentsManagerDialogProps {
|
interface AgentsManagerDialogProps {
|
||||||
@@ -8,7 +8,7 @@ import { useCallback } from 'react';
|
|||||||
import { useStdin } from 'ink';
|
import { useStdin } from 'ink';
|
||||||
import { EditorType } from '@qwen-code/qwen-code-core';
|
import { EditorType } from '@qwen-code/qwen-code-core';
|
||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the editor command to use based on user preferences and platform.
|
* Determines the editor command to use based on user preferences and platform.
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
Config,
|
Config,
|
||||||
Kind,
|
Kind,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
|
ToolResultDisplay,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
} from '../index.js';
|
} from '../index.js';
|
||||||
import { Part, PartListUnion } from '@google/genai';
|
import { Part, PartListUnion } from '@google/genai';
|
||||||
@@ -633,6 +634,135 @@ describe('CoreToolScheduler YOLO mode', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('CoreToolScheduler cancellation during executing with live output', () => {
|
||||||
|
it('sets status to cancelled and preserves last output', async () => {
|
||||||
|
class StreamingInvocation extends BaseToolInvocation<
|
||||||
|
{ id: string },
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
getDescription(): string {
|
||||||
|
return `Streaming tool ${this.params.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
signal: AbortSignal,
|
||||||
|
updateOutput?: (output: ToolResultDisplay) => void,
|
||||||
|
): Promise<ToolResult> {
|
||||||
|
updateOutput?.('hello');
|
||||||
|
// Wait until aborted to emulate a long-running task
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
if (signal.aborted) return resolve();
|
||||||
|
const onAbort = () => {
|
||||||
|
signal.removeEventListener('abort', onAbort);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
signal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
});
|
||||||
|
// Return a normal (non-error) result; scheduler should still mark cancelled
|
||||||
|
return { llmContent: 'done', returnDisplay: 'done' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamingTool extends BaseDeclarativeTool<
|
||||||
|
{ id: string },
|
||||||
|
ToolResult
|
||||||
|
> {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'stream-tool',
|
||||||
|
'Stream Tool',
|
||||||
|
'Emits live output and waits for abort',
|
||||||
|
Kind.Other,
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: { id: { type: 'string' } },
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
protected createInvocation(params: { id: string }) {
|
||||||
|
return new StreamingInvocation(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = new StreamingTool();
|
||||||
|
const mockToolRegistry = {
|
||||||
|
getTool: () => tool,
|
||||||
|
getFunctionDeclarations: () => [],
|
||||||
|
tools: new Map(),
|
||||||
|
discovery: {},
|
||||||
|
registerTool: () => {},
|
||||||
|
getToolByName: () => tool,
|
||||||
|
getToolByDisplayName: () => tool,
|
||||||
|
getTools: () => [],
|
||||||
|
discoverTools: async () => {},
|
||||||
|
getAllTools: () => [],
|
||||||
|
getToolsByServer: () => [],
|
||||||
|
} as unknown as ToolRegistry;
|
||||||
|
|
||||||
|
const onAllToolCallsComplete = vi.fn();
|
||||||
|
const onToolCallsUpdate = vi.fn();
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
getSessionId: () => 'test-session-id',
|
||||||
|
getUsageStatisticsEnabled: () => true,
|
||||||
|
getDebugMode: () => false,
|
||||||
|
getApprovalMode: () => ApprovalMode.DEFAULT,
|
||||||
|
getContentGeneratorConfig: () => ({
|
||||||
|
model: 'test-model',
|
||||||
|
authType: 'oauth-personal',
|
||||||
|
}),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const scheduler = new CoreToolScheduler({
|
||||||
|
config: mockConfig,
|
||||||
|
toolRegistry: mockToolRegistry,
|
||||||
|
onAllToolCallsComplete,
|
||||||
|
onToolCallsUpdate,
|
||||||
|
getPreferredEditor: () => 'vscode',
|
||||||
|
onEditorClose: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const request = {
|
||||||
|
callId: '1',
|
||||||
|
name: 'stream-tool',
|
||||||
|
args: { id: 'x' },
|
||||||
|
isClientInitiated: true,
|
||||||
|
prompt_id: 'prompt-stream',
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulePromise = scheduler.schedule(
|
||||||
|
[request],
|
||||||
|
abortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait until executing
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
const calls = onToolCallsUpdate.mock.calls;
|
||||||
|
const last = calls[calls.length - 1]?.[0][0] as ToolCall | undefined;
|
||||||
|
expect(last?.status).toBe('executing');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now abort
|
||||||
|
abortController.abort();
|
||||||
|
|
||||||
|
await schedulePromise;
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
const completedCalls = onAllToolCallsComplete.mock
|
||||||
|
.calls[0][0] as ToolCall[];
|
||||||
|
expect(completedCalls[0].status).toBe('cancelled');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const cancelled: any = completedCalls[0];
|
||||||
|
expect(cancelled.response.resultDisplay).toBe('hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('CoreToolScheduler request queueing', () => {
|
describe('CoreToolScheduler request queueing', () => {
|
||||||
it('should queue a request if another is running', async () => {
|
it('should queue a request if another is running', async () => {
|
||||||
let resolveFirstCall: (result: ToolResult) => void;
|
let resolveFirstCall: (result: ToolResult) => void;
|
||||||
|
|||||||
@@ -374,6 +374,13 @@ export class CoreToolScheduler {
|
|||||||
newContent: waitingCall.confirmationDetails.newContent,
|
newContent: waitingCall.confirmationDetails.newContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (currentCall.status === 'executing') {
|
||||||
|
// If the tool was streaming live output, preserve the latest
|
||||||
|
// output so the UI can continue to show it after cancellation.
|
||||||
|
const executingCall = currentCall as ExecutingToolCall;
|
||||||
|
if (executingCall.liveOutput !== undefined) {
|
||||||
|
resultDisplay = executingCall.liveOutput;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -816,20 +823,19 @@ export class CoreToolScheduler {
|
|||||||
const invocation = scheduledCall.invocation;
|
const invocation = scheduledCall.invocation;
|
||||||
this.setStatusInternal(callId, 'executing');
|
this.setStatusInternal(callId, 'executing');
|
||||||
|
|
||||||
const liveOutputCallback =
|
const liveOutputCallback = scheduledCall.tool.canUpdateOutput
|
||||||
scheduledCall.tool.canUpdateOutput && this.outputUpdateHandler
|
? (outputChunk: ToolResultDisplay) => {
|
||||||
? (outputChunk: ToolResultDisplay) => {
|
if (this.outputUpdateHandler) {
|
||||||
if (this.outputUpdateHandler) {
|
this.outputUpdateHandler(callId, outputChunk);
|
||||||
this.outputUpdateHandler(callId, outputChunk);
|
|
||||||
}
|
|
||||||
this.toolCalls = this.toolCalls.map((tc) =>
|
|
||||||
tc.request.callId === callId && tc.status === 'executing'
|
|
||||||
? { ...tc, liveOutput: outputChunk }
|
|
||||||
: tc,
|
|
||||||
);
|
|
||||||
this.notifyToolCallsUpdate();
|
|
||||||
}
|
}
|
||||||
: undefined;
|
this.toolCalls = this.toolCalls.map((tc) =>
|
||||||
|
tc.request.callId === callId && tc.status === 'executing'
|
||||||
|
? { ...tc, liveOutput: outputChunk }
|
||||||
|
: tc,
|
||||||
|
);
|
||||||
|
this.notifyToolCallsUpdate();
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
invocation
|
invocation
|
||||||
.execute(signal, liveOutputCallback)
|
.execute(signal, liveOutputCallback)
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export type {
|
|||||||
RunConfig,
|
RunConfig,
|
||||||
ToolConfig,
|
ToolConfig,
|
||||||
SubagentTerminateMode,
|
SubagentTerminateMode,
|
||||||
OutputObject,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
export { SubAgentScope } from './subagent.js';
|
export { SubAgentScope } from './subagent.js';
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ describe('subagent.ts', () => {
|
|||||||
await expect(scope.runNonInteractive(context)).rejects.toThrow(
|
await expect(scope.runNonInteractive(context)).rejects.toThrow(
|
||||||
'Missing context values for the following keys: missing',
|
'Missing context values for the following keys: missing',
|
||||||
);
|
);
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.ERROR);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate that systemPrompt and initialMessages are mutually exclusive', async () => {
|
it('should validate that systemPrompt and initialMessages are mutually exclusive', async () => {
|
||||||
@@ -434,7 +434,7 @@ describe('subagent.ts', () => {
|
|||||||
await expect(agent.runNonInteractive(context)).rejects.toThrow(
|
await expect(agent.runNonInteractive(context)).rejects.toThrow(
|
||||||
'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
|
'PromptConfig cannot have both `systemPrompt` and `initialMessages` defined.',
|
||||||
);
|
);
|
||||||
expect(agent.output.terminate_reason).toBe(SubagentTerminateMode.ERROR);
|
expect(agent.getTerminateMode()).toBe(SubagentTerminateMode.ERROR);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -457,8 +457,7 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.GOAL);
|
||||||
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([
|
||||||
@@ -482,8 +481,7 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.GOAL);
|
||||||
expect(scope.output.result).toBe('Done.');
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -549,7 +547,7 @@ describe('subagent.ts', () => {
|
|||||||
{ text: 'file1.txt\nfile2.ts' },
|
{ text: 'file1.txt\nfile2.ts' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.GOAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide specific tool error responses to the model', async () => {
|
it('should provide specific tool error responses to the model', async () => {
|
||||||
@@ -645,9 +643,7 @@ describe('subagent.ts', () => {
|
|||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
expect(mockSendMessageStream).toHaveBeenCalledTimes(2);
|
||||||
expect(scope.output.terminate_reason).toBe(
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.MAX_TURNS);
|
||||||
SubagentTerminateMode.MAX_TURNS,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should terminate with TIMEOUT if the time limit is reached during an LLM call', async () => {
|
it('should terminate with TIMEOUT if the time limit is reached during an LLM call', async () => {
|
||||||
@@ -690,9 +686,7 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
await runPromise;
|
await runPromise;
|
||||||
|
|
||||||
expect(scope.output.terminate_reason).toBe(
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.TIMEOUT);
|
||||||
SubagentTerminateMode.TIMEOUT,
|
|
||||||
);
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
@@ -713,7 +707,7 @@ describe('subagent.ts', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
scope.runNonInteractive(new ContextState()),
|
scope.runNonInteractive(new ContextState()),
|
||||||
).rejects.toThrow('API Failure');
|
).rejects.toThrow('API Failure');
|
||||||
expect(scope.output.terminate_reason).toBe(SubagentTerminateMode.ERROR);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.ERROR);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { GeminiChat } from '../core/geminiChat.js';
|
import { GeminiChat } from '../core/geminiChat.js';
|
||||||
import {
|
import {
|
||||||
OutputObject,
|
|
||||||
SubagentTerminateMode,
|
SubagentTerminateMode,
|
||||||
PromptConfig,
|
PromptConfig,
|
||||||
ModelConfig,
|
ModelConfig,
|
||||||
@@ -150,10 +149,6 @@ function templateString(template: string, context: ContextState): string {
|
|||||||
* runtime context, and the collection of its outputs.
|
* runtime context, and the collection of its outputs.
|
||||||
*/
|
*/
|
||||||
export class SubAgentScope {
|
export class SubAgentScope {
|
||||||
output: OutputObject = {
|
|
||||||
terminate_reason: SubagentTerminateMode.ERROR,
|
|
||||||
result: '',
|
|
||||||
};
|
|
||||||
executionStats: ExecutionStats = {
|
executionStats: ExecutionStats = {
|
||||||
startTimeMs: 0,
|
startTimeMs: 0,
|
||||||
totalDurationMs: 0,
|
totalDurationMs: 0,
|
||||||
@@ -179,6 +174,7 @@ export class SubAgentScope {
|
|||||||
>();
|
>();
|
||||||
private eventEmitter?: SubAgentEventEmitter;
|
private eventEmitter?: SubAgentEventEmitter;
|
||||||
private finalText: string = '';
|
private finalText: string = '';
|
||||||
|
private terminateMode: SubagentTerminateMode = SubagentTerminateMode.ERROR;
|
||||||
private readonly stats = new SubagentStatistics();
|
private readonly stats = new SubagentStatistics();
|
||||||
private hooks?: SubagentHooks;
|
private hooks?: SubagentHooks;
|
||||||
private readonly subagentId: string;
|
private readonly subagentId: string;
|
||||||
@@ -312,14 +308,18 @@ export class SubAgentScope {
|
|||||||
const chat = await this.createChatObject(context);
|
const chat = await this.createChatObject(context);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
this.output.terminate_reason = SubagentTerminateMode.ERROR;
|
this.terminateMode = SubagentTerminateMode.ERROR;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const onAbort = () => abortController.abort();
|
const onAbort = () => abortController.abort();
|
||||||
if (externalSignal) {
|
if (externalSignal) {
|
||||||
if (externalSignal.aborted) abortController.abort();
|
if (externalSignal.aborted) {
|
||||||
|
abortController.abort();
|
||||||
|
this.terminateMode = SubagentTerminateMode.CANCELLED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
externalSignal.addEventListener('abort', onAbort, { once: true });
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
}
|
}
|
||||||
const toolRegistry = this.runtimeContext.getToolRegistry();
|
const toolRegistry = this.runtimeContext.getToolRegistry();
|
||||||
@@ -381,7 +381,7 @@ export class SubAgentScope {
|
|||||||
this.runConfig.max_turns &&
|
this.runConfig.max_turns &&
|
||||||
turnCounter >= this.runConfig.max_turns
|
turnCounter >= this.runConfig.max_turns
|
||||||
) {
|
) {
|
||||||
this.output.terminate_reason = SubagentTerminateMode.MAX_TURNS;
|
this.terminateMode = SubagentTerminateMode.MAX_TURNS;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let durationMin = (Date.now() - startTime) / (1000 * 60);
|
let durationMin = (Date.now() - startTime) / (1000 * 60);
|
||||||
@@ -389,7 +389,7 @@ export class SubAgentScope {
|
|||||||
this.runConfig.max_time_minutes &&
|
this.runConfig.max_time_minutes &&
|
||||||
durationMin >= this.runConfig.max_time_minutes
|
durationMin >= this.runConfig.max_time_minutes
|
||||||
) {
|
) {
|
||||||
this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
|
this.terminateMode = SubagentTerminateMode.TIMEOUT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +418,10 @@ export class SubAgentScope {
|
|||||||
let lastUsage: GenerateContentResponseUsageMetadata | undefined =
|
let lastUsage: GenerateContentResponseUsageMetadata | undefined =
|
||||||
undefined;
|
undefined;
|
||||||
for await (const resp of responseStream) {
|
for await (const resp of responseStream) {
|
||||||
if (abortController.signal.aborted) return;
|
if (abortController.signal.aborted) {
|
||||||
|
this.terminateMode = SubagentTerminateMode.CANCELLED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
|
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
|
||||||
const content = resp.candidates?.[0]?.content;
|
const content = resp.candidates?.[0]?.content;
|
||||||
const parts = content?.parts || [];
|
const parts = content?.parts || [];
|
||||||
@@ -443,7 +446,7 @@ export class SubAgentScope {
|
|||||||
this.runConfig.max_time_minutes &&
|
this.runConfig.max_time_minutes &&
|
||||||
durationMin >= this.runConfig.max_time_minutes
|
durationMin >= this.runConfig.max_time_minutes
|
||||||
) {
|
) {
|
||||||
this.output.terminate_reason = SubagentTerminateMode.TIMEOUT;
|
this.terminateMode = SubagentTerminateMode.TIMEOUT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,8 +486,7 @@ export class SubAgentScope {
|
|||||||
// No tool calls — treat this as the model's final answer.
|
// No tool calls — treat this as the model's final answer.
|
||||||
if (roundText && roundText.trim().length > 0) {
|
if (roundText && roundText.trim().length > 0) {
|
||||||
this.finalText = roundText.trim();
|
this.finalText = roundText.trim();
|
||||||
this.output.result = this.finalText;
|
this.terminateMode = SubagentTerminateMode.GOAL;
|
||||||
this.output.terminate_reason = SubagentTerminateMode.GOAL;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Otherwise, nudge the model to finalize a result.
|
// Otherwise, nudge the model to finalize a result.
|
||||||
@@ -508,7 +510,7 @@ export class SubAgentScope {
|
|||||||
}
|
}
|
||||||
} 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.terminateMode = SubagentTerminateMode.ERROR;
|
||||||
this.eventEmitter?.emit(SubAgentEventType.ERROR, {
|
this.eventEmitter?.emit(SubAgentEventType.ERROR, {
|
||||||
subagentId: this.subagentId,
|
subagentId: this.subagentId,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
@@ -529,7 +531,7 @@ export class SubAgentScope {
|
|||||||
const summary = this.stats.getSummary(Date.now());
|
const summary = this.stats.getSummary(Date.now());
|
||||||
this.eventEmitter?.emit(SubAgentEventType.FINISH, {
|
this.eventEmitter?.emit(SubAgentEventType.FINISH, {
|
||||||
subagentId: this.subagentId,
|
subagentId: this.subagentId,
|
||||||
terminate_reason: this.output.terminate_reason,
|
terminate_reason: this.terminateMode,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
rounds: summary.rounds,
|
rounds: summary.rounds,
|
||||||
totalDurationMs: summary.totalDurationMs,
|
totalDurationMs: summary.totalDurationMs,
|
||||||
@@ -541,14 +543,13 @@ export class SubAgentScope {
|
|||||||
totalTokens: summary.totalTokens,
|
totalTokens: summary.totalTokens,
|
||||||
} as SubAgentFinishEvent);
|
} as SubAgentFinishEvent);
|
||||||
|
|
||||||
// Log telemetry for subagent completion
|
|
||||||
const completionEvent = new SubagentExecutionEvent(
|
const completionEvent = new SubagentExecutionEvent(
|
||||||
this.name,
|
this.name,
|
||||||
this.output.terminate_reason === SubagentTerminateMode.GOAL
|
this.terminateMode === SubagentTerminateMode.GOAL
|
||||||
? 'completed'
|
? 'completed'
|
||||||
: 'failed',
|
: 'failed',
|
||||||
{
|
{
|
||||||
terminate_reason: this.output.terminate_reason,
|
terminate_reason: this.terminateMode,
|
||||||
result: this.finalText,
|
result: this.finalText,
|
||||||
execution_summary: this.stats.formatCompact(
|
execution_summary: this.stats.formatCompact(
|
||||||
'Subagent execution completed',
|
'Subagent execution completed',
|
||||||
@@ -560,7 +561,7 @@ export class SubAgentScope {
|
|||||||
await this.hooks?.onStop?.({
|
await this.hooks?.onStop?.({
|
||||||
subagentId: this.subagentId,
|
subagentId: this.subagentId,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
terminateReason: this.output.terminate_reason,
|
terminateReason: this.terminateMode,
|
||||||
summary: summary as unknown as Record<string, unknown>,
|
summary: summary as unknown as Record<string, unknown>,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -751,6 +752,10 @@ export class SubAgentScope {
|
|||||||
return this.finalText;
|
return this.finalText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTerminateMode(): SubagentTerminateMode {
|
||||||
|
return this.terminateMode;
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
@@ -183,24 +183,10 @@ export enum SubagentTerminateMode {
|
|||||||
* Indicates that the subagent's execution terminated because it exceeded the maximum number of turns.
|
* Indicates that the subagent's execution terminated because it exceeded the maximum number of turns.
|
||||||
*/
|
*/
|
||||||
MAX_TURNS = 'MAX_TURNS',
|
MAX_TURNS = 'MAX_TURNS',
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the output structure of a subagent's execution.
|
|
||||||
* This interface defines the data that a subagent will return upon completion,
|
|
||||||
* including the final result and the reason for its termination.
|
|
||||||
*/
|
|
||||||
export interface OutputObject {
|
|
||||||
/**
|
/**
|
||||||
* The final result text returned by the subagent upon completion.
|
* Indicates that the subagent's execution was cancelled via an abort signal.
|
||||||
* This contains the direct output from the model's final response.
|
|
||||||
*/
|
*/
|
||||||
result: string;
|
CANCELLED = 'CANCELLED',
|
||||||
/**
|
|
||||||
* The reason for the subagent's termination, indicating whether it completed
|
|
||||||
* successfully, timed out, or encountered an error.
|
|
||||||
*/
|
|
||||||
terminate_reason: SubagentTerminateMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export function recordContentRetryFailure(config: Config): void {
|
|||||||
export function recordSubagentExecutionMetrics(
|
export function recordSubagentExecutionMetrics(
|
||||||
config: Config,
|
config: Config,
|
||||||
subagentName: string,
|
subagentName: string,
|
||||||
status: 'started' | 'progress' | 'completed' | 'failed',
|
status: 'started' | 'completed' | 'failed' | 'cancelled',
|
||||||
terminateReason?: string,
|
terminateReason?: string,
|
||||||
): void {
|
): void {
|
||||||
if (!subagentExecutionCounter || !isMetricsInitialized) return;
|
if (!subagentExecutionCounter || !isMetricsInitialized) return;
|
||||||
|
|||||||
@@ -448,14 +448,14 @@ export class SubagentExecutionEvent implements BaseTelemetryEvent {
|
|||||||
'event.name': 'subagent_execution';
|
'event.name': 'subagent_execution';
|
||||||
'event.timestamp': string;
|
'event.timestamp': string;
|
||||||
subagent_name: string;
|
subagent_name: string;
|
||||||
status: 'started' | 'progress' | 'completed' | 'failed';
|
status: 'started' | 'completed' | 'failed' | 'cancelled';
|
||||||
terminate_reason?: string;
|
terminate_reason?: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
execution_summary?: string;
|
execution_summary?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
subagent_name: string,
|
subagent_name: string,
|
||||||
status: 'started' | 'progress' | 'completed' | 'failed',
|
status: 'started' | 'completed' | 'failed' | 'cancelled',
|
||||||
options?: {
|
options?: {
|
||||||
terminate_reason?: string;
|
terminate_reason?: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
|
|||||||
@@ -258,10 +258,8 @@ describe('TaskTool', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockSubagentScope = {
|
mockSubagentScope = {
|
||||||
runNonInteractive: vi.fn().mockResolvedValue(undefined),
|
runNonInteractive: vi.fn().mockResolvedValue(undefined),
|
||||||
output: {
|
result: 'Task completed successfully',
|
||||||
result: 'Task completed successfully',
|
terminateMode: SubagentTerminateMode.GOAL,
|
||||||
terminate_reason: SubagentTerminateMode.GOAL,
|
|
||||||
},
|
|
||||||
getFinalText: vi.fn().mockReturnValue('Task completed successfully'),
|
getFinalText: vi.fn().mockReturnValue('Task completed successfully'),
|
||||||
formatCompactResult: vi
|
formatCompactResult: vi
|
||||||
.fn()
|
.fn()
|
||||||
@@ -305,6 +303,7 @@ describe('TaskTool', () => {
|
|||||||
successfulToolCalls: 3,
|
successfulToolCalls: 3,
|
||||||
failedToolCalls: 0,
|
failedToolCalls: 0,
|
||||||
}),
|
}),
|
||||||
|
getTerminateMode: vi.fn().mockReturnValue(SubagentTerminateMode.GOAL),
|
||||||
} as unknown as SubAgentScope;
|
} as unknown as SubAgentScope;
|
||||||
|
|
||||||
mockContextState = {
|
mockContextState = {
|
||||||
@@ -375,25 +374,6 @@ describe('TaskTool', () => {
|
|||||||
expect(display.subagentName).toBe('non-existent');
|
expect(display.subagentName).toBe('non-existent');
|
||||||
});
|
});
|
||||||
|
|
||||||
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 display = result.returnDisplay as TaskResultDisplay;
|
|
||||||
expect(display.status).toBe('failed');
|
|
||||||
expect(display.terminateReason).toBe('ERROR');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle execution errors gracefully', async () => {
|
it('should handle execution errors gracefully', async () => {
|
||||||
vi.mocked(mockSubagentManager.createSubagentScope).mockRejectedValue(
|
vi.mocked(mockSubagentManager.createSubagentScope).mockRejectedValue(
|
||||||
new Error('Creation failed'),
|
new Error('Creation failed'),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { Config } from '../config/config.js';
|
import { Config } from '../config/config.js';
|
||||||
import { SubagentManager } from '../subagents/subagent-manager.js';
|
import { SubagentManager } from '../subagents/subagent-manager.js';
|
||||||
import { SubagentConfig } from '../subagents/types.js';
|
import { SubagentConfig, SubagentTerminateMode } from '../subagents/types.js';
|
||||||
import { ContextState } from '../subagents/subagent.js';
|
import { ContextState } from '../subagents/subagent.js';
|
||||||
import {
|
import {
|
||||||
SubAgentEventEmitter,
|
SubAgentEventEmitter,
|
||||||
@@ -409,21 +409,6 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
// Set up event listeners for real-time updates
|
// Set up event listeners for real-time updates
|
||||||
this.setupEventListeners(updateOutput);
|
this.setupEventListeners(updateOutput);
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
signal.addEventListener('abort', () => {
|
|
||||||
if (this.currentDisplay) {
|
|
||||||
this.updateDisplay(
|
|
||||||
{
|
|
||||||
status: 'failed',
|
|
||||||
terminateReason: 'CANCELLED',
|
|
||||||
result: 'Task was cancelled by user',
|
|
||||||
},
|
|
||||||
updateOutput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send initial display
|
// Send initial display
|
||||||
if (updateOutput) {
|
if (updateOutput) {
|
||||||
updateOutput(this.currentDisplay);
|
updateOutput(this.currentDisplay);
|
||||||
@@ -474,20 +459,31 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
|
|
||||||
// Get the results
|
// Get the results
|
||||||
const finalText = subagentScope.getFinalText();
|
const finalText = subagentScope.getFinalText();
|
||||||
const terminateReason = subagentScope.output.terminate_reason;
|
const terminateReason = subagentScope.getTerminateMode();
|
||||||
const success = terminateReason === 'GOAL';
|
const success = terminateReason === SubagentTerminateMode.GOAL;
|
||||||
const executionSummary = subagentScope.getExecutionSummary();
|
const executionSummary = subagentScope.getExecutionSummary();
|
||||||
|
|
||||||
// Update the final display state
|
if (signal?.aborted) {
|
||||||
this.updateDisplay(
|
this.updateDisplay(
|
||||||
{
|
{
|
||||||
status: success ? 'completed' : 'failed',
|
status: 'cancelled',
|
||||||
terminateReason,
|
terminateReason: 'CANCELLED',
|
||||||
result: finalText,
|
result: finalText || 'Task was cancelled by user',
|
||||||
executionSummary,
|
executionSummary,
|
||||||
},
|
},
|
||||||
updateOutput,
|
updateOutput,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.updateDisplay(
|
||||||
|
{
|
||||||
|
status: success ? 'completed' : 'failed',
|
||||||
|
terminateReason,
|
||||||
|
result: finalText,
|
||||||
|
executionSummary,
|
||||||
|
},
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: [{ text: finalText }],
|
llmContent: [{ text: finalText }],
|
||||||
@@ -500,7 +496,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
|
|
||||||
const errorDisplay: TaskResultDisplay = {
|
const errorDisplay: TaskResultDisplay = {
|
||||||
...this.currentDisplay!,
|
...this.currentDisplay!,
|
||||||
status: 'failed' as const,
|
status: 'failed',
|
||||||
terminateReason: 'ERROR',
|
terminateReason: 'ERROR',
|
||||||
result: `Failed to run subagent: ${errorMessage}`,
|
result: `Failed to run subagent: ${errorMessage}`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ export interface TaskResultDisplay {
|
|||||||
subagentColor?: string;
|
subagentColor?: string;
|
||||||
taskDescription: string;
|
taskDescription: string;
|
||||||
taskPrompt: string;
|
taskPrompt: string;
|
||||||
status: 'running' | 'completed' | 'failed';
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||||
terminateReason?: string;
|
terminateReason?: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
executionSummary?: SubagentStatsSummary;
|
executionSummary?: SubagentStatsSummary;
|
||||||
|
|||||||
Reference in New Issue
Block a user