refactor(core): consolidate execute() arguments into ExecuteOptions (#25101)

This commit is contained in:
Michael Bleigh
2026-04-10 10:11:17 -07:00
committed by GitHub
parent 1d36309f5f
commit 3b7c17a22c
69 changed files with 849 additions and 527 deletions
+7 -6
View File
@@ -11,7 +11,7 @@ import {
type ToolResult,
BaseToolInvocation,
type ToolCallConfirmationDetails,
type ToolLiveOutput,
type ExecuteOptions,
} from '../tools/tools.js';
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
@@ -185,10 +185,8 @@ class DelegateInvocation extends BaseToolInvocation<
return invocation.shouldConfirmExecute(abortSignal);
}
async execute(
signal: AbortSignal,
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
async execute(options: ExecuteOptions): Promise<ToolResult> {
const { abortSignal: signal, updateOutput } = options;
const hintedParams = this.withUserHints(this.mappedInputs);
const invocation = this.buildChildInvocation(hintedParams);
@@ -204,7 +202,10 @@ class DelegateInvocation extends BaseToolInvocation<
},
async ({ metadata }) => {
metadata.input = this.params;
const result = await invocation.execute(signal, updateOutput);
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
metadata.output = result;
return result;
},
@@ -99,7 +99,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Find the blue submit button',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
// Verify screenshot was captured
expect(browserManager.callTool).toHaveBeenCalledWith(
@@ -165,7 +167,7 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Find the search bar',
});
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
const contentGenerator = config.getContentGenerator();
expect(contentGenerator.generateContent).toHaveBeenCalledWith(
@@ -194,7 +196,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Find the button',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('Failed to capture screenshot');
@@ -217,7 +221,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Check the layout',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('Visual model returned no analysis');
@@ -238,7 +244,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Find the red error',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain(
@@ -261,7 +269,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Identify the element',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain(
@@ -281,7 +291,9 @@ describe('analyzeScreenshot', () => {
const invocation = tool.build({
instruction: 'Find something',
});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('Visual analysis failed');
@@ -23,6 +23,7 @@ import {
Kind,
type ToolResult,
type ToolInvocation,
type ExecuteOptions,
} from '../../tools/tools.js';
import { Environment } from '@google/genai';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
@@ -80,7 +81,7 @@ class AnalyzeScreenshotInvocation extends BaseToolInvocation<
return `Visual analysis: "${instruction}"`;
}
async execute(signal: AbortSignal): Promise<ToolResult> {
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
try {
const instruction = String(this.params['instruction'] ?? '');
@@ -223,7 +223,10 @@ describe('BrowserAgentInvocation', () => {
const controller = new AbortController();
const updateOutput: (output: ToolLiveOutput) => void = vi.fn();
const result = await invocation.execute(controller.signal, updateOutput);
const result = await invocation.execute({
abortSignal: controller.signal,
updateOutput,
});
expect(Array.isArray(result.llmContent)).toBe(true);
expect((result.llmContent as Array<{ text: string }>)[0].text).toContain(
@@ -242,7 +245,7 @@ describe('BrowserAgentInvocation', () => {
const controller = new AbortController();
// Should not throw even with no updateOutput
await expect(
invocation.execute(controller.signal),
invocation.execute({ abortSignal: controller.signal }),
).resolves.toBeDefined();
});
@@ -256,7 +259,9 @@ describe('BrowserAgentInvocation', () => {
);
const controller = new AbortController();
const result = await invocation.execute(controller.signal);
const result = await invocation.execute({
abortSignal: controller.signal,
});
expect(result.error).toBeDefined();
expect(removeInputBlocker).toHaveBeenCalled();
@@ -298,7 +303,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
await invocation.execute(new AbortController().signal, updateOutput);
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
});
const firstCall = updateOutput.mock.calls[0]?.[0] as SubagentProgress;
expect(firstCall.isSubagentProgress).toBe(true);
@@ -315,7 +323,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
await invocation.execute(new AbortController().signal, updateOutput);
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
});
const lastCall = updateOutput.mock.calls[
updateOutput.mock.calls.length - 1
@@ -334,10 +345,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
// Allow createBrowserAgentDefinition to resolve and onActivity to be registered
await Promise.resolve();
@@ -377,10 +388,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
// Allow createBrowserAgentDefinition to resolve and onActivity to be registered
await Promise.resolve();
@@ -424,10 +435,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -475,10 +486,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -519,10 +530,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -564,10 +575,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -604,10 +615,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -647,10 +658,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
const executePromise = invocation.execute(
new AbortController().signal,
const executePromise = invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
await Promise.resolve();
await Promise.resolve();
@@ -703,7 +714,10 @@ describe('BrowserAgentInvocation', () => {
mockParams,
mockMessageBus,
);
await invocation.execute(new AbortController().signal, vi.fn());
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput: vi.fn(),
});
expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith(
mockConfig,
@@ -731,7 +745,10 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
await invocation.execute(new AbortController().signal, updateOutput);
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
});
expect(recordBrowserAgentTaskOutcome).toHaveBeenCalledWith(
mockConfig,
@@ -751,7 +768,10 @@ describe('BrowserAgentInvocation', () => {
mockParams,
mockMessageBus,
);
await invocation.execute(new AbortController().signal, vi.fn());
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput: vi.fn(),
});
expect(cleanupBrowserAgent).not.toHaveBeenCalled();
});
@@ -807,7 +827,7 @@ describe('BrowserAgentInvocation', () => {
mockMessageBus,
);
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
// Verify list_pages was called
expect(mockBrowserManager.callTool).toHaveBeenCalledWith(
@@ -22,7 +22,7 @@ import { LocalAgentExecutor } from '../local-executor.js';
import {
BaseToolInvocation,
type ToolResult,
type ToolLiveOutput,
type ExecuteOptions,
} from '../../tools/tools.js';
import { ToolErrorType } from '../../tools/tool-error.js';
import {
@@ -107,10 +107,8 @@ export class BrowserAgentInvocation extends BaseToolInvocation<
* 3. Runs the agent via LocalAgentExecutor
* 4. Cleans up browser resources
*/
async execute(
signal: AbortSignal,
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
async execute(options: ExecuteOptions): Promise<ToolResult> {
const { abortSignal: signal, updateOutput } = options;
const invocationStartMs = Date.now();
let browserManager;
let recentActivity: SubagentActivityItem[] = [];
@@ -139,7 +139,7 @@ describe('mcpToolWrapper', () => {
);
const invocation = tools[1].build({ uid: 'elem-123' });
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
expect(mockBrowserManager.callTool).toHaveBeenCalledWith(
'click',
@@ -158,7 +158,9 @@ describe('mcpToolWrapper', () => {
);
const invocation = tools[0].build({ verbose: true });
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.llmContent).toBe('Tool result');
expect(result.error).toBeUndefined();
@@ -177,7 +179,9 @@ describe('mcpToolWrapper', () => {
);
const invocation = tools[1].build({ uid: 'invalid' });
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.error?.message).toBe('Element not found');
@@ -195,7 +199,9 @@ describe('mcpToolWrapper', () => {
);
const invocation = tools[0].build({});
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.error?.message).toBe('Connection lost');
@@ -212,7 +218,7 @@ describe('mcpToolWrapper', () => {
const clickTool = tools.find((t) => t.name === 'click')!;
const invocation = clickTool.build({ uid: 'elem-42' });
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
// callTool: suspend blocker + click + resume blocker
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(3);
@@ -257,7 +263,7 @@ describe('mcpToolWrapper', () => {
const snapshotTool = tools.find((t) => t.name === 'take_snapshot')!;
const invocation = snapshotTool.build({});
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
// callTool should only be called once for take_snapshot — no suspend/resume
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(1);
@@ -277,7 +283,7 @@ describe('mcpToolWrapper', () => {
const clickTool = tools.find((t) => t.name === 'click')!;
const invocation = clickTool.build({ uid: 'elem-42' });
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
// callTool should only be called once for click — no suspend/resume
expect(mockBrowserManager.callTool).toHaveBeenCalledTimes(1);
@@ -297,7 +303,9 @@ describe('mcpToolWrapper', () => {
const clickTool = tools.find((t) => t.name === 'click')!;
const invocation = clickTool.build({ uid: 'bad-elem' });
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
// Should return error, not throw
expect(result.error).toBeDefined();
@@ -328,7 +336,9 @@ describe('mcpToolWrapper', () => {
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
const invocation = uploadTool.build({ path: 'test.txt' });
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeDefined();
expect(result.llmContent).toContain('File uploads are blocked');
@@ -345,7 +355,9 @@ describe('mcpToolWrapper', () => {
const uploadTool = tools.find((t) => t.name === 'upload_file')!;
const invocation = uploadTool.build({ path: 'test.txt' });
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.error).toBeUndefined();
expect(result.llmContent).toBe('Tool result');
@@ -26,6 +26,7 @@ import {
type ToolInvocation,
type ToolCallConfirmationDetails,
type PolicyUpdateOptions,
type ExecuteOptions,
} from '../../tools/tools.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import {
@@ -117,7 +118,7 @@ class McpToolInvocation extends BaseToolInvocation<
return this.shouldDisableInput && INTERACTIVE_TOOLS.has(this.toolName);
}
async execute(signal: AbortSignal): Promise<ToolResult> {
async execute({ abortSignal: signal }: ExecuteOptions): Promise<ToolResult> {
try {
// Hard block for file uploads if configured
if (this.blockFileUploads && this.toolName === 'upload_file') {
@@ -187,7 +187,10 @@ describe('LocalSubagentInvocation', () => {
};
mockExecutorInstance.run.mockResolvedValue(mockOutput);
const result = await invocation.execute(signal, updateOutput);
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
expect(MockLocalAgentExecutor.create).toHaveBeenCalledWith(
testDefinition,
@@ -224,7 +227,10 @@ describe('LocalSubagentInvocation', () => {
};
mockExecutorInstance.run.mockResolvedValue(mockOutput);
const result = await invocation.execute(signal, updateOutput);
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
const display = result.returnDisplay as SubagentProgress;
expect(display.isSubagentProgress).toBe(true);
@@ -254,7 +260,7 @@ describe('LocalSubagentInvocation', () => {
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
@@ -293,7 +299,7 @@ describe('LocalSubagentInvocation', () => {
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
const calls = updateOutput.mock.calls;
const lastCall = calls[calls.length - 1][0] as SubagentProgress;
@@ -326,7 +332,7 @@ describe('LocalSubagentInvocation', () => {
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
@@ -360,7 +366,7 @@ describe('LocalSubagentInvocation', () => {
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
expect(updateOutput).toHaveBeenCalled();
const lastCall = updateOutput.mock.calls[
@@ -404,7 +410,7 @@ describe('LocalSubagentInvocation', () => {
};
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
expect(updateOutput).toHaveBeenCalledTimes(4);
const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
@@ -433,7 +439,7 @@ describe('LocalSubagentInvocation', () => {
});
// Execute without the optional callback
const result = await invocation.execute(signal);
const result = await invocation.execute({ abortSignal: signal });
expect(result.error).toBeUndefined();
const display = result.returnDisplay as SubagentProgress;
expect(display.isSubagentProgress).toBe(true);
@@ -445,7 +451,10 @@ describe('LocalSubagentInvocation', () => {
const error = new Error('Model failed during execution.');
mockExecutorInstance.run.mockRejectedValue(error);
const result = await invocation.execute(signal, updateOutput);
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
expect(result.error).toBeUndefined();
expect(result.llmContent).toBe(
@@ -466,7 +475,10 @@ describe('LocalSubagentInvocation', () => {
const creationError = new Error('Failed to initialize tools.');
MockLocalAgentExecutor.create.mockRejectedValue(creationError);
const result = await invocation.execute(signal, updateOutput);
const result = await invocation.execute({
abortSignal: signal,
updateOutput,
});
expect(mockExecutorInstance.run).not.toHaveBeenCalled();
expect(result.error).toBeUndefined();
@@ -487,10 +499,10 @@ describe('LocalSubagentInvocation', () => {
mockExecutorInstance.run.mockRejectedValue(abortError);
const controller = new AbortController();
const executePromise = invocation.execute(
controller.signal,
const executePromise = invocation.execute({
abortSignal: controller.signal,
updateOutput,
);
});
controller.abort();
await expect(executePromise).rejects.toThrow('Aborted');
@@ -507,9 +519,9 @@ describe('LocalSubagentInvocation', () => {
};
mockExecutorInstance.run.mockResolvedValue(mockOutput);
await expect(invocation.execute(signal, updateOutput)).rejects.toThrow(
'Operation cancelled by user',
);
await expect(
invocation.execute({ abortSignal: signal, updateOutput }),
).rejects.toThrow('Operation cancelled by user');
});
it('should publish SUBAGENT_ACTIVITY events to the MessageBus', async () => {
@@ -529,7 +541,7 @@ describe('LocalSubagentInvocation', () => {
return { result: 'Done', terminate_reason: AgentTerminateMode.GOAL };
});
await invocation.execute(signal, updateOutput);
await invocation.execute({ abortSignal: signal, updateOutput });
expect(mockMessageBus.publish).toHaveBeenCalledWith(
expect.objectContaining({
+3 -5
View File
@@ -10,7 +10,7 @@ import { LocalAgentExecutor } from './local-executor.js';
import {
BaseToolInvocation,
type ToolResult,
type ToolLiveOutput,
type ExecuteOptions,
} from '../tools/tools.js';
import {
type LocalAgentDefinition,
@@ -105,10 +105,8 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
* agent's thoughts, to the user interface.
* @returns A `Promise` that resolves with the final `ToolResult`.
*/
async execute(
signal: AbortSignal,
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
async execute(options: ExecuteOptions): Promise<ToolResult> {
const { abortSignal: signal, updateOutput } = options;
const recentActivity: SubagentActivityItem[] = [];
let executor: LocalAgentExecutor<z.ZodUnknown> | undefined;
@@ -142,7 +142,7 @@ describe('RemoteAgentInvocation', () => {
{},
mockMessageBus,
);
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
expect(mockClientManager.sendMessageStream).toHaveBeenCalledWith(
'test-agent',
@@ -185,7 +185,7 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
expect(mockClientManager.loadAgent).toHaveBeenCalledWith(
'test-agent',
@@ -230,7 +230,7 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
expect(A2AAuthProviderFactory.create).toHaveBeenCalledWith({
authConfig: mockAuth,
@@ -264,7 +264,9 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect((result.returnDisplay as SubagentProgress).result).toContain(
@@ -293,7 +295,7 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
await invocation.execute(new AbortController().signal);
await invocation.execute({ abortSignal: new AbortController().signal });
expect(mockClientManager.loadAgent).not.toHaveBeenCalled();
});
@@ -325,7 +327,9 @@ describe('RemoteAgentInvocation', () => {
);
// Execute first time
const result1 = await invocation1.execute(new AbortController().signal);
const result1 = await invocation1.execute({
abortSignal: new AbortController().signal,
});
expect(result1.returnDisplay).toMatchObject({
result: 'Response 1',
});
@@ -357,7 +361,9 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
const result2 = await invocation2.execute(new AbortController().signal);
const result2 = await invocation2.execute({
abortSignal: new AbortController().signal,
});
expect((result2.returnDisplay as SubagentProgress).result).toBe(
'Response 2',
);
@@ -390,7 +396,7 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
await invocation3.execute(new AbortController().signal);
await invocation3.execute({ abortSignal: new AbortController().signal });
// Fourth call: Should start new task (taskId undefined)
mockClientManager.sendMessageStream.mockImplementationOnce(
@@ -412,7 +418,7 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
await invocation4.execute(new AbortController().signal);
await invocation4.execute({ abortSignal: new AbortController().signal });
expect(mockClientManager.sendMessageStream).toHaveBeenLastCalledWith(
'test-agent',
@@ -447,7 +453,10 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
await invocation.execute(new AbortController().signal, updateOutput);
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
});
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
@@ -495,7 +504,9 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(controller.signal);
const result = await invocation.execute({
abortSignal: controller.signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
});
@@ -517,7 +528,9 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({
state: 'error',
@@ -550,7 +563,9 @@ describe('RemoteAgentInvocation', () => {
},
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
// Just check that text is present, exact formatting depends on helper
expect((result.returnDisplay as SubagentProgress).result).toContain(
@@ -593,10 +608,10 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(
new AbortController().signal,
const result = await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
);
});
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
@@ -670,7 +685,10 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
await invocation.execute(new AbortController().signal, updateOutput);
await invocation.execute({
abortSignal: new AbortController().signal,
updateOutput,
});
expect(updateOutput).toHaveBeenCalledWith(
expect.objectContaining({
@@ -738,7 +756,9 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect((result.returnDisplay as SubagentProgress).result).toContain(
@@ -758,7 +778,9 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect((result.returnDisplay as SubagentProgress).result).toContain(
@@ -787,7 +809,9 @@ describe('RemoteAgentInvocation', () => {
{ query: 'hi' },
mockMessageBus,
);
const result = await invocation.execute(new AbortController().signal);
const result = await invocation.execute({
abortSignal: new AbortController().signal,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
// Should contain both the partial output and the error message
@@ -9,6 +9,7 @@ import {
type ToolConfirmationOutcome,
type ToolResult,
type ToolCallConfirmationDetails,
type ExecuteOptions,
} from '../tools/tools.js';
import {
DEFAULT_QUERY_STRING,
@@ -28,7 +29,6 @@ import type {
import { extractIdsFromResponse, A2AResultReassembler } from './a2aUtils.js';
import type { AuthenticationHandler } from '@a2a-js/sdk/client';
import { debugLogger } from '../utils/debugLogger.js';
import type { AnsiOutput } from '../utils/terminalSerializer.js';
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
import { A2AAgentError } from './a2a-errors.js';
@@ -126,10 +126,8 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
};
}
async execute(
_signal: AbortSignal,
updateOutput?: (output: string | AnsiOutput | SubagentProgress) => void,
): Promise<ToolResult> {
async execute(options: ExecuteOptions): Promise<ToolResult> {
const { abortSignal: _signal, updateOutput } = options;
// 1. Ensure the agent is loaded (cached by manager)
// We assume the user has provided an access token via some mechanism (TODO),
// or we rely on ADC.