refactor(core): introduce SubagentState enum for progress

This commit is contained in:
Adam Weidman
2026-05-12 13:44:28 -04:00
parent 07792f98cd
commit 086337784a
6 changed files with 76 additions and 48 deletions
@@ -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',
);
@@ -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;
}