fix(core): accurately reflect subagent tool failure in UI (#23187)

This commit is contained in:
Abhi
2026-03-23 21:56:00 -04:00
committed by GitHub
parent 89ca78837e
commit a1f9af3fa7
8 changed files with 91 additions and 5 deletions

View File

@@ -182,4 +182,25 @@ describe('<SubagentProgressDisplay />', () => {
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders error tool status correctly', async () => {
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [
{
id: '7',
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'error',
},
],
};
const { lastFrame } = await render(
<SubagentProgressDisplay progress={progress} terminalWidth={80} />,
);
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -40,6 +40,13 @@ exports[`<SubagentProgressDisplay /> > renders correctly with file_path 1`] = `
"
`;
exports[`<SubagentProgressDisplay /> > renders error tool status correctly 1`] = `
"Running subagent TestAgent...
x run_shell_command echo hello
"
`;
exports[`<SubagentProgressDisplay /> > renders thought bubbles correctly 1`] = `
"Running subagent TestAgent...

View File

@@ -30,6 +30,7 @@ import {
type SubagentActivityEvent,
type SubagentProgress,
type SubagentActivityItem,
isToolActivityError,
} from '../types.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import {
@@ -210,8 +211,9 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
const callId = activity.data['id']
? String(activity.data['id'])
: undefined;
// Find the tool call by ID
// Find the tool call by ID
const data = activity.data['data'];
const isError = isToolActivityError(data);
for (let i = recentActivity.length - 1; i >= 0; i--) {
if (
recentActivity[i].type === 'tool_call' &&
@@ -219,7 +221,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
recentActivity[i].id === callId &&
recentActivity[i].status === 'running'
) {
recentActivity[i].status = 'completed';
recentActivity[i].status = isError ? 'error' : 'completed';
updated = true;
break;
}

View File

@@ -1240,6 +1240,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
name: toolName,
id: call.request.callId,
output: call.response.resultDisplay,
data: call.response.data,
});
} else if (call.status === 'error') {
this.emitActivity('ERROR', {

View File

@@ -338,6 +338,42 @@ describe('LocalSubagentInvocation', () => {
);
});
it('should mark tool call as error when TOOL_CALL_END contains isError: true', async () => {
mockExecutorInstance.run.mockImplementation(async () => {
const onActivity = MockLocalAgentExecutor.create.mock.calls[0][2];
if (onActivity) {
onActivity({
isSubagentActivityEvent: true,
agentName: 'MockAgent',
type: 'TOOL_CALL_START',
data: { name: 'ls', args: {}, callId: 'call1' },
} as SubagentActivityEvent);
onActivity({
isSubagentActivityEvent: true,
agentName: 'MockAgent',
type: 'TOOL_CALL_END',
data: { name: 'ls', id: 'call1', data: { isError: true } },
} as SubagentActivityEvent);
}
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
expect(updateOutput).toHaveBeenCalled();
const lastCall = updateOutput.mock.calls[
updateOutput.mock.calls.length - 1
][0] as SubagentProgress;
expect(lastCall.recentActivity).toContainEqual(
expect.objectContaining({
type: 'tool_call',
content: 'ls',
status: 'error',
}),
);
});
it('should reflect tool rejections in the activity stream as cancelled but not abort the agent', async () => {
mockExecutorInstance.run.mockImplementation(async () => {
const onActivity = MockLocalAgentExecutor.create.mock.calls[0][2];

View File

@@ -21,6 +21,7 @@ import {
SubagentActivityErrorType,
SUBAGENT_REJECTED_ERROR_PREFIX,
SUBAGENT_CANCELLED_ERROR_MESSAGE,
isToolActivityError,
} from './types.js';
import { randomUUID } from 'node:crypto';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
@@ -166,14 +167,16 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
}
case 'TOOL_CALL_END': {
const name = String(activity.data['name']);
// Find the last running tool call with this name
const data = activity.data['data'];
const isError = isToolActivityError(data);
for (let i = recentActivity.length - 1; i >= 0; i--) {
if (
recentActivity[i].type === 'tool_call' &&
recentActivity[i].content === name &&
recentActivity[i].status === 'running'
) {
recentActivity[i].status = 'completed';
recentActivity[i].status = isError ? 'error' : 'completed';
updated = true;
break;
}

View File

@@ -112,6 +112,18 @@ export function isSubagentProgress(obj: unknown): obj is SubagentProgress {
);
}
/**
* Checks if the tool call data indicates an error.
*/
export function isToolActivityError(data: unknown): boolean {
return (
data !== null &&
typeof data === 'object' &&
'isError' in data &&
data.isError === true
);
}
/**
* The base definition for an agent.
* @template TOutput The specific Zod schema for the agent's final output object.

View File

@@ -381,6 +381,10 @@ export class ShellToolInvocation extends BaseToolInvocation<
if (result.exitCode !== null && result.exitCode !== 0) {
llmContentParts.push(`Exit Code: ${result.exitCode}`);
data = {
exitCode: result.exitCode,
isError: true,
};
}
if (result.signal) {