feat(core): infrastructure for event-driven subagent history (#23914)

This commit is contained in:
Abhi
2026-03-31 17:54:22 -04:00
committed by GitHub
parent 6d48a12efe
commit 9364dd8a49
16 changed files with 525 additions and 91 deletions

View File

@@ -510,5 +510,36 @@ describe('LocalSubagentInvocation', () => {
'Operation cancelled by user',
);
});
it('should publish SUBAGENT_ACTIVITY events to the MessageBus', async () => {
const { MessageBusType } = await import('../confirmation-bus/types.js');
mockExecutorInstance.run.mockImplementation(async () => {
const onActivity = MockLocalAgentExecutor.create.mock.calls[0][2];
if (onActivity) {
onActivity({
isSubagentActivityEvent: true,
agentName: 'MockAgent',
type: 'THOUGHT_CHUNK',
data: { text: 'Thinking...' },
} as SubagentActivityEvent);
}
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
expect(mockMessageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageBusType.SUBAGENT_ACTIVITY,
subagentName: 'Mock Agent',
activity: expect.objectContaining({
type: 'thought',
content: 'Thinking...',
}),
}),
);
});
});
});

View File

@@ -5,6 +5,7 @@
*/
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import { MessageBusType } from '../confirmation-bus/types.js';
import { LocalAgentExecutor } from './local-executor.js';
import {
BaseToolInvocation,
@@ -33,7 +34,6 @@ import {
const INPUT_PREVIEW_MAX_LENGTH = 50;
const DESCRIPTION_MAX_LENGTH = 200;
const MAX_RECENT_ACTIVITY = 3;
/**
* Represents a validated, executable instance of a subagent tool.
@@ -87,6 +87,14 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
return description.slice(0, DESCRIPTION_MAX_LENGTH);
}
private publishActivity(activity: SubagentActivityItem): void {
void this.messageBus.publish({
type: MessageBusType.SUBAGENT_ACTIVITY,
subagentName: this.definition.displayName ?? this.definition.name,
activity,
});
}
/**
* Executes the subagent.
*
@@ -99,7 +107,7 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
signal: AbortSignal,
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
let recentActivity: SubagentActivityItem[] = [];
const recentActivity: SubagentActivityItem[] = [];
try {
if (updateOutput) {
@@ -140,6 +148,11 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
});
}
updated = true;
const latestThought = recentActivity[recentActivity.length - 1];
if (latestThought) {
this.publishActivity(latestThought);
}
break;
}
case 'TOOL_CALL_START': {
@@ -163,6 +176,11 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
status: 'running',
});
updated = true;
const latestTool = recentActivity[recentActivity.length - 1];
if (latestTool) {
this.publishActivity(latestTool);
}
break;
}
case 'TOOL_CALL_END': {
@@ -178,6 +196,8 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
) {
recentActivity[i].status = isError ? 'error' : 'completed';
updated = true;
this.publishActivity(recentActivity[i]);
break;
}
}
@@ -242,11 +262,6 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
}
if (updated) {
// Keep only the last N items
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
}
const progress: SubagentProgress = {
isSubagentProgress: true,
agentName: this.definition.name,
@@ -332,9 +347,7 @@ ${output.result}`;
status: 'error',
});
// Maintain size limit
if (recentActivity.length > MAX_RECENT_ACTIVITY) {
recentActivity = recentActivity.slice(-MAX_RECENT_ACTIVITY);
}
// No limit on UI events sent via bus
}
}

View File

@@ -12,6 +12,7 @@ import type {
} from '../tools/tools.js';
import type { ToolCall } from '../scheduler/types.js';
import type { SandboxPermissions } from '../services/sandboxManager.js';
import type { SubagentActivityItem } from '../agents/types.js';
export enum MessageBusType {
TOOL_CONFIRMATION_REQUEST = 'tool-confirmation-request',
@@ -23,6 +24,7 @@ export enum MessageBusType {
TOOL_CALLS_UPDATE = 'tool-calls-update',
ASK_USER_REQUEST = 'ask-user-request',
ASK_USER_RESPONSE = 'ask-user-response',
SUBAGENT_ACTIVITY = 'subagent-activity',
}
export interface ToolCallsUpdateMessage {
@@ -207,6 +209,12 @@ export interface AskUserResponse {
cancelled?: boolean;
}
export interface SubagentActivityMessage {
type: MessageBusType.SUBAGENT_ACTIVITY;
subagentName: string;
activity: SubagentActivityItem;
}
export type Message =
| ToolConfirmationRequest
| ToolConfirmationResponse
@@ -216,4 +224,5 @@ export type Message =
| UpdatePolicy
| AskUserRequest
| AskUserResponse
| ToolCallsUpdateMessage;
| ToolCallsUpdateMessage
| SubagentActivityMessage;

View File

@@ -22,7 +22,7 @@ export class MockMessageBus {
/**
* Mock publish method that captures messages and simulates responses
*/
publish = vi.fn((message: Message) => {
publish = vi.fn(async (message: Message) => {
this.publishedMessages.push(message);
// Handle tool confirmation requests