@google/gemini-cli-a2a-server 0.33.0-preview.13 → 0.33.0-preview.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a-server.mjs +240 -47
- package/dist/src/agent/executor.js +37 -13
- package/dist/src/agent/executor.js.map +1 -1
- package/dist/src/agent/executor.test.d.ts +6 -0
- package/dist/src/agent/executor.test.js +186 -0
- package/dist/src/agent/executor.test.js.map +1 -0
- package/dist/src/agent/task-event-driven.test.d.ts +1 -0
- package/dist/src/agent/task-event-driven.test.js +446 -0
- package/dist/src/agent/task-event-driven.test.js.map +1 -0
- package/dist/src/agent/task.d.ts +14 -3
- package/dist/src/agent/task.js +208 -33
- package/dist/src/agent/task.js.map +1 -1
- package/dist/src/agent/task.test.js +19 -2
- package/dist/src/agent/task.test.js.map +1 -1
- package/dist/src/config/config.js +1 -0
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/settings.d.ts +6 -0
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/utils/testing_utils.js +1 -0
- package/dist/src/utils/testing_utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { CoderAgentExecutor } from './executor.js';
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
import { requestStorage } from '../http/requestStorage.js';
|
|
10
|
+
// Mocks for constructor dependencies
|
|
11
|
+
vi.mock('../config/config.js', () => ({
|
|
12
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
13
|
+
getSessionId: () => 'test-session',
|
|
14
|
+
getTargetDir: () => '/tmp',
|
|
15
|
+
getCheckpointingEnabled: () => false,
|
|
16
|
+
}),
|
|
17
|
+
loadEnvironment: vi.fn(),
|
|
18
|
+
setTargetDir: vi.fn().mockReturnValue('/tmp'),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('../config/settings.js', () => ({
|
|
21
|
+
loadSettings: vi.fn().mockReturnValue({}),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('../config/extension.js', () => ({
|
|
24
|
+
loadExtensions: vi.fn().mockReturnValue([]),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock('../http/requestStorage.js', () => ({
|
|
27
|
+
requestStorage: {
|
|
28
|
+
getStore: vi.fn(),
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('./task.js', () => {
|
|
32
|
+
const mockTaskInstance = (taskId, contextId) => ({
|
|
33
|
+
id: taskId,
|
|
34
|
+
contextId,
|
|
35
|
+
taskState: 'working',
|
|
36
|
+
acceptUserMessage: vi
|
|
37
|
+
.fn()
|
|
38
|
+
.mockImplementation(async function* (context, aborted) {
|
|
39
|
+
const isConfirmation = context.userMessage.parts.some((p) => p.kind === 'confirmation');
|
|
40
|
+
// Hang only for main user messages (text), allow confirmations to finish quickly
|
|
41
|
+
if (!isConfirmation && aborted) {
|
|
42
|
+
await new Promise((resolve) => {
|
|
43
|
+
aborted.addEventListener('abort', resolve, { once: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
yield { type: 'content', value: 'hello' };
|
|
47
|
+
}),
|
|
48
|
+
acceptAgentMessage: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
|
|
50
|
+
waitForPendingTools: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
getAndClearCompletedTools: vi.fn().mockReturnValue([]),
|
|
52
|
+
addToolResponsesToHistory: vi.fn(),
|
|
53
|
+
sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () { }),
|
|
54
|
+
cancelPendingTools: vi.fn(),
|
|
55
|
+
setTaskStateAndPublishUpdate: vi.fn(),
|
|
56
|
+
dispose: vi.fn(),
|
|
57
|
+
getMetadata: vi.fn().mockResolvedValue({}),
|
|
58
|
+
geminiClient: {
|
|
59
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
60
|
+
},
|
|
61
|
+
toSDKTask: () => ({
|
|
62
|
+
id: taskId,
|
|
63
|
+
contextId,
|
|
64
|
+
kind: 'task',
|
|
65
|
+
status: { state: 'working', timestamp: new Date().toISOString() },
|
|
66
|
+
metadata: {},
|
|
67
|
+
history: [],
|
|
68
|
+
artifacts: [],
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
const MockTask = vi.fn().mockImplementation(mockTaskInstance);
|
|
72
|
+
MockTask.create = vi
|
|
73
|
+
.fn()
|
|
74
|
+
.mockImplementation(async (taskId, contextId) => mockTaskInstance(taskId, contextId));
|
|
75
|
+
return { Task: MockTask };
|
|
76
|
+
});
|
|
77
|
+
describe('CoderAgentExecutor', () => {
|
|
78
|
+
let executor;
|
|
79
|
+
let mockTaskStore;
|
|
80
|
+
let mockEventBus;
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
mockTaskStore = {
|
|
84
|
+
save: vi.fn().mockResolvedValue(undefined),
|
|
85
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
86
|
+
delete: vi.fn().mockResolvedValue(undefined),
|
|
87
|
+
list: vi.fn().mockResolvedValue([]),
|
|
88
|
+
};
|
|
89
|
+
mockEventBus = new EventEmitter();
|
|
90
|
+
mockEventBus.publish = vi.fn();
|
|
91
|
+
mockEventBus.finished = vi.fn();
|
|
92
|
+
executor = new CoderAgentExecutor(mockTaskStore);
|
|
93
|
+
});
|
|
94
|
+
it('should distinguish between primary and secondary execution', async () => {
|
|
95
|
+
const taskId = 'test-task';
|
|
96
|
+
const contextId = 'test-context';
|
|
97
|
+
const mockSocket = new EventEmitter();
|
|
98
|
+
const requestContext = {
|
|
99
|
+
userMessage: {
|
|
100
|
+
messageId: 'msg-1',
|
|
101
|
+
taskId,
|
|
102
|
+
contextId,
|
|
103
|
+
parts: [{ kind: 'text', text: 'hi' }],
|
|
104
|
+
metadata: {
|
|
105
|
+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
// Mock requestStorage for primary
|
|
110
|
+
requestStorage.getStore.mockReturnValue({
|
|
111
|
+
req: { socket: mockSocket },
|
|
112
|
+
});
|
|
113
|
+
// First execution (Primary)
|
|
114
|
+
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
|
115
|
+
// Give it enough time to reach line 490 in executor.ts
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
117
|
+
expect(executor.executingTasks.has(taskId)).toBe(true);
|
|
118
|
+
const wrapper = executor.getTask(taskId);
|
|
119
|
+
expect(wrapper).toBeDefined();
|
|
120
|
+
// Mock requestStorage for secondary
|
|
121
|
+
const secondarySocket = new EventEmitter();
|
|
122
|
+
requestStorage.getStore.mockReturnValue({
|
|
123
|
+
req: { socket: secondarySocket },
|
|
124
|
+
});
|
|
125
|
+
const secondaryRequestContext = {
|
|
126
|
+
userMessage: {
|
|
127
|
+
messageId: 'msg-2',
|
|
128
|
+
taskId,
|
|
129
|
+
contextId,
|
|
130
|
+
parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
|
|
131
|
+
metadata: {
|
|
132
|
+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const secondaryPromise = executor.execute(secondaryRequestContext, mockEventBus);
|
|
137
|
+
// Secondary execution should NOT add to executingTasks (already there)
|
|
138
|
+
// and should return early after its loop
|
|
139
|
+
await secondaryPromise;
|
|
140
|
+
// Task should still be in executingTasks and NOT disposed
|
|
141
|
+
expect(executor.executingTasks.has(taskId)).toBe(true);
|
|
142
|
+
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
|
143
|
+
// Now simulate secondary socket closure - it should NOT affect primary
|
|
144
|
+
secondarySocket.emit('end');
|
|
145
|
+
expect(executor.executingTasks.has(taskId)).toBe(true);
|
|
146
|
+
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
|
147
|
+
// Set to terminal state to verify disposal on finish
|
|
148
|
+
wrapper.task.taskState = 'completed';
|
|
149
|
+
// Now close primary socket
|
|
150
|
+
mockSocket.emit('end');
|
|
151
|
+
await primaryPromise;
|
|
152
|
+
expect(executor.executingTasks.has(taskId)).toBe(false);
|
|
153
|
+
expect(wrapper?.task.dispose).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
it('should evict task from cache when it reaches terminal state', async () => {
|
|
156
|
+
const taskId = 'test-task-terminal';
|
|
157
|
+
const contextId = 'test-context';
|
|
158
|
+
const mockSocket = new EventEmitter();
|
|
159
|
+
requestStorage.getStore.mockReturnValue({
|
|
160
|
+
req: { socket: mockSocket },
|
|
161
|
+
});
|
|
162
|
+
const requestContext = {
|
|
163
|
+
userMessage: {
|
|
164
|
+
messageId: 'msg-1',
|
|
165
|
+
taskId,
|
|
166
|
+
contextId,
|
|
167
|
+
parts: [{ kind: 'text', text: 'hi' }],
|
|
168
|
+
metadata: {
|
|
169
|
+
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
175
|
+
const wrapper = executor.getTask(taskId);
|
|
176
|
+
expect(wrapper).toBeDefined();
|
|
177
|
+
// Simulate terminal state
|
|
178
|
+
wrapper.task.taskState = 'completed';
|
|
179
|
+
// Finish primary execution
|
|
180
|
+
mockSocket.emit('end');
|
|
181
|
+
await primaryPromise;
|
|
182
|
+
expect(executor.getTask(taskId)).toBeUndefined();
|
|
183
|
+
expect(wrapper.task.dispose).toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=executor.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.test.js","sourceRoot":"","sources":["../../../src/agent/executor.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { Task } from './task.js';
|
|
8
|
+
import { MessageBusType, ToolConfirmationOutcome, ApprovalMode, Scheduler, } from '@google/gemini-cli-core';
|
|
9
|
+
import { createMockConfig } from '../utils/testing_utils.js';
|
|
10
|
+
describe('Task Event-Driven Scheduler', () => {
|
|
11
|
+
let mockConfig;
|
|
12
|
+
let mockEventBus;
|
|
13
|
+
let messageBus;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
mockConfig = createMockConfig({
|
|
17
|
+
isEventDrivenSchedulerEnabled: () => true,
|
|
18
|
+
});
|
|
19
|
+
messageBus = mockConfig.getMessageBus();
|
|
20
|
+
mockEventBus = {
|
|
21
|
+
publish: vi.fn(),
|
|
22
|
+
on: vi.fn(),
|
|
23
|
+
off: vi.fn(),
|
|
24
|
+
once: vi.fn(),
|
|
25
|
+
removeAllListeners: vi.fn(),
|
|
26
|
+
finished: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
it('should instantiate Scheduler when enabled', () => {
|
|
30
|
+
// @ts-expect-error - Calling private constructor
|
|
31
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
32
|
+
expect(task.scheduler).toBeInstanceOf(Scheduler);
|
|
33
|
+
});
|
|
34
|
+
it('should subscribe to TOOL_CALLS_UPDATE and map status changes', async () => {
|
|
35
|
+
// @ts-expect-error - Calling private constructor
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
37
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
38
|
+
const toolCall = {
|
|
39
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
40
|
+
status: 'executing',
|
|
41
|
+
};
|
|
42
|
+
// Simulate MessageBus event
|
|
43
|
+
// Simulate MessageBus event
|
|
44
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
45
|
+
if (!handler) {
|
|
46
|
+
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
47
|
+
}
|
|
48
|
+
handler({
|
|
49
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
50
|
+
toolCalls: [toolCall],
|
|
51
|
+
});
|
|
52
|
+
expect(mockEventBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
53
|
+
status: expect.objectContaining({
|
|
54
|
+
state: 'submitted', // initial task state
|
|
55
|
+
}),
|
|
56
|
+
metadata: expect.objectContaining({
|
|
57
|
+
coderAgent: expect.objectContaining({
|
|
58
|
+
kind: 'tool-call-update',
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
it('should handle tool confirmations by publishing to MessageBus', async () => {
|
|
64
|
+
// @ts-expect-error - Calling private constructor
|
|
65
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
66
|
+
const toolCall = {
|
|
67
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
68
|
+
status: 'awaiting_approval',
|
|
69
|
+
correlationId: 'corr-1',
|
|
70
|
+
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
71
|
+
};
|
|
72
|
+
// Simulate MessageBus event to stash the correlationId
|
|
73
|
+
// Simulate MessageBus event
|
|
74
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
75
|
+
if (!handler) {
|
|
76
|
+
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
77
|
+
}
|
|
78
|
+
handler({
|
|
79
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
80
|
+
toolCalls: [toolCall],
|
|
81
|
+
});
|
|
82
|
+
// Simulate A2A client confirmation
|
|
83
|
+
const part = {
|
|
84
|
+
kind: 'data',
|
|
85
|
+
data: {
|
|
86
|
+
callId: '1',
|
|
87
|
+
outcome: 'proceed_once',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const handled = await task._handleToolConfirmationPart(part);
|
|
91
|
+
expect(handled).toBe(true);
|
|
92
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
93
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
94
|
+
correlationId: 'corr-1',
|
|
95
|
+
confirmed: true,
|
|
96
|
+
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
it('should handle Rejection (Cancel) and Modification (ModifyWithEditor)', async () => {
|
|
100
|
+
// @ts-expect-error - Calling private constructor
|
|
101
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
102
|
+
const toolCall = {
|
|
103
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
104
|
+
status: 'awaiting_approval',
|
|
105
|
+
correlationId: 'corr-1',
|
|
106
|
+
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
107
|
+
};
|
|
108
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
109
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
110
|
+
// Simulate Rejection (Cancel)
|
|
111
|
+
const handled = await task._handleToolConfirmationPart({
|
|
112
|
+
kind: 'data',
|
|
113
|
+
data: { callId: '1', outcome: 'cancel' },
|
|
114
|
+
});
|
|
115
|
+
expect(handled).toBe(true);
|
|
116
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
117
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
118
|
+
correlationId: 'corr-1',
|
|
119
|
+
confirmed: false,
|
|
120
|
+
}));
|
|
121
|
+
const toolCall2 = {
|
|
122
|
+
request: { callId: '2', name: 'ls', args: {} },
|
|
123
|
+
status: 'awaiting_approval',
|
|
124
|
+
correlationId: 'corr-2',
|
|
125
|
+
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
126
|
+
};
|
|
127
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
|
|
128
|
+
// Simulate ModifyWithEditor
|
|
129
|
+
const handled2 = await task._handleToolConfirmationPart({
|
|
130
|
+
kind: 'data',
|
|
131
|
+
data: { callId: '2', outcome: 'modify_with_editor' },
|
|
132
|
+
});
|
|
133
|
+
expect(handled2).toBe(true);
|
|
134
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
135
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
136
|
+
correlationId: 'corr-2',
|
|
137
|
+
confirmed: false,
|
|
138
|
+
outcome: ToolConfirmationOutcome.ModifyWithEditor,
|
|
139
|
+
payload: undefined,
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
it('should handle MCP Server tool operations correctly', async () => {
|
|
143
|
+
// @ts-expect-error - Calling private constructor
|
|
144
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
145
|
+
const toolCall = {
|
|
146
|
+
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
147
|
+
status: 'awaiting_approval',
|
|
148
|
+
correlationId: 'corr-mcp-1',
|
|
149
|
+
confirmationDetails: {
|
|
150
|
+
type: 'mcp',
|
|
151
|
+
title: 'MCP Server Operation',
|
|
152
|
+
prompt: 'test_mcp',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
156
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
157
|
+
// Simulate ProceedOnce for MCP
|
|
158
|
+
const handled = await task._handleToolConfirmationPart({
|
|
159
|
+
kind: 'data',
|
|
160
|
+
data: { callId: '1', outcome: 'proceed_once' },
|
|
161
|
+
});
|
|
162
|
+
expect(handled).toBe(true);
|
|
163
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
164
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
165
|
+
correlationId: 'corr-mcp-1',
|
|
166
|
+
confirmed: true,
|
|
167
|
+
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
it('should handle MCP Server tool ProceedAlwaysServer outcome', async () => {
|
|
171
|
+
// @ts-expect-error - Calling private constructor
|
|
172
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
173
|
+
const toolCall = {
|
|
174
|
+
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
175
|
+
status: 'awaiting_approval',
|
|
176
|
+
correlationId: 'corr-mcp-2',
|
|
177
|
+
confirmationDetails: {
|
|
178
|
+
type: 'mcp',
|
|
179
|
+
title: 'MCP Server Operation',
|
|
180
|
+
prompt: 'test_mcp',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
184
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
185
|
+
const handled = await task._handleToolConfirmationPart({
|
|
186
|
+
kind: 'data',
|
|
187
|
+
data: { callId: '1', outcome: 'proceed_always_server' },
|
|
188
|
+
});
|
|
189
|
+
expect(handled).toBe(true);
|
|
190
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
191
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
192
|
+
correlationId: 'corr-mcp-2',
|
|
193
|
+
confirmed: true,
|
|
194
|
+
outcome: ToolConfirmationOutcome.ProceedAlwaysServer,
|
|
195
|
+
}));
|
|
196
|
+
});
|
|
197
|
+
it('should handle MCP Server tool ProceedAlwaysTool outcome', async () => {
|
|
198
|
+
// @ts-expect-error - Calling private constructor
|
|
199
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
200
|
+
const toolCall = {
|
|
201
|
+
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
202
|
+
status: 'awaiting_approval',
|
|
203
|
+
correlationId: 'corr-mcp-3',
|
|
204
|
+
confirmationDetails: {
|
|
205
|
+
type: 'mcp',
|
|
206
|
+
title: 'MCP Server Operation',
|
|
207
|
+
prompt: 'test_mcp',
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
211
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
212
|
+
const handled = await task._handleToolConfirmationPart({
|
|
213
|
+
kind: 'data',
|
|
214
|
+
data: { callId: '1', outcome: 'proceed_always_tool' },
|
|
215
|
+
});
|
|
216
|
+
expect(handled).toBe(true);
|
|
217
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
218
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
219
|
+
correlationId: 'corr-mcp-3',
|
|
220
|
+
confirmed: true,
|
|
221
|
+
outcome: ToolConfirmationOutcome.ProceedAlwaysTool,
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
it('should handle MCP Server tool ProceedAlwaysAndSave outcome', async () => {
|
|
225
|
+
// @ts-expect-error - Calling private constructor
|
|
226
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
227
|
+
const toolCall = {
|
|
228
|
+
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
229
|
+
status: 'awaiting_approval',
|
|
230
|
+
correlationId: 'corr-mcp-4',
|
|
231
|
+
confirmationDetails: {
|
|
232
|
+
type: 'mcp',
|
|
233
|
+
title: 'MCP Server Operation',
|
|
234
|
+
prompt: 'test_mcp',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
238
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
239
|
+
const handled = await task._handleToolConfirmationPart({
|
|
240
|
+
kind: 'data',
|
|
241
|
+
data: { callId: '1', outcome: 'proceed_always_and_save' },
|
|
242
|
+
});
|
|
243
|
+
expect(handled).toBe(true);
|
|
244
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
245
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
246
|
+
correlationId: 'corr-mcp-4',
|
|
247
|
+
confirmed: true,
|
|
248
|
+
outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
251
|
+
it('should execute without confirmation in YOLO mode and not transition to input-required', async () => {
|
|
252
|
+
// Enable YOLO mode
|
|
253
|
+
const yoloConfig = createMockConfig({
|
|
254
|
+
isEventDrivenSchedulerEnabled: () => true,
|
|
255
|
+
getApprovalMode: () => ApprovalMode.YOLO,
|
|
256
|
+
});
|
|
257
|
+
const yoloMessageBus = yoloConfig.getMessageBus();
|
|
258
|
+
// @ts-expect-error - Calling private constructor
|
|
259
|
+
const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);
|
|
260
|
+
task.setTaskStateAndPublishUpdate = vi.fn();
|
|
261
|
+
const toolCall = {
|
|
262
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
263
|
+
status: 'awaiting_approval',
|
|
264
|
+
correlationId: 'corr-1',
|
|
265
|
+
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
266
|
+
};
|
|
267
|
+
const handler = yoloMessageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
268
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
269
|
+
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
|
|
270
|
+
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(expect.objectContaining({
|
|
271
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
272
|
+
}));
|
|
273
|
+
// Should NOT transition to input-required since it was auto-approved
|
|
274
|
+
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
|
|
275
|
+
});
|
|
276
|
+
it('should handle output updates via the message bus', async () => {
|
|
277
|
+
// @ts-expect-error - Calling private constructor
|
|
278
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
279
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
280
|
+
const toolCall = {
|
|
281
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
282
|
+
status: 'executing',
|
|
283
|
+
liveOutput: 'chunk1',
|
|
284
|
+
};
|
|
285
|
+
// Simulate MessageBus event
|
|
286
|
+
// Simulate MessageBus event
|
|
287
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
288
|
+
if (!handler) {
|
|
289
|
+
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
290
|
+
}
|
|
291
|
+
handler({
|
|
292
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
293
|
+
toolCalls: [toolCall],
|
|
294
|
+
});
|
|
295
|
+
// Should publish artifact update for output
|
|
296
|
+
expect(mockEventBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
297
|
+
kind: 'artifact-update',
|
|
298
|
+
artifact: expect.objectContaining({
|
|
299
|
+
artifactId: 'tool-1-output',
|
|
300
|
+
parts: [{ kind: 'text', text: 'chunk1' }],
|
|
301
|
+
}),
|
|
302
|
+
}));
|
|
303
|
+
});
|
|
304
|
+
it('should complete artifact creation without hanging', async () => {
|
|
305
|
+
// @ts-expect-error - Calling private constructor
|
|
306
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
307
|
+
const toolCallId = 'create-file-123';
|
|
308
|
+
task['_registerToolCall'](toolCallId, 'executing');
|
|
309
|
+
const toolCall = {
|
|
310
|
+
request: {
|
|
311
|
+
callId: toolCallId,
|
|
312
|
+
name: 'writeFile',
|
|
313
|
+
args: { path: 'test.sh' },
|
|
314
|
+
},
|
|
315
|
+
status: 'success',
|
|
316
|
+
result: { ok: true },
|
|
317
|
+
};
|
|
318
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
319
|
+
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
|
320
|
+
// The tool should be complete and registered appropriately, eventually
|
|
321
|
+
// triggering the toolCompletionPromise resolution when all clear.
|
|
322
|
+
const internalTask = task;
|
|
323
|
+
expect(internalTask.completedToolCalls.length).toBe(1);
|
|
324
|
+
expect(internalTask.pendingToolCalls.size).toBe(0);
|
|
325
|
+
});
|
|
326
|
+
it('should preserve messageId across multiple text chunks to prevent UI duplication', async () => {
|
|
327
|
+
// @ts-expect-error - Calling private constructor
|
|
328
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
329
|
+
// Initialize the ID for the first turn (happens internally upon LLM stream)
|
|
330
|
+
task.currentAgentMessageId = 'test-id-123';
|
|
331
|
+
// Simulate sending multiple text chunks
|
|
332
|
+
task._sendTextContent('chunk 1');
|
|
333
|
+
task._sendTextContent('chunk 2');
|
|
334
|
+
// Both text contents should have been published with the same messageId
|
|
335
|
+
const textCalls = mockEventBus.publish.mock.calls.filter((call) => call[0].status?.message?.kind === 'message');
|
|
336
|
+
expect(textCalls.length).toBe(2);
|
|
337
|
+
expect(textCalls[0][0].status.message.messageId).toBe('test-id-123');
|
|
338
|
+
expect(textCalls[1][0].status.message.messageId).toBe('test-id-123');
|
|
339
|
+
// Simulate starting a new turn by calling getAndClearCompletedTools
|
|
340
|
+
// (which precedes sendCompletedToolsToLlm where a new ID is minted)
|
|
341
|
+
task.getAndClearCompletedTools();
|
|
342
|
+
// sendCompletedToolsToLlm internally rolls the ID forward.
|
|
343
|
+
// Simulate what sendCompletedToolsToLlm does:
|
|
344
|
+
const internalTask = task;
|
|
345
|
+
internalTask.setTaskStateAndPublishUpdate('working', {});
|
|
346
|
+
// Simulate what sendCompletedToolsToLlm does: generate a new UUID for the next turn
|
|
347
|
+
task.currentAgentMessageId = 'test-id-456';
|
|
348
|
+
task._sendTextContent('chunk 3');
|
|
349
|
+
const secondTurnCalls = mockEventBus.publish.mock.calls.filter((call) => call[0].status?.message?.messageId === 'test-id-456');
|
|
350
|
+
expect(secondTurnCalls.length).toBe(1);
|
|
351
|
+
expect(secondTurnCalls[0][0].status.message.parts[0].text).toBe('chunk 3');
|
|
352
|
+
});
|
|
353
|
+
it('should handle parallel tool calls correctly', async () => {
|
|
354
|
+
// @ts-expect-error - Calling private constructor
|
|
355
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
356
|
+
const toolCall1 = {
|
|
357
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
358
|
+
status: 'awaiting_approval',
|
|
359
|
+
correlationId: 'corr-1',
|
|
360
|
+
confirmationDetails: { type: 'info', title: 'test 1', prompt: 'test 1' },
|
|
361
|
+
};
|
|
362
|
+
const toolCall2 = {
|
|
363
|
+
request: { callId: '2', name: 'pwd', args: {} },
|
|
364
|
+
status: 'awaiting_approval',
|
|
365
|
+
correlationId: 'corr-2',
|
|
366
|
+
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
|
367
|
+
};
|
|
368
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
369
|
+
// Publish update for both tool calls simultaneously
|
|
370
|
+
handler({
|
|
371
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
372
|
+
toolCalls: [toolCall1, toolCall2],
|
|
373
|
+
});
|
|
374
|
+
// Confirm first tool call
|
|
375
|
+
const handled1 = await task._handleToolConfirmationPart({
|
|
376
|
+
kind: 'data',
|
|
377
|
+
data: { callId: '1', outcome: 'proceed_once' },
|
|
378
|
+
});
|
|
379
|
+
expect(handled1).toBe(true);
|
|
380
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
381
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
382
|
+
correlationId: 'corr-1',
|
|
383
|
+
confirmed: true,
|
|
384
|
+
}));
|
|
385
|
+
// Confirm second tool call
|
|
386
|
+
const handled2 = await task._handleToolConfirmationPart({
|
|
387
|
+
kind: 'data',
|
|
388
|
+
data: { callId: '2', outcome: 'cancel' },
|
|
389
|
+
});
|
|
390
|
+
expect(handled2).toBe(true);
|
|
391
|
+
expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
392
|
+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
393
|
+
correlationId: 'corr-2',
|
|
394
|
+
confirmed: false,
|
|
395
|
+
}));
|
|
396
|
+
});
|
|
397
|
+
it('should wait for executing tools before transitioning to input-required state', async () => {
|
|
398
|
+
// @ts-expect-error - Calling private constructor
|
|
399
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
400
|
+
task.setTaskStateAndPublishUpdate = vi.fn();
|
|
401
|
+
// Register tool 1 as executing
|
|
402
|
+
task['_registerToolCall']('1', 'executing');
|
|
403
|
+
const toolCall1 = {
|
|
404
|
+
request: { callId: '1', name: 'ls', args: {} },
|
|
405
|
+
status: 'executing',
|
|
406
|
+
};
|
|
407
|
+
const toolCall2 = {
|
|
408
|
+
request: { callId: '2', name: 'pwd', args: {} },
|
|
409
|
+
status: 'awaiting_approval',
|
|
410
|
+
correlationId: 'corr-2',
|
|
411
|
+
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
|
412
|
+
};
|
|
413
|
+
const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
|
|
414
|
+
handler({
|
|
415
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
416
|
+
toolCalls: [toolCall1, toolCall2],
|
|
417
|
+
});
|
|
418
|
+
// Should NOT transition to input-required yet
|
|
419
|
+
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
|
|
420
|
+
// Complete tool 1
|
|
421
|
+
const toolCall1Complete = {
|
|
422
|
+
...toolCall1,
|
|
423
|
+
status: 'success',
|
|
424
|
+
result: { ok: true },
|
|
425
|
+
};
|
|
426
|
+
handler({
|
|
427
|
+
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
428
|
+
toolCalls: [toolCall1Complete, toolCall2],
|
|
429
|
+
});
|
|
430
|
+
// Now it should transition
|
|
431
|
+
expect(task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
|
|
432
|
+
});
|
|
433
|
+
it('should ignore confirmations for unknown tool calls', async () => {
|
|
434
|
+
// @ts-expect-error - Calling private constructor
|
|
435
|
+
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
436
|
+
const handled = await task._handleToolConfirmationPart({
|
|
437
|
+
kind: 'data',
|
|
438
|
+
data: { callId: 'unknown-id', outcome: 'proceed_once' },
|
|
439
|
+
});
|
|
440
|
+
// Should return false for unhandled tool call
|
|
441
|
+
expect(handled).toBe(false);
|
|
442
|
+
// Should not publish anything to the message bus
|
|
443
|
+
expect(messageBus.publish).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
//# sourceMappingURL=task-event-driven.test.js.map
|