mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-25 13:30:45 -07:00
fix(core): accurately reflect subagent tool failure in UI (#23187)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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...
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user