mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: enhances the capabilities of subagents by allowing them to use tools that require user
confirmation
This commit is contained in:
@@ -106,7 +106,13 @@ const SubagentExecutionRenderer: React.FC<{
|
|||||||
data: TaskResultDisplay;
|
data: TaskResultDisplay;
|
||||||
availableHeight?: number;
|
availableHeight?: number;
|
||||||
childWidth: number;
|
childWidth: number;
|
||||||
}> = ({ data }) => <AgentExecutionDisplay data={data} />;
|
}> = ({ data, availableHeight, childWidth }) => (
|
||||||
|
<AgentExecutionDisplay
|
||||||
|
data={data}
|
||||||
|
availableHeight={availableHeight}
|
||||||
|
childWidth={childWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render string results (markdown or plain text)
|
* Component to render string results (markdown or plain text)
|
||||||
|
|||||||
@@ -15,19 +15,27 @@ 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';
|
||||||
|
import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.js';
|
||||||
|
|
||||||
export type DisplayMode = 'default' | 'verbose';
|
export type DisplayMode = 'default' | 'verbose';
|
||||||
|
|
||||||
export interface AgentExecutionDisplayProps {
|
export interface AgentExecutionDisplayProps {
|
||||||
data: TaskResultDisplay;
|
data: TaskResultDisplay;
|
||||||
|
availableHeight?: number;
|
||||||
|
childWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (
|
const getStatusColor = (
|
||||||
status: TaskResultDisplay['status'] | 'executing' | 'success',
|
status:
|
||||||
|
| TaskResultDisplay['status']
|
||||||
|
| 'executing'
|
||||||
|
| 'success'
|
||||||
|
| 'awaiting_approval',
|
||||||
) => {
|
) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'running':
|
case 'running':
|
||||||
case 'executing':
|
case 'executing':
|
||||||
|
case 'awaiting_approval':
|
||||||
return theme.status.warning;
|
return theme.status.warning;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
case 'success':
|
case 'success':
|
||||||
@@ -66,6 +74,8 @@ const MAX_TASK_PROMPT_LINES = 5;
|
|||||||
*/
|
*/
|
||||||
export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
||||||
data,
|
data,
|
||||||
|
availableHeight,
|
||||||
|
childWidth,
|
||||||
}) => {
|
}) => {
|
||||||
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('default');
|
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('default');
|
||||||
|
|
||||||
@@ -137,6 +147,18 @@ export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Inline approval prompt when awaiting confirmation */}
|
||||||
|
{data.pendingConfirmation && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<ToolConfirmationMessage
|
||||||
|
confirmationDetails={data.pendingConfirmation}
|
||||||
|
isFocused={true}
|
||||||
|
availableTerminalHeight={availableHeight}
|
||||||
|
terminalWidth={childWidth ?? 80}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Results section for completed/failed tasks */}
|
{/* Results section for completed/failed tasks */}
|
||||||
{(data.status === 'completed' ||
|
{(data.status === 'completed' ||
|
||||||
data.status === 'failed' ||
|
data.status === 'failed' ||
|
||||||
@@ -247,7 +269,7 @@ const ToolCallsList: React.FC<{
|
|||||||
const ToolCallItem: React.FC<{
|
const ToolCallItem: React.FC<{
|
||||||
toolCall: {
|
toolCall: {
|
||||||
name: string;
|
name: string;
|
||||||
status: 'executing' | 'success' | 'failed';
|
status: 'executing' | 'awaiting_approval' | 'success' | 'failed';
|
||||||
error?: string;
|
error?: string;
|
||||||
args?: Record<string, unknown>;
|
args?: Record<string, unknown>;
|
||||||
result?: string;
|
result?: string;
|
||||||
@@ -263,6 +285,8 @@ const ToolCallItem: React.FC<{
|
|||||||
switch (toolCall.status) {
|
switch (toolCall.status) {
|
||||||
case 'executing':
|
case 'executing':
|
||||||
return <Text color={color}>⊷</Text>; // Using same as ToolMessage
|
return <Text color={color}>⊷</Text>; // Using same as ToolMessage
|
||||||
|
case 'awaiting_approval':
|
||||||
|
return <Text color={theme.status.warning}>?</Text>;
|
||||||
case 'success':
|
case 'success':
|
||||||
return <Text color={color}>✔</Text>;
|
return <Text color={color}>✔</Text>;
|
||||||
case 'failed':
|
case 'failed':
|
||||||
@@ -401,7 +425,7 @@ const ResultsSection: React.FC<{
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Execution Summary section - hide when cancelled */}
|
{/* Execution Summary section - hide when cancelled */}
|
||||||
{data.status !== 'cancelled' && (
|
{data.status === 'completed' && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box flexDirection="row" marginBottom={1}>
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
<Text color={theme.text.primary}>Execution Summary:</Text>
|
<Text color={theme.text.primary}>Execution Summary:</Text>
|
||||||
@@ -411,7 +435,7 @@ const ResultsSection: React.FC<{
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tool Usage section - hide when cancelled */}
|
{/* Tool Usage section - hide when cancelled */}
|
||||||
{data.status !== 'cancelled' && data.executionSummary && (
|
{data.status === 'completed' && 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>
|
||||||
@@ -426,13 +450,11 @@ const ResultsSection: React.FC<{
|
|||||||
<Text color={theme.status.warning}>⏹ User Cancelled</Text>
|
<Text color={theme.status.warning}>⏹ User Cancelled</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{data.status === 'failed' &&
|
{data.status === 'failed' && (
|
||||||
data.terminateReason &&
|
<Box flexDirection="row">
|
||||||
data.terminateReason !== 'CANCELLED' && (
|
<Text color={theme.status.error}>Task Failed: </Text>
|
||||||
<Box flexDirection="row">
|
<Text color={theme.status.error}>{data.terminateReason}</Text>
|
||||||
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
</Box>
|
||||||
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
)}
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import {
|
||||||
|
ToolCallConfirmationDetails,
|
||||||
|
ToolConfirmationOutcome,
|
||||||
|
} from '../tools/tools.js';
|
||||||
|
|
||||||
export type SubAgentEvent =
|
export type SubAgentEvent =
|
||||||
| 'start'
|
| 'start'
|
||||||
@@ -13,6 +17,7 @@ export type SubAgentEvent =
|
|||||||
| 'stream_text'
|
| 'stream_text'
|
||||||
| 'tool_call'
|
| 'tool_call'
|
||||||
| 'tool_result'
|
| 'tool_result'
|
||||||
|
| 'tool_waiting_approval'
|
||||||
| 'finish'
|
| 'finish'
|
||||||
| 'error';
|
| 'error';
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ export enum SubAgentEventType {
|
|||||||
STREAM_TEXT = 'stream_text',
|
STREAM_TEXT = 'stream_text',
|
||||||
TOOL_CALL = 'tool_call',
|
TOOL_CALL = 'tool_call',
|
||||||
TOOL_RESULT = 'tool_result',
|
TOOL_RESULT = 'tool_result',
|
||||||
|
TOOL_WAITING_APPROVAL = 'tool_waiting_approval',
|
||||||
FINISH = 'finish',
|
FINISH = 'finish',
|
||||||
ERROR = 'error',
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
@@ -71,9 +77,25 @@ export interface SubAgentToolResultEvent {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubAgentApprovalRequestEvent {
|
||||||
|
subagentId: string;
|
||||||
|
round: number;
|
||||||
|
callId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
confirmationDetails: Omit<ToolCallConfirmationDetails, 'onConfirm'> & {
|
||||||
|
type: ToolCallConfirmationDetails['type'];
|
||||||
|
};
|
||||||
|
respond: (
|
||||||
|
outcome: ToolConfirmationOutcome,
|
||||||
|
payload?: Parameters<ToolCallConfirmationDetails['onConfirm']>[1],
|
||||||
|
) => Promise<void>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubAgentFinishEvent {
|
export interface SubAgentFinishEvent {
|
||||||
subagentId: string;
|
subagentId: string;
|
||||||
terminate_reason: string;
|
terminateReason: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
rounds?: number;
|
rounds?: number;
|
||||||
totalDurationMs?: number;
|
totalDurationMs?: number;
|
||||||
|
|||||||
@@ -19,15 +19,16 @@ import { createContentGenerator } from '../core/contentGenerator.js';
|
|||||||
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
||||||
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
|
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
|
||||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||||
|
import { AnyDeclarativeTool } from '../tools/tools.js';
|
||||||
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
|
||||||
import {
|
import {
|
||||||
Content,
|
Content,
|
||||||
FunctionCall,
|
FunctionCall,
|
||||||
FunctionDeclaration,
|
FunctionDeclaration,
|
||||||
GenerateContentConfig,
|
GenerateContentConfig,
|
||||||
|
Part,
|
||||||
Type,
|
Type,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
import { ToolErrorType } from '../tools/tool-error.js';
|
|
||||||
|
|
||||||
vi.mock('../core/geminiChat.js');
|
vi.mock('../core/geminiChat.js');
|
||||||
vi.mock('../core/contentGenerator.js');
|
vi.mock('../core/contentGenerator.js');
|
||||||
@@ -193,7 +194,7 @@ describe('subagent.ts', () => {
|
|||||||
expect(scope).toBeInstanceOf(SubAgentScope);
|
expect(scope).toBeInstanceOf(SubAgentScope);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a tool requires confirmation', async () => {
|
it('should not block creation when a tool may require confirmation', async () => {
|
||||||
const mockTool = {
|
const mockTool = {
|
||||||
schema: { parameters: { type: Type.OBJECT, properties: {} } },
|
schema: { parameters: { type: Type.OBJECT, properties: {} } },
|
||||||
build: vi.fn().mockReturnValue({
|
build: vi.fn().mockReturnValue({
|
||||||
@@ -212,18 +213,15 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
const toolConfig: ToolConfig = { tools: ['risky_tool'] };
|
const toolConfig: ToolConfig = { tools: ['risky_tool'] };
|
||||||
|
|
||||||
await expect(
|
const scope = await SubAgentScope.create(
|
||||||
SubAgentScope.create(
|
'test-agent',
|
||||||
'test-agent',
|
config,
|
||||||
config,
|
promptConfig,
|
||||||
promptConfig,
|
defaultModelConfig,
|
||||||
defaultModelConfig,
|
defaultRunConfig,
|
||||||
defaultRunConfig,
|
toolConfig,
|
||||||
toolConfig,
|
|
||||||
),
|
|
||||||
).rejects.toThrow(
|
|
||||||
'Tool "risky_tool" requires user confirmation and cannot be used in a non-interactive subagent.',
|
|
||||||
);
|
);
|
||||||
|
expect(scope).toBeInstanceOf(SubAgentScope);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should succeed if tools do not require confirmation', async () => {
|
it('should succeed if tools do not require confirmation', async () => {
|
||||||
@@ -251,11 +249,7 @@ describe('subagent.ts', () => {
|
|||||||
expect(scope).toBeInstanceOf(SubAgentScope);
|
expect(scope).toBeInstanceOf(SubAgentScope);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip interactivity check and warn for tools with required parameters', async () => {
|
it('should allow creation regardless of tool parameter requirements', async () => {
|
||||||
const consoleWarnSpy = vi
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const mockToolWithParams = {
|
const mockToolWithParams = {
|
||||||
schema: {
|
schema: {
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -266,7 +260,6 @@ describe('subagent.ts', () => {
|
|||||||
required: ['path'],
|
required: ['path'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// build should not be called, but we mock it to be safe
|
|
||||||
build: vi.fn(),
|
build: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,7 +269,6 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
const toolConfig: ToolConfig = { tools: ['tool_with_params'] };
|
const toolConfig: ToolConfig = { tools: ['tool_with_params'] };
|
||||||
|
|
||||||
// The creation should succeed without throwing
|
|
||||||
const scope = await SubAgentScope.create(
|
const scope = await SubAgentScope.create(
|
||||||
'test-agent',
|
'test-agent',
|
||||||
config,
|
config,
|
||||||
@@ -287,16 +279,8 @@ describe('subagent.ts', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(scope).toBeInstanceOf(SubAgentScope);
|
expect(scope).toBeInstanceOf(SubAgentScope);
|
||||||
|
// Ensure build was not called during creation
|
||||||
// Check that the warning was logged
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
'Cannot check tool "tool_with_params" for interactivity because it requires parameters. Assuming it is safe for non-interactive use.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure build was never called
|
|
||||||
expect(mockToolWithParams.build).not.toHaveBeenCalled();
|
expect(mockToolWithParams.build).not.toHaveBeenCalled();
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -514,14 +498,31 @@ describe('subagent.ts', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mock the tool execution result
|
// Provide a mock tool via ToolRegistry that returns a successful result
|
||||||
vi.mocked(executeToolCall).mockResolvedValue({
|
const listFilesInvocation = {
|
||||||
callId: 'call_1',
|
params: { path: '.' },
|
||||||
responseParts: 'file1.txt\nfile2.ts',
|
getDescription: vi.fn().mockReturnValue('List files'),
|
||||||
resultDisplay: 'Listed 2 files',
|
toolLocations: vi.fn().mockReturnValue([]),
|
||||||
error: undefined,
|
shouldConfirmExecute: vi.fn().mockResolvedValue(false),
|
||||||
errorType: undefined, // Or ToolErrorType.NONE if available and appropriate
|
execute: vi.fn().mockResolvedValue({
|
||||||
});
|
llmContent: 'file1.txt\nfile2.ts',
|
||||||
|
returnDisplay: 'Listed 2 files',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const listFilesTool = {
|
||||||
|
name: 'list_files',
|
||||||
|
displayName: 'List Files',
|
||||||
|
description: 'List files in directory',
|
||||||
|
kind: 'READ' as const,
|
||||||
|
schema: listFilesToolDef,
|
||||||
|
build: vi.fn().mockImplementation(() => listFilesInvocation),
|
||||||
|
canUpdateOutput: false,
|
||||||
|
isOutputMarkdown: true,
|
||||||
|
} as unknown as AnyDeclarativeTool;
|
||||||
|
vi.mocked((config.getToolRegistry() as unknown as ToolRegistry).getTool)
|
||||||
|
.mockImplementation((name: string) =>
|
||||||
|
name === 'list_files' ? listFilesTool : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const scope = await SubAgentScope.create(
|
const scope = await SubAgentScope.create(
|
||||||
'test-agent',
|
'test-agent',
|
||||||
@@ -534,70 +535,19 @@ describe('subagent.ts', () => {
|
|||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
await scope.runNonInteractive(new ContextState());
|
||||||
|
|
||||||
// Check tool execution
|
// Check the response sent back to the model (functionResponse part)
|
||||||
expect(executeToolCall).toHaveBeenCalledWith(
|
|
||||||
config,
|
|
||||||
expect.objectContaining({ name: 'list_files', args: { path: '.' } }),
|
|
||||||
expect.any(AbortSignal),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check the response sent back to the model
|
|
||||||
const secondCallArgs = mockSendMessageStream.mock.calls[1][0];
|
const secondCallArgs = mockSendMessageStream.mock.calls[1][0];
|
||||||
expect(secondCallArgs.message).toEqual([
|
const parts = secondCallArgs.message as unknown[];
|
||||||
{ text: 'file1.txt\nfile2.ts' },
|
expect(Array.isArray(parts)).toBe(true);
|
||||||
]);
|
const firstPart = parts[0] as Part;
|
||||||
|
expect(firstPart.functionResponse?.response?.['output']).toBe(
|
||||||
|
'file1.txt\nfile2.ts',
|
||||||
|
);
|
||||||
|
|
||||||
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.GOAL);
|
expect(scope.getTerminateMode()).toBe(SubagentTerminateMode.GOAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide specific tool error responses to the model', async () => {
|
|
||||||
const { config } = await createMockConfig();
|
|
||||||
const toolConfig: ToolConfig = { tools: ['failing_tool'] };
|
|
||||||
|
|
||||||
// Turn 1: Model calls the failing tool
|
|
||||||
// Turn 2: Model stops after receiving the error response
|
|
||||||
mockSendMessageStream.mockImplementation(
|
|
||||||
createMockStream([
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'call_fail',
|
|
||||||
name: 'failing_tool',
|
|
||||||
args: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'stop',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock the tool execution failure.
|
|
||||||
vi.mocked(executeToolCall).mockResolvedValue({
|
|
||||||
callId: 'call_fail',
|
|
||||||
responseParts: 'ERROR: Tool failed catastrophically', // This should be sent to the model
|
|
||||||
resultDisplay: 'Tool failed catastrophically',
|
|
||||||
error: new Error('Failure'),
|
|
||||||
errorType: ToolErrorType.INVALID_TOOL_PARAMS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const scope = await SubAgentScope.create(
|
|
||||||
'test-agent',
|
|
||||||
config,
|
|
||||||
promptConfig,
|
|
||||||
defaultModelConfig,
|
|
||||||
defaultRunConfig,
|
|
||||||
toolConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
await scope.runNonInteractive(new ContextState());
|
|
||||||
|
|
||||||
// The agent should send the specific error message from responseParts.
|
|
||||||
const secondCallArgs = mockSendMessageStream.mock.calls[1][0];
|
|
||||||
|
|
||||||
expect(secondCallArgs.message).toEqual([
|
|
||||||
{
|
|
||||||
text: 'ERROR: Tool failed catastrophically',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runNonInteractive - Termination and Recovery', () => {
|
describe('runNonInteractive - Termination and Recovery', () => {
|
||||||
|
|||||||
@@ -7,7 +7,15 @@
|
|||||||
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 '../core/turn.js';
|
import { ToolCallRequestInfo } from '../core/turn.js';
|
||||||
import { executeToolCall } from '../core/nonInteractiveToolExecutor.js';
|
import {
|
||||||
|
CoreToolScheduler,
|
||||||
|
ToolCall,
|
||||||
|
WaitingToolCall,
|
||||||
|
} from '../core/coreToolScheduler.js';
|
||||||
|
import type {
|
||||||
|
ToolConfirmationOutcome,
|
||||||
|
ToolCallConfirmationDetails,
|
||||||
|
} from '../tools/tools.js';
|
||||||
import { createContentGenerator } from '../core/contentGenerator.js';
|
import { createContentGenerator } from '../core/contentGenerator.js';
|
||||||
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
import { getEnvironmentContext } from '../utils/environmentContext.js';
|
||||||
import {
|
import {
|
||||||
@@ -227,61 +235,6 @@ export class SubAgentScope {
|
|||||||
eventEmitter?: SubAgentEventEmitter,
|
eventEmitter?: SubAgentEventEmitter,
|
||||||
hooks?: SubagentHooks,
|
hooks?: SubagentHooks,
|
||||||
): Promise<SubAgentScope> {
|
): Promise<SubAgentScope> {
|
||||||
// Validate tools for non-interactive use
|
|
||||||
if (toolConfig?.tools) {
|
|
||||||
const toolRegistry = runtimeContext.getToolRegistry();
|
|
||||||
|
|
||||||
for (const toolItem of toolConfig.tools) {
|
|
||||||
if (typeof toolItem !== 'string') {
|
|
||||||
continue; // Skip inline function declarations
|
|
||||||
}
|
|
||||||
const tool = toolRegistry.getTool(toolItem);
|
|
||||||
if (!tool) {
|
|
||||||
continue; // Skip unknown tools
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if tool has required parameters
|
|
||||||
const hasRequiredParams =
|
|
||||||
tool.schema?.parameters?.required &&
|
|
||||||
Array.isArray(tool.schema.parameters.required) &&
|
|
||||||
tool.schema.parameters.required.length > 0;
|
|
||||||
|
|
||||||
if (hasRequiredParams) {
|
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmationDetails) {
|
|
||||||
throw new Error(
|
|
||||||
`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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SubAgentScope(
|
return new SubAgentScope(
|
||||||
name,
|
name,
|
||||||
runtimeContext,
|
runtimeContext,
|
||||||
@@ -517,13 +470,6 @@ export class SubAgentScope {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as SubAgentErrorEvent);
|
} as SubAgentErrorEvent);
|
||||||
|
|
||||||
// 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 {
|
} finally {
|
||||||
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
||||||
@@ -531,7 +477,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.terminateMode,
|
terminateReason: this.terminateMode,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
rounds: summary.rounds,
|
rounds: summary.rounds,
|
||||||
totalDurationMs: summary.totalDurationMs,
|
totalDurationMs: summary.totalDurationMs,
|
||||||
@@ -587,134 +533,192 @@ export class SubAgentScope {
|
|||||||
): Promise<Content[]> {
|
): Promise<Content[]> {
|
||||||
const toolResponseParts: Part[] = [];
|
const toolResponseParts: Part[] = [];
|
||||||
|
|
||||||
for (const functionCall of functionCalls) {
|
// Build scheduler
|
||||||
const toolName = String(functionCall.name || 'unknown');
|
const responded = new Set<string>();
|
||||||
const callId = functionCall.id ?? `${functionCall.name}-${Date.now()}`;
|
let resolveBatch: (() => void) | null = null;
|
||||||
const requestInfo: ToolCallRequestInfo = {
|
const scheduler = new CoreToolScheduler({
|
||||||
|
toolRegistry: this.runtimeContext.getToolRegistry(),
|
||||||
|
outputUpdateHandler: undefined,
|
||||||
|
onAllToolCallsComplete: async (completedCalls) => {
|
||||||
|
for (const call of completedCalls) {
|
||||||
|
const toolName = call.request.name;
|
||||||
|
const duration = call.durationMs ?? 0;
|
||||||
|
const success = call.status === 'success';
|
||||||
|
const errorMessage =
|
||||||
|
call.status === 'error' || call.status === 'cancelled'
|
||||||
|
? call.response.error?.message
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Update aggregate stats
|
||||||
|
this.executionStats.totalToolCalls += 1;
|
||||||
|
if (success) {
|
||||||
|
this.executionStats.successfulToolCalls += 1;
|
||||||
|
} else {
|
||||||
|
this.executionStats.failedToolCalls += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-tool usage
|
||||||
|
const tu = this.toolUsage.get(toolName) || {
|
||||||
|
count: 0,
|
||||||
|
success: 0,
|
||||||
|
failure: 0,
|
||||||
|
totalDurationMs: 0,
|
||||||
|
averageDurationMs: 0,
|
||||||
|
};
|
||||||
|
tu.count += 1;
|
||||||
|
if (success) {
|
||||||
|
tu.success += 1;
|
||||||
|
} else {
|
||||||
|
tu.failure += 1;
|
||||||
|
tu.lastError = errorMessage || 'Unknown error';
|
||||||
|
}
|
||||||
|
tu.totalDurationMs = (tu.totalDurationMs || 0) + duration;
|
||||||
|
tu.averageDurationMs =
|
||||||
|
tu.count > 0 ? tu.totalDurationMs / tu.count : 0;
|
||||||
|
this.toolUsage.set(toolName, tu);
|
||||||
|
|
||||||
|
// Emit tool result event
|
||||||
|
this.eventEmitter?.emit(SubAgentEventType.TOOL_RESULT, {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
round: currentRound,
|
||||||
|
callId: call.request.callId,
|
||||||
|
name: toolName,
|
||||||
|
success,
|
||||||
|
error: errorMessage,
|
||||||
|
resultDisplay: call.response.resultDisplay
|
||||||
|
? typeof call.response.resultDisplay === 'string'
|
||||||
|
? call.response.resultDisplay
|
||||||
|
: JSON.stringify(call.response.resultDisplay)
|
||||||
|
: undefined,
|
||||||
|
durationMs: duration,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
} as SubAgentToolResultEvent);
|
||||||
|
|
||||||
|
// Update statistics service
|
||||||
|
this.stats.recordToolCall(
|
||||||
|
toolName,
|
||||||
|
success,
|
||||||
|
duration,
|
||||||
|
this.toolUsage.get(toolName)?.lastError,
|
||||||
|
);
|
||||||
|
|
||||||
|
// post-tool hook
|
||||||
|
await this.hooks?.postToolUse?.({
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
name: this.name,
|
||||||
|
toolName,
|
||||||
|
args: call.request.args,
|
||||||
|
success,
|
||||||
|
durationMs: duration,
|
||||||
|
errorMessage,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append response parts
|
||||||
|
const respParts = call.response.responseParts;
|
||||||
|
if (respParts) {
|
||||||
|
const parts = Array.isArray(respParts) ? respParts : [respParts];
|
||||||
|
for (const part of parts) {
|
||||||
|
if (typeof part === 'string') {
|
||||||
|
toolResponseParts.push({ text: part });
|
||||||
|
} else if (part) {
|
||||||
|
toolResponseParts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Signal that this batch is complete (all tools terminal)
|
||||||
|
resolveBatch?.();
|
||||||
|
},
|
||||||
|
onToolCallsUpdate: (calls: ToolCall[]) => {
|
||||||
|
for (const call of calls) {
|
||||||
|
if (call.status !== 'awaiting_approval') continue;
|
||||||
|
const waiting = call as WaitingToolCall;
|
||||||
|
|
||||||
|
// Emit approval request event for UI visibility
|
||||||
|
try {
|
||||||
|
const { confirmationDetails } = waiting;
|
||||||
|
const { onConfirm: _onConfirm, ...rest } = confirmationDetails;
|
||||||
|
this.eventEmitter?.emit(SubAgentEventType.TOOL_WAITING_APPROVAL, {
|
||||||
|
subagentId: this.subagentId,
|
||||||
|
round: currentRound,
|
||||||
|
callId: waiting.request.callId,
|
||||||
|
name: waiting.request.name,
|
||||||
|
description: this.getToolDescription(
|
||||||
|
waiting.request.name,
|
||||||
|
waiting.request.args,
|
||||||
|
),
|
||||||
|
confirmationDetails: rest,
|
||||||
|
respond: async (
|
||||||
|
outcome: ToolConfirmationOutcome,
|
||||||
|
payload?: Parameters<
|
||||||
|
ToolCallConfirmationDetails['onConfirm']
|
||||||
|
>[1],
|
||||||
|
) => {
|
||||||
|
if (responded.has(waiting.request.callId)) return;
|
||||||
|
responded.add(waiting.request.callId);
|
||||||
|
await waiting.confirmationDetails.onConfirm(outcome, payload);
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore UI event emission failures
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI now renders inline confirmation via task tool live output.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPreferredEditor: () => undefined,
|
||||||
|
config: this.runtimeContext,
|
||||||
|
onEditorClose: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare requests and emit TOOL_CALL events
|
||||||
|
const requests: ToolCallRequestInfo[] = functionCalls.map((fc) => {
|
||||||
|
const toolName = String(fc.name || 'unknown');
|
||||||
|
const callId = fc.id ?? `${fc.name}-${Date.now()}`;
|
||||||
|
const args = (fc.args ?? {}) as Record<string, unknown>;
|
||||||
|
const request: ToolCallRequestInfo = {
|
||||||
callId,
|
callId,
|
||||||
name: functionCall.name as string,
|
name: toolName,
|
||||||
args: (functionCall.args ?? {}) as Record<string, unknown>,
|
args,
|
||||||
isClientInitiated: true,
|
isClientInitiated: true,
|
||||||
prompt_id: promptId,
|
prompt_id: promptId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get tool description before execution
|
const description = this.getToolDescription(toolName, args);
|
||||||
const description = this.getToolDescription(toolName, requestInfo.args);
|
|
||||||
|
|
||||||
// Emit tool call event BEFORE execution
|
|
||||||
this.eventEmitter?.emit(SubAgentEventType.TOOL_CALL, {
|
this.eventEmitter?.emit(SubAgentEventType.TOOL_CALL, {
|
||||||
subagentId: this.subagentId,
|
subagentId: this.subagentId,
|
||||||
round: currentRound,
|
round: currentRound,
|
||||||
callId,
|
callId,
|
||||||
name: toolName,
|
name: toolName,
|
||||||
args: requestInfo.args,
|
args,
|
||||||
description,
|
description,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
} as SubAgentToolCallEvent);
|
} as SubAgentToolCallEvent);
|
||||||
|
|
||||||
// Execute tools with timing and hooks
|
// pre-tool hook
|
||||||
const start = Date.now();
|
void this.hooks?.preToolUse?.({
|
||||||
await this.hooks?.preToolUse?.({
|
|
||||||
subagentId: this.subagentId,
|
subagentId: this.subagentId,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
toolName,
|
toolName,
|
||||||
args: requestInfo.args,
|
args,
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
const toolResponse = await executeToolCall(
|
|
||||||
this.runtimeContext,
|
|
||||||
requestInfo,
|
|
||||||
abortController.signal,
|
|
||||||
);
|
|
||||||
const duration = Date.now() - start;
|
|
||||||
// Update tool call stats
|
|
||||||
this.executionStats.totalToolCalls += 1;
|
|
||||||
if (toolResponse.error) {
|
|
||||||
this.executionStats.failedToolCalls += 1;
|
|
||||||
} else {
|
|
||||||
this.executionStats.successfulToolCalls += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
tu.lastError = 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 result event
|
|
||||||
this.eventEmitter?.emit(SubAgentEventType.TOOL_RESULT, {
|
|
||||||
subagentId: this.subagentId,
|
|
||||||
round: currentRound,
|
|
||||||
callId,
|
|
||||||
name: toolName,
|
|
||||||
success: !toolResponse?.error,
|
|
||||||
error: toolResponse?.error?.message,
|
|
||||||
resultDisplay: toolResponse?.resultDisplay
|
|
||||||
? typeof toolResponse.resultDisplay === 'string'
|
|
||||||
? toolResponse.resultDisplay
|
|
||||||
: JSON.stringify(toolResponse.resultDisplay)
|
|
||||||
: undefined,
|
|
||||||
durationMs: duration,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
} as SubAgentToolResultEvent);
|
|
||||||
|
|
||||||
// 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?.message,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (toolResponse.error) {
|
return request;
|
||||||
console.error(
|
});
|
||||||
`Error executing tool ${functionCall.name}: ${toolResponse.error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolResponse.responseParts) {
|
if (requests.length > 0) {
|
||||||
const parts = Array.isArray(toolResponse.responseParts)
|
// Create a per-batch completion promise, resolve when onAllToolCallsComplete fires
|
||||||
? toolResponse.responseParts
|
const batchDone = new Promise<void>((resolve) => {
|
||||||
: [toolResponse.responseParts];
|
resolveBatch = () => {
|
||||||
for (const part of parts) {
|
resolve();
|
||||||
if (typeof part === 'string') {
|
resolveBatch = null;
|
||||||
toolResponseParts.push({ text: part });
|
};
|
||||||
} else if (part) {
|
});
|
||||||
toolResponseParts.push(part);
|
await scheduler.schedule(requests, abortController.signal);
|
||||||
}
|
await batchDone; // Wait for approvals + execution to finish
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If all tool calls failed, inform the model so it can re-evaluate.
|
// If all tool calls failed, inform the model so it can re-evaluate.
|
||||||
if (functionCalls.length > 0 && toolResponseParts.length === 0) {
|
if (functionCalls.length > 0 && toolResponseParts.length === 0) {
|
||||||
|
|||||||
@@ -395,9 +395,6 @@ describe('TaskTool', () => {
|
|||||||
const display = result.returnDisplay as TaskResultDisplay;
|
const display = result.returnDisplay as TaskResultDisplay;
|
||||||
|
|
||||||
expect(display.status).toBe('failed');
|
expect(display.status).toBe('failed');
|
||||||
expect(display.result ?? '').toContain(
|
|
||||||
'Failed to run subagent: Creation failed',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute subagent without live output callback', async () => {
|
it('should execute subagent without live output callback', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import {
|
|||||||
ToolResultDisplay,
|
ToolResultDisplay,
|
||||||
TaskResultDisplay,
|
TaskResultDisplay,
|
||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
|
import { ToolConfirmationOutcome } from './tools.js';
|
||||||
|
import type {
|
||||||
|
ToolCallConfirmationDetails,
|
||||||
|
ToolConfirmationPayload,
|
||||||
|
} 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, SubagentTerminateMode } from '../subagents/types.js';
|
import { SubagentConfig, SubagentTerminateMode } from '../subagents/types.js';
|
||||||
@@ -23,6 +28,7 @@ import {
|
|||||||
SubAgentFinishEvent,
|
SubAgentFinishEvent,
|
||||||
SubAgentEventType,
|
SubAgentEventType,
|
||||||
SubAgentErrorEvent,
|
SubAgentErrorEvent,
|
||||||
|
SubAgentApprovalRequestEvent,
|
||||||
} from '../subagents/subagent-events.js';
|
} from '../subagents/subagent-events.js';
|
||||||
|
|
||||||
export interface TaskParams {
|
export interface TaskParams {
|
||||||
@@ -338,9 +344,8 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
const event = args[0] as SubAgentFinishEvent;
|
const event = args[0] as SubAgentFinishEvent;
|
||||||
this.updateDisplay(
|
this.updateDisplay(
|
||||||
{
|
{
|
||||||
status: event.terminate_reason === 'GOAL' ? 'completed' : 'failed',
|
status: event.terminateReason === 'GOAL' ? 'completed' : 'failed',
|
||||||
terminateReason: event.terminate_reason,
|
terminateReason: event.terminateReason,
|
||||||
// Keep toolCalls data for final display
|
|
||||||
},
|
},
|
||||||
updateOutput,
|
updateOutput,
|
||||||
);
|
);
|
||||||
@@ -356,6 +361,85 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
updateOutput,
|
updateOutput,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Indicate when a tool call is waiting for approval
|
||||||
|
this.eventEmitter.on(
|
||||||
|
SubAgentEventType.TOOL_WAITING_APPROVAL,
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
const event = args[0] as SubAgentApprovalRequestEvent;
|
||||||
|
const idx = this.currentToolCalls!.findIndex(
|
||||||
|
(c) => c.callId === event.callId,
|
||||||
|
);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.currentToolCalls![idx] = {
|
||||||
|
...this.currentToolCalls![idx],
|
||||||
|
status: 'awaiting_approval',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.currentToolCalls!.push({
|
||||||
|
callId: event.callId,
|
||||||
|
name: event.name,
|
||||||
|
status: 'awaiting_approval',
|
||||||
|
description: event.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bridge scheduler confirmation details to UI inline prompt
|
||||||
|
const details: ToolCallConfirmationDetails = {
|
||||||
|
...(event.confirmationDetails as Omit<
|
||||||
|
ToolCallConfirmationDetails,
|
||||||
|
'onConfirm'
|
||||||
|
>),
|
||||||
|
onConfirm: async (
|
||||||
|
outcome: ToolConfirmationOutcome,
|
||||||
|
payload?: ToolConfirmationPayload,
|
||||||
|
) => {
|
||||||
|
// Clear the inline prompt immediately
|
||||||
|
// and optimistically mark the tool as executing for proceed outcomes.
|
||||||
|
const proceedOutcomes = new Set<ToolConfirmationOutcome>([
|
||||||
|
ToolConfirmationOutcome.ProceedOnce,
|
||||||
|
ToolConfirmationOutcome.ProceedAlways,
|
||||||
|
ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||||
|
ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (proceedOutcomes.has(outcome)) {
|
||||||
|
const idx2 = this.currentToolCalls!.findIndex(
|
||||||
|
(c) => c.callId === event.callId,
|
||||||
|
);
|
||||||
|
if (idx2 >= 0) {
|
||||||
|
this.currentToolCalls![idx2] = {
|
||||||
|
...this.currentToolCalls![idx2],
|
||||||
|
status: 'executing',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.updateDisplay(
|
||||||
|
{
|
||||||
|
toolCalls: [...this.currentToolCalls!],
|
||||||
|
pendingConfirmation: undefined,
|
||||||
|
},
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.updateDisplay(
|
||||||
|
{ pendingConfirmation: undefined },
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await event.respond(outcome, payload);
|
||||||
|
},
|
||||||
|
} as ToolCallConfirmationDetails;
|
||||||
|
|
||||||
|
this.updateDisplay(
|
||||||
|
{
|
||||||
|
toolCalls: [...this.currentToolCalls!],
|
||||||
|
pendingConfirmation: details,
|
||||||
|
},
|
||||||
|
updateOutput,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@@ -384,9 +468,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
taskDescription: this.params.description,
|
taskDescription: this.params.description,
|
||||||
taskPrompt: this.params.prompt,
|
taskPrompt: this.params.prompt,
|
||||||
status: 'failed' as const,
|
status: 'failed' as const,
|
||||||
terminateReason: 'ERROR',
|
terminateReason: `Subagent "${this.params.subagent_type}" not found`,
|
||||||
result: `Subagent "${this.params.subagent_type}" not found`,
|
|
||||||
subagentColor: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -427,16 +509,15 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
|
|
||||||
// Get the results
|
// Get the results
|
||||||
const finalText = subagentScope.getFinalText();
|
const finalText = subagentScope.getFinalText();
|
||||||
const terminateReason = subagentScope.getTerminateMode();
|
const terminateMode = subagentScope.getTerminateMode();
|
||||||
const success = terminateReason === SubagentTerminateMode.GOAL;
|
const success = terminateMode === SubagentTerminateMode.GOAL;
|
||||||
const executionSummary = subagentScope.getExecutionSummary();
|
const executionSummary = subagentScope.getExecutionSummary();
|
||||||
|
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
this.updateDisplay(
|
this.updateDisplay(
|
||||||
{
|
{
|
||||||
status: 'cancelled',
|
status: 'cancelled',
|
||||||
terminateReason: 'CANCELLED',
|
terminateReason: 'Task was cancelled by user',
|
||||||
result: finalText || 'Task was cancelled by user',
|
|
||||||
executionSummary,
|
executionSummary,
|
||||||
},
|
},
|
||||||
updateOutput,
|
updateOutput,
|
||||||
@@ -445,7 +526,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
this.updateDisplay(
|
this.updateDisplay(
|
||||||
{
|
{
|
||||||
status: success ? 'completed' : 'failed',
|
status: success ? 'completed' : 'failed',
|
||||||
terminateReason,
|
terminateReason: terminateMode,
|
||||||
result: finalText,
|
result: finalText,
|
||||||
executionSummary,
|
executionSummary,
|
||||||
},
|
},
|
||||||
@@ -465,8 +546,7 @@ class TaskToolInvocation extends BaseToolInvocation<TaskParams, ToolResult> {
|
|||||||
const errorDisplay: TaskResultDisplay = {
|
const errorDisplay: TaskResultDisplay = {
|
||||||
...this.currentDisplay!,
|
...this.currentDisplay!,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
terminateReason: 'ERROR',
|
terminateReason: `Failed to run subagent: ${errorMessage}`,
|
||||||
result: `Failed to run subagent: ${errorMessage}`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -432,10 +432,15 @@ export interface TaskResultDisplay {
|
|||||||
terminateReason?: string;
|
terminateReason?: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
executionSummary?: SubagentStatsSummary;
|
executionSummary?: SubagentStatsSummary;
|
||||||
|
|
||||||
|
// If the subagent is awaiting approval for a tool call,
|
||||||
|
// this contains the confirmation details for inline UI rendering.
|
||||||
|
pendingConfirmation?: ToolCallConfirmationDetails;
|
||||||
|
|
||||||
toolCalls?: Array<{
|
toolCalls?: Array<{
|
||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'executing' | 'success' | 'failed';
|
status: 'executing' | 'awaiting_approval' | 'success' | 'failed';
|
||||||
error?: string;
|
error?: string;
|
||||||
args?: Record<string, unknown>;
|
args?: Record<string, unknown>;
|
||||||
result?: string;
|
result?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user