refactor(core): introduce SubagentState enum for progress (#26934)

This commit is contained in:
Adam Weidman
2026-05-12 14:58:25 -04:00
committed by GitHub
parent c4973d01da
commit c987b99394
15 changed files with 172 additions and 121 deletions
@@ -6,7 +6,11 @@
import { waitFor } from '../../../test-utils/async.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
import { Kind, CoreToolCallStatus } from '@google/gemini-cli-core';
import {
Kind,
CoreToolCallStatus,
SubagentState,
} from '@google/gemini-cli-core';
import type { IndividualToolCallDisplay } from '../../types.js';
import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
@@ -27,12 +31,12 @@ describe('<SubagentGroupDisplay />', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'api-monitor',
state: 'running',
state: SubagentState.RUNNING,
recentActivity: [
{
id: 'act-1',
type: 'tool_call',
status: 'running',
status: SubagentState.RUNNING,
content: '',
displayName: 'Action Required',
description: 'Verify server is running',
@@ -50,13 +54,13 @@ describe('<SubagentGroupDisplay />', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'db-manager',
state: 'completed',
state: SubagentState.COMPLETED,
result: 'Database schema validated',
recentActivity: [
{
id: 'act-2',
type: 'thought',
status: 'completed',
status: SubagentState.COMPLETED,
content: 'Database schema validated',
},
],
@@ -13,6 +13,7 @@ import {
isSubagentProgress,
checkExhaustive,
type SubagentActivityItem,
SubagentState,
} from '@google/gemini-cli-core';
import {
SubagentProgressDisplay,
@@ -66,13 +67,13 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
const singleAgent = toolCalls[0].resultDisplay;
if (isSubagentProgress(singleAgent)) {
switch (singleAgent.state) {
case 'completed':
case SubagentState.COMPLETED:
headerText = 'Agent Completed';
break;
case 'cancelled':
case SubagentState.CANCELLED:
headerText = 'Agent Cancelled';
break;
case 'error':
case SubagentState.ERROR:
headerText = 'Agent Error';
break;
default:
@@ -88,8 +89,8 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
for (const tc of toolCalls) {
const progress = tc.resultDisplay;
if (isSubagentProgress(progress)) {
if (progress.state === 'completed') completedCount++;
else if (progress.state === 'running') runningCount++;
if (progress.state === SubagentState.COMPLETED) completedCount++;
else if (progress.state === SubagentState.RUNNING) runningCount++;
} else {
// It hasn't emitted progress yet, but it is "running"
runningCount++;
@@ -200,7 +201,7 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
let content = 'Starting...';
let formattedArgs: string | undefined;
if (progress.state === 'completed') {
if (progress.state === SubagentState.COMPLETED) {
if (
progress.terminateReason &&
progress.terminateReason !== 'GOAL'
@@ -223,18 +224,18 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
}
const displayArgs =
progress.state === 'completed' ? '' : formattedArgs;
progress.state === SubagentState.COMPLETED ? '' : formattedArgs;
const renderStatusIcon = () => {
const state = progress.state ?? 'running';
const state = progress.state ?? SubagentState.RUNNING;
switch (state) {
case 'running':
case SubagentState.RUNNING:
return <Text color={theme.text.primary}>!</Text>;
case 'completed':
case SubagentState.COMPLETED:
return <Text color={theme.status.success}></Text>;
case 'cancelled':
case SubagentState.CANCELLED:
return <Text color={theme.status.warning}></Text>;
case 'error':
case SubagentState.ERROR:
return <Text color={theme.status.error}></Text>;
default:
return checkExhaustive(state);
@@ -8,6 +8,7 @@ import { describe, it, expect } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { SubagentHistoryMessage } from './SubagentHistoryMessage.js';
import type { HistoryItemSubagent } from '../../types.js';
import { SubagentState } from '@google/gemini-cli-core';
describe('SubagentHistoryMessage', () => {
const mockItem: HistoryItemSubagent = {
@@ -18,19 +19,19 @@ describe('SubagentHistoryMessage', () => {
id: '1',
type: 'thought',
content: 'Thinking about the problem',
status: 'completed',
status: SubagentState.COMPLETED,
},
{
id: '2',
type: 'tool_call',
content: 'Calling search_web',
status: 'running',
status: SubagentState.RUNNING,
},
{
id: '3',
type: 'tool_call',
content: 'Calling read_file fail',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -6,7 +6,7 @@
import { render, cleanup } from '../../../test-utils/render.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';
import type { SubagentProgress } from '@google/gemini-cli-core';
import { type SubagentProgress, SubagentState } from '@google/gemini-cli-core';
import { describe, it, expect, vi, afterEach } from 'vitest';
describe('<SubagentProgressDisplay />', () => {
@@ -25,7 +25,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello", "description": "Say hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -48,7 +48,7 @@ describe('<SubagentProgressDisplay />', () => {
displayName: 'RunShellCommand',
description: 'Executing echo hello',
args: '{"command": "echo hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -69,7 +69,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -90,7 +90,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'write_file',
args: '{"file_path": "/tmp/test.txt", "content": "foo"}',
status: 'completed',
status: SubagentState.COMPLETED,
},
],
};
@@ -113,7 +113,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: JSON.stringify({ description: longDesc }),
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -133,7 +133,7 @@ describe('<SubagentProgressDisplay />', () => {
id: '5',
type: 'thought',
content: 'Thinking about life',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -149,7 +149,7 @@ describe('<SubagentProgressDisplay />', () => {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [],
state: 'cancelled',
state: SubagentState.CANCELLED,
};
const { lastFrame } = await render(
@@ -167,7 +167,7 @@ describe('<SubagentProgressDisplay />', () => {
id: '6',
type: 'thought',
content: 'Request cancelled.',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -188,7 +188,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -9,9 +9,10 @@ import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import Spinner from 'ink-spinner';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import type {
SubagentProgress,
SubagentActivityItem,
import {
type SubagentProgress,
type SubagentActivityItem,
SubagentState,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
@@ -62,13 +63,13 @@ export const SubagentProgressDisplay: React.FC<
let headerText: string | undefined;
let headerColor = theme.text.secondary;
if (progress.state === 'cancelled') {
if (progress.state === SubagentState.CANCELLED) {
headerText = `Subagent ${progress.agentName} was cancelled.`;
headerColor = theme.status.warning;
} else if (progress.state === 'error') {
} else if (progress.state === SubagentState.ERROR) {
headerText = `Subagent ${progress.agentName} failed.`;
headerColor = theme.status.error;
} else if (progress.state === 'completed') {
} else if (progress.state === SubagentState.COMPLETED) {
headerText = `Subagent ${progress.agentName} completed.`;
headerColor = theme.status.success;
} else {
@@ -107,13 +108,13 @@ export const SubagentProgressDisplay: React.FC<
);
} else if (item.type === 'tool_call') {
const statusSymbol =
item.status === 'running' ? (
item.status === SubagentState.RUNNING ? (
<Spinner type="dots" />
) : item.status === 'completed' ? (
) : item.status === SubagentState.COMPLETED ? (
<Text color={theme.status.success}>
{TOOL_STATUS.SUCCESS}
</Text>
) : item.status === 'cancelled' ? (
) : item.status === SubagentState.CANCELLED ? (
<Text color={theme.status.warning} bold>
{TOOL_STATUS.CANCELED}
</Text>
@@ -135,7 +136,7 @@ export const SubagentProgressDisplay: React.FC<
<Text
bold
color={theme.text.primary}
strikethrough={item.status === 'cancelled'}
strikethrough={item.status === SubagentState.CANCELLED}
>
{item.displayName || item.content}
</Text>
@@ -144,7 +145,9 @@ export const SubagentProgressDisplay: React.FC<
<Text
color={theme.text.secondary}
wrap="truncate"
strikethrough={item.status === 'cancelled'}
strikethrough={
item.status === SubagentState.CANCELLED
}
>
{displayArgs}
</Text>
@@ -170,7 +173,7 @@ export const SubagentProgressDisplay: React.FC<
)}
<MarkdownDisplay
text={safeJsonToMarkdown(progress.result)}
isPending={progress.state !== 'completed'}
isPending={progress.state !== SubagentState.COMPLETED}
terminalWidth={terminalWidth}
/>
</Box>
@@ -13,6 +13,7 @@ import {
ApprovalMode,
WRITE_FILE_DISPLAY_NAME,
Kind,
SubagentState,
} from '@google/gemini-cli-core';
import os from 'node:os';
import { createMockSettings } from '../../../test-utils/settings.js';
@@ -76,7 +77,7 @@ describe('ToolGroupMessage Regression Tests', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'TestAgent',
state: 'running',
state: SubagentState.RUNNING,
recentActivity: [],
},
}),
@@ -112,7 +113,7 @@ describe('ToolGroupMessage Regression Tests', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'TestAgent',
state: 'completed',
state: SubagentState.COMPLETED,
recentActivity: [],
},
}),
@@ -21,6 +21,7 @@ import {
ROOT_SCHEDULER_ID,
CoreToolCallStatus,
type WaitingToolCall,
SubagentState,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
@@ -630,7 +631,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking...',
status: 'running',
status: SubagentState.RUNNING,
},
});
});
@@ -648,7 +649,7 @@ describe('useToolScheduler', () => {
id: '2',
type: 'tool_call',
content: 'Calling tool',
status: 'completed',
status: SubagentState.COMPLETED,
},
});
});
@@ -697,7 +698,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking...',
status: 'running',
status: SubagentState.RUNNING,
},
});
});
@@ -716,7 +717,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking... Done!',
status: 'completed',
status: SubagentState.COMPLETED,
},
});
});
@@ -726,6 +727,8 @@ describe('useToolScheduler', () => {
expect(result.current[0][0].subagentHistory![0].content).toBe(
'Thinking... Done!',
);
expect(result.current[0][0].subagentHistory![0].status).toBe('completed');
expect(result.current[0][0].subagentHistory![0].status).toBe(
SubagentState.COMPLETED,
);
});
});
+4 -4
View File
@@ -16,7 +16,7 @@ import type {
AgentInterface,
} from '@a2a-js/sdk';
import type { SendMessageResult } from './a2a-client-manager.js';
import type { SubagentActivityItem } from './types.js';
import { type SubagentActivityItem, SubagentState } from './types.js';
export const AUTH_REQUIRED_MSG = `[Authorization Required] The agent has indicated it requires authorization to proceed. Please follow the agent's instructions.`;
@@ -143,7 +143,7 @@ export class A2AResultReassembler {
id: 'auth-required',
type: 'thought',
content: AUTH_REQUIRED_MSG,
status: 'running',
status: SubagentState.RUNNING,
});
}
@@ -152,7 +152,7 @@ export class A2AResultReassembler {
id: `msg-${index}`,
type: 'thought',
content: msg.trim(),
status: 'completed',
status: SubagentState.COMPLETED,
});
});
@@ -161,7 +161,7 @@ export class A2AResultReassembler {
id: 'pending',
type: 'thought',
content: 'Working...',
status: 'running',
status: SubagentState.RUNNING,
});
}
@@ -32,6 +32,7 @@ import {
type SubagentActivityItem,
AgentTerminateMode,
isToolActivityError,
SubagentState,
} from '../types.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import { createBrowserAgentDefinition } from './browserAgentFactory.js';
@@ -123,7 +124,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.agentName,
recentActivity: [],
state: 'running',
state: SubagentState.RUNNING,
};
updateOutput(initialProgress);
}
@@ -137,7 +138,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
id: randomUUID(),
type: 'thought',
content: sanitizedMsg,
status: 'completed',
status: SubagentState.COMPLETED,
});
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
@@ -146,7 +147,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.agentName,
recentActivity: [...recentActivity],
state: 'running',
state: SubagentState.RUNNING,
} as SubagentProgress);
}
: undefined;
@@ -175,7 +176,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
if (
lastItem &&
lastItem.type === 'thought' &&
lastItem.status === 'running'
lastItem.status === SubagentState.RUNNING
) {
lastItem.content = sanitizeThoughtContent(text);
} else {
@@ -183,7 +184,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
id: randomUUID(),
type: 'thought',
content: sanitizeThoughtContent(text),
status: 'running',
status: SubagentState.RUNNING,
});
}
updated = true;
@@ -210,7 +211,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
displayName,
description,
args,
status: 'running',
status: SubagentState.RUNNING,
});
updated = true;
break;
@@ -227,9 +228,11 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
recentActivity[i].type === 'tool_call' &&
callId != null &&
recentActivity[i].id === callId &&
recentActivity[i].status === 'running'
recentActivity[i].status === SubagentState.RUNNING
) {
recentActivity[i].status = isError ? 'error' : 'completed';
recentActivity[i].status = isError
? SubagentState.ERROR
: SubagentState.COMPLETED;
updated = true;
break;
}
@@ -242,7 +245,9 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
const callId = activity.data['callId']
? String(activity.data['callId'])
: undefined;
const newStatus = isCancellation ? 'cancelled' : 'error';
const newStatus = isCancellation
? SubagentState.CANCELLED
: SubagentState.ERROR;
if (callId) {
// Mark the specific tool as error/cancelled
@@ -250,7 +255,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
if (
recentActivity[i].type === 'tool_call' &&
recentActivity[i].id === callId &&
recentActivity[i].status === 'running'
recentActivity[i].status === SubagentState.RUNNING
) {
recentActivity[i].status = newStatus;
updated = true;
@@ -260,7 +265,10 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
} else {
// No specific tool — mark ALL running tool_call items
for (const item of recentActivity) {
if (item.type === 'tool_call' && item.status === 'running') {
if (
item.type === 'tool_call' &&
item.status === SubagentState.RUNNING
) {
item.status = newStatus;
updated = true;
}
@@ -293,7 +301,7 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.agentName,
recentActivity: [...recentActivity],
state: 'running',
state: SubagentState.RUNNING,
};
updateOutput(progress);
}
@@ -330,13 +338,13 @@ ${output.result}`;
// GOAL = agent completed its task normally.
// ABORTED = user cancelled.
// Others (ERROR, MAX_TURNS, ERROR_NO_COMPLETE_TASK_CALL) = error.
let progressState: SubagentProgress['state'];
let progressState: SubagentState;
if (output.terminate_reason === AgentTerminateMode.ABORTED) {
progressState = 'cancelled';
progressState = SubagentState.CANCELLED;
} else if (output.terminate_reason === AgentTerminateMode.GOAL) {
progressState = 'completed';
progressState = SubagentState.COMPLETED;
} else {
progressState = 'error';
progressState = SubagentState.ERROR;
}
const progress: SubagentProgress = {
@@ -366,8 +374,8 @@ ${output.result}`;
// Mark any running items as error/cancelled
for (const item of recentActivity) {
if (item.status === 'running') {
item.status = isAbort ? 'cancelled' : 'error';
if (item.status === SubagentState.RUNNING) {
item.status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR;
}
}
@@ -375,7 +383,7 @@ ${output.result}`;
isSubagentProgress: true,
agentName: this.agentName,
recentActivity: [...recentActivity],
state: isAbort ? 'cancelled' : 'error',
state: isAbort ? SubagentState.CANCELLED : SubagentState.ERROR,
};
if (updateOutput) {
@@ -21,6 +21,7 @@ import {
type SubagentProgress,
SubagentActivityErrorType,
SUBAGENT_REJECTED_ERROR_PREFIX,
SubagentState,
} from './types.js';
import { LocalSubagentInvocation } from './local-invocation.js';
import { LocalAgentExecutor } from './local-executor.js';
@@ -215,7 +216,7 @@ describe('LocalSubagentInvocation', () => {
]);
const display = result.returnDisplay as SubagentProgress;
expect(display.isSubagentProgress).toBe(true);
expect(display.state).toBe('completed');
expect(display.state).toBe(SubagentState.COMPLETED);
expect(display.result).toBe('Analysis complete.');
expect(display.terminateReason).toBe(AgentTerminateMode.GOAL);
});
@@ -234,7 +235,7 @@ describe('LocalSubagentInvocation', () => {
const display = result.returnDisplay as SubagentProgress;
expect(display.isSubagentProgress).toBe(true);
expect(display.state).toBe('completed');
expect(display.state).toBe(SubagentState.COMPLETED);
expect(display.result).toBe('Partial progress...');
expect(display.terminateReason).toBe(AgentTerminateMode.TIMEOUT);
});
@@ -340,7 +341,7 @@ describe('LocalSubagentInvocation', () => {
expect.objectContaining({
type: 'thought',
content: 'Error: Failed',
status: 'error',
status: SubagentState.ERROR,
}),
);
});
@@ -376,7 +377,7 @@ describe('LocalSubagentInvocation', () => {
expect.objectContaining({
type: 'tool_call',
content: 'ls',
status: 'error',
status: SubagentState.ERROR,
}),
);
});
@@ -418,7 +419,7 @@ describe('LocalSubagentInvocation', () => {
expect.objectContaining({
type: 'tool_call',
content: 'ls',
status: 'cancelled',
status: SubagentState.CANCELLED,
}),
);
});
@@ -443,7 +444,7 @@ describe('LocalSubagentInvocation', () => {
expect(result.error).toBeUndefined();
const display = result.returnDisplay as SubagentProgress;
expect(display.isSubagentProgress).toBe(true);
expect(display.state).toBe('completed');
expect(display.state).toBe(SubagentState.COMPLETED);
expect(display.result).toBe('Done');
});
@@ -466,7 +467,7 @@ describe('LocalSubagentInvocation', () => {
expect.objectContaining({
type: 'thought',
content: `Error: ${error.message}`,
status: 'error',
status: SubagentState.ERROR,
}),
);
});
@@ -488,7 +489,7 @@ describe('LocalSubagentInvocation', () => {
expect(display.recentActivity).toContainEqual(
expect.objectContaining({
content: `Error: ${creationError.message}`,
status: 'error',
status: SubagentState.ERROR,
}),
);
});
+25 -19
View File
@@ -23,6 +23,7 @@ import {
SUBAGENT_REJECTED_ERROR_PREFIX,
SUBAGENT_CANCELLED_ERROR_MESSAGE,
isToolActivityError,
SubagentState,
} from './types.js';
import { randomUUID } from 'node:crypto';
import type { z } from 'zod';
@@ -117,7 +118,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.definition.name,
recentActivity: [],
state: 'running',
state: SubagentState.RUNNING,
};
updateOutput(initialProgress);
}
@@ -137,7 +138,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
if (
lastItem &&
lastItem.type === 'thought' &&
lastItem.status === 'running'
lastItem.status === SubagentState.RUNNING
) {
lastItem.content = sanitizeThoughtContent(text);
} else {
@@ -145,7 +146,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
id: randomUUID(),
type: 'thought',
content: sanitizeThoughtContent(text),
status: 'running',
status: SubagentState.RUNNING,
});
}
updated = true;
@@ -174,7 +175,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
displayName,
description,
args,
status: 'running',
status: SubagentState.RUNNING,
});
updated = true;
@@ -193,9 +194,11 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
if (
recentActivity[i].type === 'tool_call' &&
recentActivity[i].content === name &&
recentActivity[i].status === 'running'
recentActivity[i].status === SubagentState.RUNNING
) {
recentActivity[i].status = isError ? 'error' : 'completed';
recentActivity[i].status = isError
? SubagentState.ERROR
: SubagentState.COMPLETED;
updated = true;
this.publishActivity(recentActivity[i]);
@@ -224,9 +227,9 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
if (
recentActivity[i].type === 'tool_call' &&
recentActivity[i].content === toolName &&
recentActivity[i].status === 'running'
recentActivity[i].status === SubagentState.RUNNING
) {
recentActivity[i].status = 'cancelled';
recentActivity[i].status = SubagentState.CANCELLED;
updated = true;
break;
}
@@ -237,9 +240,9 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
if (
recentActivity[i].type === 'tool_call' &&
recentActivity[i].content === toolName &&
recentActivity[i].status === 'running'
recentActivity[i].status === SubagentState.RUNNING
) {
recentActivity[i].status = 'error';
recentActivity[i].status = SubagentState.ERROR;
updated = true;
break;
}
@@ -253,7 +256,10 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
isCancellation || isRejection
? sanitizedError
: `Error: ${sanitizedError}`,
status: isCancellation || isRejection ? 'cancelled' : 'error',
status:
isCancellation || isRejection
? SubagentState.CANCELLED
: SubagentState.ERROR,
});
updated = true;
break;
@@ -267,7 +273,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.definition.name,
recentActivity: [...recentActivity], // Copy to avoid mutation issues
state: 'running',
state: SubagentState.RUNNING,
};
updateOutput(progress);
@@ -287,7 +293,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.definition.name,
recentActivity: [...recentActivity],
state: 'cancelled',
state: SubagentState.CANCELLED,
};
if (updateOutput) {
@@ -303,7 +309,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
isSubagentProgress: true,
agentName: this.definition.name,
recentActivity: [...recentActivity],
state: 'completed',
state: SubagentState.COMPLETED,
result: output.result,
terminateReason: output.terminate_reason,
};
@@ -334,8 +340,8 @@ ${output.result}`;
// Mark any running items as error/cancelled
for (const item of recentActivity) {
if (item.status === 'running') {
item.status = isAbort ? 'cancelled' : 'error';
if (item.status === SubagentState.RUNNING) {
item.status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR;
}
}
@@ -343,12 +349,12 @@ ${output.result}`;
// But only if it's NOT an abort, or if we want to show "Cancelled" as a thought
if (!isAbort) {
const lastActivity = recentActivity[recentActivity.length - 1];
if (!lastActivity || lastActivity.status !== 'error') {
if (!lastActivity || lastActivity.status !== SubagentState.ERROR) {
recentActivity.push({
id: randomUUID(),
type: 'thought',
content: `Error: ${errorMessage}`,
status: 'error',
status: SubagentState.ERROR,
});
// Maintain size limit
// No limit on UI events sent via bus
@@ -359,7 +365,7 @@ ${output.result}`;
isSubagentProgress: true,
agentName: this.definition.name,
recentActivity: [...recentActivity],
state: isAbort ? 'cancelled' : 'error',
state: isAbort ? SubagentState.CANCELLED : SubagentState.ERROR,
};
if (updateOutput) {
@@ -20,7 +20,11 @@ import {
type A2AClientManager,
} from './a2a-client-manager.js';
import type { RemoteAgentDefinition, SubagentProgress } from './types.js';
import {
type RemoteAgentDefinition,
type SubagentProgress,
SubagentState,
} from './types.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
import type { A2AAuthProvider } from './auth-provider/types.js';
@@ -268,7 +272,9 @@ describe('RemoteAgentInvocation', () => {
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({
state: SubagentState.ERROR,
});
expect((result.returnDisplay as SubagentProgress).result).toContain(
"Failed to create auth provider for agent 'test-agent'",
);
@@ -461,7 +467,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: expect.arrayContaining([
expect.objectContaining({ content: 'Working...' }),
]),
@@ -470,7 +476,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'completed',
state: SubagentState.COMPLETED,
result: 'HelloHello World',
}),
);
@@ -508,7 +514,9 @@ describe('RemoteAgentInvocation', () => {
abortSignal: controller.signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({
state: SubagentState.ERROR,
});
});
it('should handle errors gracefully', async () => {
@@ -533,7 +541,7 @@ describe('RemoteAgentInvocation', () => {
});
expect(result.returnDisplay).toMatchObject({
state: 'error',
state: SubagentState.ERROR,
result: expect.stringContaining('Network error'),
});
});
@@ -616,7 +624,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: expect.arrayContaining([
expect.objectContaining({ content: 'Working...' }),
]),
@@ -625,7 +633,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'completed',
state: SubagentState.COMPLETED,
result: 'Thinking...Final Answer',
}),
);
@@ -693,7 +701,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: expect.arrayContaining([
expect.objectContaining({ content: 'Working...' }),
]),
@@ -702,7 +710,7 @@ describe('RemoteAgentInvocation', () => {
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
isSubagentProgress: true,
state: 'completed',
state: SubagentState.COMPLETED,
result: 'Generating...\n\nArtifact (Result):\nPart 1 Part 2',
}),
);
@@ -760,7 +768,9 @@ describe('RemoteAgentInvocation', () => {
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({
state: SubagentState.ERROR,
});
expect((result.returnDisplay as SubagentProgress).result).toContain(
a2aError.userMessage,
);
@@ -782,7 +792,9 @@ describe('RemoteAgentInvocation', () => {
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({
state: SubagentState.ERROR,
});
expect((result.returnDisplay as SubagentProgress).result).toContain(
'Error calling remote agent: something unexpected',
);
@@ -813,7 +825,9 @@ describe('RemoteAgentInvocation', () => {
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({
state: SubagentState.ERROR,
});
// Should contain both the partial output and the error message
expect(result.returnDisplay).toMatchObject({
result: expect.stringContaining('Partial response'),
@@ -17,6 +17,7 @@ import {
type RemoteAgentDefinition,
type AgentInputs,
type SubagentProgress,
SubagentState,
getAgentCardLoadOptions,
getRemoteAgentTargetUrl,
} from './types.js';
@@ -138,13 +139,13 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
updateOutput({
isSubagentProgress: true,
agentName,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: [
{
id: 'pending',
type: 'thought',
content: 'Working...',
status: 'running',
status: SubagentState.RUNNING,
},
],
});
@@ -193,7 +194,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
updateOutput({
isSubagentProgress: true,
agentName,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: reassembler.toActivityItems(),
result: reassembler.toString(),
});
@@ -225,7 +226,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
const finalProgress: SubagentProgress = {
isSubagentProgress: true,
agentName,
state: 'completed',
state: SubagentState.COMPLETED,
result: finalOutput,
recentActivity: reassembler.toActivityItems(),
};
@@ -249,7 +250,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
const errorProgress: SubagentProgress = {
isSubagentProgress: true,
agentName,
state: 'error',
state: SubagentState.ERROR,
result: fullDisplay,
recentActivity: reassembler.toActivityItems(),
};
@@ -28,6 +28,7 @@ import {
DEFAULT_QUERY_STRING,
type RemoteAgentDefinition,
type SubagentProgress,
SubagentState,
getRemoteAgentTargetUrl,
getAgentCardLoadOptions,
} from './types.js';
@@ -233,7 +234,7 @@ class RemoteSubagentProtocol implements AgentProtocol {
this._latestProgress = {
isSubagentProgress: true,
agentName: this._agentName,
state: 'running',
state: SubagentState.RUNNING,
recentActivity: reassembler.toActivityItems(),
result: currentText,
};
@@ -259,7 +260,7 @@ class RemoteSubagentProtocol implements AgentProtocol {
const finalProgress: SubagentProgress = {
isSubagentProgress: true,
agentName: this._agentName,
state: 'completed',
state: SubagentState.COMPLETED,
result: finalOutput,
recentActivity: reassembler.toActivityItems(),
};
+9 -2
View File
@@ -88,6 +88,13 @@ export interface SubagentActivityEvent {
data: Record<string, unknown>;
}
export enum SubagentState {
RUNNING = 'running',
COMPLETED = 'completed',
ERROR = 'error',
CANCELLED = 'cancelled',
}
export interface SubagentActivityItem {
id: string;
type: 'thought' | 'tool_call';
@@ -95,14 +102,14 @@ export interface SubagentActivityItem {
displayName?: string;
description?: string;
args?: string;
status: 'running' | 'completed' | 'error' | 'cancelled';
status: SubagentState;
}
export interface SubagentProgress {
isSubagentProgress: true;
agentName: string;
recentActivity: SubagentActivityItem[];
state?: 'running' | 'completed' | 'error' | 'cancelled';
state?: SubagentState;
result?: string;
terminateReason?: AgentTerminateMode;
}