feat(core,cli): enforce mandatory MessageBus injection (Phase 3 Hard Migration) (#15776)

This commit is contained in:
Abhi
2026-01-04 17:11:43 -05:00
committed by GitHub
parent 90be9c3587
commit 12c7c9cc42
57 changed files with 442 additions and 278 deletions
@@ -13,6 +13,7 @@ import { LocalSubagentInvocation } from './local-invocation.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { MessageBusType } from '../confirmation-bus/types.js';
import { DELEGATE_TO_AGENT_TOOL_NAME } from '../tools/tool-names.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('./local-invocation.js', () => ({
LocalSubagentInvocation: vi.fn().mockImplementation(() => ({
@@ -58,11 +59,7 @@ describe('DelegateToAgentTool', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(registry as any).agents.set(mockAgentDef.name, mockAgentDef);
messageBus = {
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
} as unknown as MessageBus;
messageBus = createMockMessageBus();
tool = new DelegateToAgentTool(registry, config, messageBus);
});
@@ -155,7 +152,7 @@ describe('DelegateToAgentTool', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(registry as any).agents.set(invalidAgentDef.name, invalidAgentDef);
expect(() => new DelegateToAgentTool(registry, config)).toThrow(
expect(() => new DelegateToAgentTool(registry, config, messageBus)).toThrow(
"Agent 'invalid_agent' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.",
);
});
@@ -30,7 +30,7 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<
constructor(
private readonly registry: AgentRegistry,
private readonly config: Config,
messageBus?: MessageBus,
messageBus: MessageBus,
) {
const definitions = registry.getAllDefinitions();
@@ -119,15 +119,15 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<
registry.getToolDescription(),
Kind.Think,
zodToJsonSchema(schema),
messageBus,
/* isOutputMarkdown */ true,
/* canUpdateOutput */ true,
messageBus,
);
}
protected createInvocation(
params: DelegateParams,
messageBus?: MessageBus,
messageBus: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
): ToolInvocation<DelegateParams, ToolResult> {
@@ -135,7 +135,7 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<
params,
this.registry,
this.config,
messageBus ?? this.messageBus,
messageBus,
_toolName,
_toolDisplayName,
);
@@ -150,7 +150,7 @@ class DelegateInvocation extends BaseToolInvocation<
params: DelegateParams,
private readonly registry: AgentRegistry,
private readonly config: Config,
messageBus?: MessageBus,
messageBus: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
) {
@@ -6,7 +6,7 @@
import { describe, it, expect } from 'vitest';
import { IntrospectionAgent } from './introspection-agent.js';
import { GetInternalDocsTool } from '../tools/get-internal-docs.js';
import { GET_INTERNAL_DOCS_TOOL_NAME } from '../tools/tool-names.js';
import { GEMINI_MODEL_ALIAS_FLASH } from '../config/models.js';
import type { LocalAgentDefinition } from './types.js';
@@ -32,9 +32,7 @@ describe('IntrospectionAgent', () => {
expect(localAgent.modelConfig?.model).toBe(GEMINI_MODEL_ALIAS_FLASH);
const tools = localAgent.toolConfig?.tools || [];
const hasInternalDocsTool = tools.some(
(t) => t instanceof GetInternalDocsTool,
);
const hasInternalDocsTool = tools.includes(GET_INTERNAL_DOCS_TOOL_NAME);
expect(hasInternalDocsTool).toBe(true);
});
@@ -5,7 +5,7 @@
*/
import type { AgentDefinition } from './types.js';
import { GetInternalDocsTool } from '../tools/get-internal-docs.js';
import { GET_INTERNAL_DOCS_TOOL_NAME } from '../tools/tool-names.js';
import { GEMINI_MODEL_ALIAS_FLASH } from '../config/models.js';
import { z } from 'zod';
@@ -60,7 +60,7 @@ export const IntrospectionAgent: AgentDefinition<
},
toolConfig: {
tools: [new GetInternalDocsTool()],
tools: [GET_INTERNAL_DOCS_TOOL_NAME],
},
promptConfig: {
@@ -269,8 +269,13 @@ describe('LocalAgentExecutor', () => {
vi.useFakeTimers();
mockConfig = makeFakeConfig();
parentToolRegistry = new ToolRegistry(mockConfig);
parentToolRegistry.registerTool(new LSTool(mockConfig));
parentToolRegistry = new ToolRegistry(
mockConfig,
mockConfig.getMessageBus(),
);
parentToolRegistry.registerTool(
new LSTool(mockConfig, mockConfig.getMessageBus()),
);
parentToolRegistry.registerTool(
new MockTool({ name: READ_FILE_TOOL_NAME }),
);
+4 -1
View File
@@ -99,7 +99,10 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
onActivity?: ActivityCallback,
): Promise<LocalAgentExecutor<TOutput>> {
// Create an isolated tool registry for this agent instance.
const agentToolRegistry = new ToolRegistry(runtimeContext);
const agentToolRegistry = new ToolRegistry(
runtimeContext,
runtimeContext.getMessageBus(),
);
const parentToolRegistry = runtimeContext.getToolRegistry();
if (definition.toolConfig) {
@@ -15,6 +15,7 @@ import { ToolErrorType } from '../tools/tool-error.js';
import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { type z } from 'zod';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('./local-executor.js');
@@ -39,10 +40,12 @@ const testDefinition: LocalAgentDefinition<z.ZodUnknown> = {
describe('LocalSubagentInvocation', () => {
let mockExecutorInstance: Mocked<LocalAgentExecutor<z.ZodUnknown>>;
let mockMessageBus: MessageBus;
beforeEach(() => {
vi.clearAllMocks();
mockConfig = makeFakeConfig();
mockMessageBus = createMockMessageBus();
mockExecutorInstance = {
run: vi.fn(),
@@ -55,7 +58,6 @@ describe('LocalSubagentInvocation', () => {
});
it('should pass the messageBus to the parent constructor', () => {
const mockMessageBus = {} as MessageBus;
const params = { task: 'Analyze data' };
const invocation = new LocalSubagentInvocation(
testDefinition,
@@ -76,6 +78,7 @@ describe('LocalSubagentInvocation', () => {
testDefinition,
mockConfig,
params,
mockMessageBus,
);
const description = invocation.getDescription();
expect(description).toBe(
@@ -90,6 +93,7 @@ describe('LocalSubagentInvocation', () => {
testDefinition,
mockConfig,
params,
mockMessageBus,
);
const description = invocation.getDescription();
// Default INPUT_PREVIEW_MAX_LENGTH is 50
@@ -112,6 +116,7 @@ describe('LocalSubagentInvocation', () => {
longNameDef,
mockConfig,
params,
mockMessageBus,
);
const description = invocation.getDescription();
// Default DESCRIPTION_MAX_LENGTH is 200
@@ -137,6 +142,7 @@ describe('LocalSubagentInvocation', () => {
testDefinition,
mockConfig,
params,
mockMessageBus,
);
});
+2 -2
View File
@@ -37,13 +37,13 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
* @param definition The definition object that configures the agent.
* @param config The global runtime configuration.
* @param params The validated input parameters for the agent.
* @param messageBus Optional message bus for policy enforcement.
* @param messageBus Message bus for policy enforcement.
*/
constructor(
private readonly definition: LocalAgentDefinition,
private readonly config: Config,
params: AgentInputs,
messageBus?: MessageBus,
messageBus: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
) {
@@ -8,6 +8,7 @@ import { describe, it, expect } from 'vitest';
import type { ToolCallConfirmationDetails } from '../tools/tools.js';
import { RemoteAgentInvocation } from './remote-invocation.js';
import type { RemoteAgentDefinition } from './types.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
class TestableRemoteAgentInvocation extends RemoteAgentInvocation {
override async getConfirmationDetails(
@@ -29,8 +30,14 @@ describe('RemoteAgentInvocation', () => {
},
};
const mockMessageBus = createMockMessageBus();
it('should be instantiated with correct params', () => {
const invocation = new RemoteAgentInvocation(mockDefinition, {});
const invocation = new RemoteAgentInvocation(
mockDefinition,
{},
mockMessageBus,
);
expect(invocation).toBeDefined();
expect(invocation.getDescription()).toBe(
'Calling remote agent Test Remote Agent',
@@ -38,7 +45,11 @@ describe('RemoteAgentInvocation', () => {
});
it('should return false for confirmation details (not yet implemented)', async () => {
const invocation = new TestableRemoteAgentInvocation(mockDefinition, {});
const invocation = new TestableRemoteAgentInvocation(
mockDefinition,
{},
mockMessageBus,
);
const details = await invocation.getConfirmationDetails(
new AbortController().signal,
);
@@ -25,7 +25,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
constructor(
private readonly definition: RemoteAgentDefinition,
params: AgentInputs,
messageBus?: MessageBus,
messageBus: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
) {
@@ -13,6 +13,7 @@ import type { LocalAgentDefinition, AgentInputs } from './types.js';
import type { Config } from '../config/config.js';
import { Kind } from '../tools/tools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
// Mock dependencies to isolate the SubagentToolWrapper class
vi.mock('./local-invocation.js');
@@ -25,6 +26,7 @@ const mockConvertInputConfigToJsonSchema = vi.mocked(
// Define reusable test data
let mockConfig: Config;
let mockMessageBus: MessageBus;
const mockDefinition: LocalAgentDefinition = {
kind: 'local',
@@ -59,6 +61,7 @@ describe('SubagentToolWrapper', () => {
beforeEach(() => {
vi.clearAllMocks();
mockConfig = makeFakeConfig();
mockMessageBus = createMockMessageBus();
// Provide a mock implementation for the schema conversion utility
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockConvertInputConfigToJsonSchema.mockReturnValue(mockSchema as any);
@@ -66,7 +69,7 @@ describe('SubagentToolWrapper', () => {
describe('constructor', () => {
it('should call convertInputConfigToJsonSchema with the correct agent inputConfig', () => {
new SubagentToolWrapper(mockDefinition, mockConfig);
new SubagentToolWrapper(mockDefinition, mockConfig, mockMessageBus);
expect(convertInputConfigToJsonSchema).toHaveBeenCalledExactlyOnceWith(
mockDefinition.inputConfig,
@@ -74,7 +77,11 @@ describe('SubagentToolWrapper', () => {
});
it('should correctly configure the tool properties from the agent definition', () => {
const wrapper = new SubagentToolWrapper(mockDefinition, mockConfig);
const wrapper = new SubagentToolWrapper(
mockDefinition,
mockConfig,
mockMessageBus,
);
expect(wrapper.name).toBe(mockDefinition.name);
expect(wrapper.displayName).toBe(mockDefinition.displayName);
@@ -92,12 +99,17 @@ describe('SubagentToolWrapper', () => {
const wrapper = new SubagentToolWrapper(
definitionWithoutDisplayName,
mockConfig,
mockMessageBus,
);
expect(wrapper.displayName).toBe(definitionWithoutDisplayName.name);
});
it('should generate a valid tool schema using the definition and converted schema', () => {
const wrapper = new SubagentToolWrapper(mockDefinition, mockConfig);
const wrapper = new SubagentToolWrapper(
mockDefinition,
mockConfig,
mockMessageBus,
);
const schema = wrapper.schema;
expect(schema.name).toBe(mockDefinition.name);
@@ -108,7 +120,11 @@ describe('SubagentToolWrapper', () => {
describe('createInvocation', () => {
it('should create a LocalSubagentInvocation with the correct parameters', () => {
const wrapper = new SubagentToolWrapper(mockDefinition, mockConfig);
const wrapper = new SubagentToolWrapper(
mockDefinition,
mockConfig,
mockMessageBus,
);
const params: AgentInputs = { goal: 'Test the invocation', priority: 1 };
// The public `build` method calls the protected `createInvocation` after validation
@@ -119,18 +135,22 @@ describe('SubagentToolWrapper', () => {
mockDefinition,
mockConfig,
params,
undefined,
mockMessageBus,
mockDefinition.name,
mockDefinition.displayName,
);
});
it('should pass the messageBus to the LocalSubagentInvocation constructor', () => {
const mockMessageBus = {} as MessageBus;
const specificMessageBus = {
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
} as unknown as MessageBus;
const wrapper = new SubagentToolWrapper(
mockDefinition,
mockConfig,
mockMessageBus,
specificMessageBus,
);
const params: AgentInputs = { goal: 'Test the invocation', priority: 1 };
@@ -140,14 +160,18 @@ describe('SubagentToolWrapper', () => {
mockDefinition,
mockConfig,
params,
mockMessageBus,
specificMessageBus,
mockDefinition.name,
mockDefinition.displayName,
);
});
it('should throw a validation error for invalid parameters before creating an invocation', () => {
const wrapper = new SubagentToolWrapper(mockDefinition, mockConfig);
const wrapper = new SubagentToolWrapper(
mockDefinition,
mockConfig,
mockMessageBus,
);
// Missing the required 'goal' parameter
const invalidParams = { priority: 1 };
@@ -38,7 +38,7 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
constructor(
private readonly definition: AgentDefinition,
private readonly config: Config,
messageBus?: MessageBus,
messageBus: MessageBus,
) {
const parameterSchema = convertInputConfigToJsonSchema(
definition.inputConfig,
@@ -50,9 +50,9 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
definition.description,
Kind.Think,
parameterSchema,
messageBus,
/* isOutputMarkdown */ true,
/* canUpdateOutput */ true,
messageBus,
);
}
@@ -67,12 +67,12 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
*/
protected createInvocation(
params: AgentInputs,
messageBus?: MessageBus,
messageBus: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
): ToolInvocation<AgentInputs, ToolResult> {
const definition = this.definition;
const effectiveMessageBus = messageBus ?? this.messageBus;
const effectiveMessageBus = messageBus;
if (definition.kind === 'remote') {
return new RemoteAgentInvocation(