@google/gemini-cli-a2a-server 0.10.0-nightly.20251014.49b66733
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/README.md +5 -0
- package/dist/.last_build +0 -0
- package/dist/a2a-server.mjs +354316 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/src/agent/executor.d.ts +40 -0
- package/dist/src/agent/executor.js +406 -0
- package/dist/src/agent/executor.js.map +1 -0
- package/dist/src/agent/task.d.ts +54 -0
- package/dist/src/agent/task.js +652 -0
- package/dist/src/agent/task.js.map +1 -0
- package/dist/src/agent/task.test.d.ts +6 -0
- package/dist/src/agent/task.test.js +44 -0
- package/dist/src/agent/task.test.js.map +1 -0
- package/dist/src/config/config.d.ts +14 -0
- package/dist/src/config/config.js +150 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/extension.d.ts +11 -0
- package/dist/src/config/extension.js +104 -0
- package/dist/src/config/extension.js.map +1 -0
- package/dist/src/config/settings.d.ts +38 -0
- package/dist/src/config/settings.js +102 -0
- package/dist/src/config/settings.js.map +1 -0
- package/dist/src/http/app.d.ts +8 -0
- package/dist/src/http/app.js +170 -0
- package/dist/src/http/app.js.map +1 -0
- package/dist/src/http/app.test.d.ts +6 -0
- package/dist/src/http/app.test.js +520 -0
- package/dist/src/http/app.test.js.map +1 -0
- package/dist/src/http/endpoints.test.d.ts +6 -0
- package/dist/src/http/endpoints.test.js +138 -0
- package/dist/src/http/endpoints.test.js.map +1 -0
- package/dist/src/http/requestStorage.d.ts +10 -0
- package/dist/src/http/requestStorage.js +8 -0
- package/dist/src/http/requestStorage.js.map +1 -0
- package/dist/src/http/server.d.ts +7 -0
- package/dist/src/http/server.js +26 -0
- package/dist/src/http/server.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/persistence/gcs.d.ts +24 -0
- package/dist/src/persistence/gcs.js +238 -0
- package/dist/src/persistence/gcs.js.map +1 -0
- package/dist/src/persistence/gcs.test.d.ts +6 -0
- package/dist/src/persistence/gcs.test.js +265 -0
- package/dist/src/persistence/gcs.test.js.map +1 -0
- package/dist/src/types.d.ts +91 -0
- package/dist/src/types.js +44 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/executor_utils.d.ts +7 -0
- package/dist/src/utils/executor_utils.js +41 -0
- package/dist/src/utils/executor_utils.js.map +1 -0
- package/dist/src/utils/logger.d.ts +8 -0
- package/dist/src/utils/logger.js +23 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/testing_utils.d.ts +33 -0
- package/dist/src/utils/testing_utils.js +91 -0
- package/dist/src/utils/testing_utils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { CoreToolScheduler, GeminiEventType, ToolConfirmationOutcome, ApprovalMode, getAllMCPServerStatuses, MCPServerStatus, isNodeError, parseAndFormatApiError, safeLiteralReplace, } from '@google/gemini-cli-core';
|
|
7
|
+
import {} from '@a2a-js/sdk/server';
|
|
8
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import { CoderAgentEvent } from '../types.js';
|
|
12
|
+
export class Task {
|
|
13
|
+
id;
|
|
14
|
+
contextId;
|
|
15
|
+
scheduler;
|
|
16
|
+
config;
|
|
17
|
+
geminiClient;
|
|
18
|
+
pendingToolConfirmationDetails;
|
|
19
|
+
taskState;
|
|
20
|
+
eventBus;
|
|
21
|
+
completedToolCalls;
|
|
22
|
+
skipFinalTrueAfterInlineEdit = false;
|
|
23
|
+
// For tool waiting logic
|
|
24
|
+
pendingToolCalls = new Map(); //toolCallId --> status
|
|
25
|
+
toolCompletionPromise;
|
|
26
|
+
toolCompletionNotifier;
|
|
27
|
+
constructor(id, contextId, config, eventBus) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.contextId = contextId;
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.scheduler = this.createScheduler();
|
|
32
|
+
this.geminiClient = this.config.getGeminiClient();
|
|
33
|
+
this.pendingToolConfirmationDetails = new Map();
|
|
34
|
+
this.taskState = 'submitted';
|
|
35
|
+
this.eventBus = eventBus;
|
|
36
|
+
this.completedToolCalls = [];
|
|
37
|
+
this._resetToolCompletionPromise();
|
|
38
|
+
this.config.setFallbackModelHandler(
|
|
39
|
+
// For a2a-server, we want to automatically switch to the fallback model
|
|
40
|
+
// for future requests without retrying the current one. The 'stop'
|
|
41
|
+
// intent achieves this.
|
|
42
|
+
async () => 'stop');
|
|
43
|
+
}
|
|
44
|
+
static async create(id, contextId, config, eventBus) {
|
|
45
|
+
return new Task(id, contextId, config, eventBus);
|
|
46
|
+
}
|
|
47
|
+
// Note: `getAllMCPServerStatuses` retrieves the status of all MCP servers for the entire
|
|
48
|
+
// process. This is not scoped to the individual task but reflects the global connection
|
|
49
|
+
// state managed within the @gemini-cli/core module.
|
|
50
|
+
async getMetadata() {
|
|
51
|
+
const toolRegistry = await this.config.getToolRegistry();
|
|
52
|
+
const mcpServers = this.config.getMcpServers() || {};
|
|
53
|
+
const serverStatuses = getAllMCPServerStatuses();
|
|
54
|
+
const servers = Object.keys(mcpServers).map((serverName) => ({
|
|
55
|
+
name: serverName,
|
|
56
|
+
status: serverStatuses.get(serverName) || MCPServerStatus.DISCONNECTED,
|
|
57
|
+
tools: toolRegistry.getToolsByServer(serverName).map((tool) => ({
|
|
58
|
+
name: tool.name,
|
|
59
|
+
description: tool.description,
|
|
60
|
+
parameterSchema: tool.schema.parameters,
|
|
61
|
+
})),
|
|
62
|
+
}));
|
|
63
|
+
const availableTools = toolRegistry.getAllTools().map((tool) => ({
|
|
64
|
+
name: tool.name,
|
|
65
|
+
description: tool.description,
|
|
66
|
+
parameterSchema: tool.schema.parameters,
|
|
67
|
+
}));
|
|
68
|
+
const metadata = {
|
|
69
|
+
id: this.id,
|
|
70
|
+
contextId: this.contextId,
|
|
71
|
+
taskState: this.taskState,
|
|
72
|
+
model: this.config.getModel(),
|
|
73
|
+
mcpServers: servers,
|
|
74
|
+
availableTools,
|
|
75
|
+
};
|
|
76
|
+
return metadata;
|
|
77
|
+
}
|
|
78
|
+
_resetToolCompletionPromise() {
|
|
79
|
+
this.toolCompletionPromise = new Promise((resolve, reject) => {
|
|
80
|
+
this.toolCompletionNotifier = { resolve, reject };
|
|
81
|
+
});
|
|
82
|
+
// If there are no pending calls when reset, resolve immediately.
|
|
83
|
+
if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) {
|
|
84
|
+
this.toolCompletionNotifier.resolve();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
_registerToolCall(toolCallId, status) {
|
|
88
|
+
const wasEmpty = this.pendingToolCalls.size === 0;
|
|
89
|
+
this.pendingToolCalls.set(toolCallId, status);
|
|
90
|
+
if (wasEmpty) {
|
|
91
|
+
this._resetToolCompletionPromise();
|
|
92
|
+
}
|
|
93
|
+
logger.info(`[Task] Registered tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`);
|
|
94
|
+
}
|
|
95
|
+
_resolveToolCall(toolCallId) {
|
|
96
|
+
if (this.pendingToolCalls.has(toolCallId)) {
|
|
97
|
+
this.pendingToolCalls.delete(toolCallId);
|
|
98
|
+
logger.info(`[Task] Resolved tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`);
|
|
99
|
+
if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) {
|
|
100
|
+
this.toolCompletionNotifier.resolve();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async waitForPendingTools() {
|
|
105
|
+
if (this.pendingToolCalls.size === 0) {
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
108
|
+
logger.info(`[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`);
|
|
109
|
+
return this.toolCompletionPromise;
|
|
110
|
+
}
|
|
111
|
+
cancelPendingTools(reason) {
|
|
112
|
+
if (this.pendingToolCalls.size > 0) {
|
|
113
|
+
logger.info(`[Task] Cancelling all ${this.pendingToolCalls.size} pending tool calls. Reason: ${reason}`);
|
|
114
|
+
}
|
|
115
|
+
if (this.toolCompletionNotifier) {
|
|
116
|
+
this.toolCompletionNotifier.reject(new Error(reason));
|
|
117
|
+
}
|
|
118
|
+
this.pendingToolCalls.clear();
|
|
119
|
+
// Reset the promise for any future operations, ensuring it's in a clean state.
|
|
120
|
+
this._resetToolCompletionPromise();
|
|
121
|
+
}
|
|
122
|
+
_createTextMessage(text, role = 'agent') {
|
|
123
|
+
return {
|
|
124
|
+
kind: 'message',
|
|
125
|
+
role,
|
|
126
|
+
parts: [{ kind: 'text', text }],
|
|
127
|
+
messageId: uuidv4(),
|
|
128
|
+
taskId: this.id,
|
|
129
|
+
contextId: this.contextId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
_createStatusUpdateEvent(stateToReport, coderAgentMessage, message, final = false, timestamp, metadataError) {
|
|
133
|
+
const metadata = {
|
|
134
|
+
coderAgent: coderAgentMessage,
|
|
135
|
+
model: this.config.getModel(),
|
|
136
|
+
userTier: this.config.getUserTier(),
|
|
137
|
+
};
|
|
138
|
+
if (metadataError) {
|
|
139
|
+
metadata.error = metadataError;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
kind: 'status-update',
|
|
143
|
+
taskId: this.id,
|
|
144
|
+
contextId: this.contextId,
|
|
145
|
+
status: {
|
|
146
|
+
state: stateToReport,
|
|
147
|
+
message, // Shorthand property
|
|
148
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
149
|
+
},
|
|
150
|
+
final,
|
|
151
|
+
metadata,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
setTaskStateAndPublishUpdate(newState, coderAgentMessage, messageText, messageParts, // For more complex messages
|
|
155
|
+
final = false, metadataError) {
|
|
156
|
+
this.taskState = newState;
|
|
157
|
+
let message;
|
|
158
|
+
if (messageText) {
|
|
159
|
+
message = this._createTextMessage(messageText);
|
|
160
|
+
}
|
|
161
|
+
else if (messageParts) {
|
|
162
|
+
message = {
|
|
163
|
+
kind: 'message',
|
|
164
|
+
role: 'agent',
|
|
165
|
+
parts: messageParts,
|
|
166
|
+
messageId: uuidv4(),
|
|
167
|
+
taskId: this.id,
|
|
168
|
+
contextId: this.contextId,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const event = this._createStatusUpdateEvent(this.taskState, coderAgentMessage, message, final, undefined, metadataError);
|
|
172
|
+
this.eventBus?.publish(event);
|
|
173
|
+
}
|
|
174
|
+
_schedulerOutputUpdate(toolCallId, outputChunk) {
|
|
175
|
+
let outputAsText;
|
|
176
|
+
if (typeof outputChunk === 'string') {
|
|
177
|
+
outputAsText = outputChunk;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
outputAsText = outputChunk
|
|
181
|
+
.map((line) => line.map((token) => token.text).join(''))
|
|
182
|
+
.join('\n');
|
|
183
|
+
}
|
|
184
|
+
logger.info('[Task] Scheduler output update for tool call ' +
|
|
185
|
+
toolCallId +
|
|
186
|
+
': ' +
|
|
187
|
+
outputAsText);
|
|
188
|
+
const artifact = {
|
|
189
|
+
artifactId: `tool-${toolCallId}-output`,
|
|
190
|
+
parts: [
|
|
191
|
+
{
|
|
192
|
+
kind: 'text',
|
|
193
|
+
text: outputAsText,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
const artifactEvent = {
|
|
198
|
+
kind: 'artifact-update',
|
|
199
|
+
taskId: this.id,
|
|
200
|
+
contextId: this.contextId,
|
|
201
|
+
artifact,
|
|
202
|
+
append: true,
|
|
203
|
+
lastChunk: false,
|
|
204
|
+
};
|
|
205
|
+
this.eventBus?.publish(artifactEvent);
|
|
206
|
+
}
|
|
207
|
+
async _schedulerAllToolCallsComplete(completedToolCalls) {
|
|
208
|
+
logger.info('[Task] All tool calls completed by scheduler (batch):', completedToolCalls.map((tc) => tc.request.callId));
|
|
209
|
+
this.completedToolCalls.push(...completedToolCalls);
|
|
210
|
+
completedToolCalls.forEach((tc) => {
|
|
211
|
+
this._resolveToolCall(tc.request.callId);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
_schedulerToolCallsUpdate(toolCalls) {
|
|
215
|
+
logger.info('[Task] Scheduler tool calls updated:', toolCalls.map((tc) => `${tc.request.callId} (${tc.status})`));
|
|
216
|
+
// Update state and send continuous, non-final updates
|
|
217
|
+
toolCalls.forEach((tc) => {
|
|
218
|
+
const previousStatus = this.pendingToolCalls.get(tc.request.callId);
|
|
219
|
+
const hasChanged = previousStatus !== tc.status;
|
|
220
|
+
// Resolve tool call if it has reached a terminal state
|
|
221
|
+
if (['success', 'error', 'cancelled'].includes(tc.status)) {
|
|
222
|
+
this._resolveToolCall(tc.request.callId);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// This will update the map
|
|
226
|
+
this._registerToolCall(tc.request.callId, tc.status);
|
|
227
|
+
}
|
|
228
|
+
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
|
229
|
+
this.pendingToolConfirmationDetails.set(tc.request.callId, tc.confirmationDetails);
|
|
230
|
+
}
|
|
231
|
+
// Only send an update if the status has actually changed.
|
|
232
|
+
if (hasChanged) {
|
|
233
|
+
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
|
234
|
+
const coderAgentMessage = tc.status === 'awaiting_approval'
|
|
235
|
+
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
|
236
|
+
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
|
237
|
+
const event = this._createStatusUpdateEvent(this.taskState, coderAgentMessage, message, false);
|
|
238
|
+
this.eventBus?.publish(event);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
if (this.config.getApprovalMode() === ApprovalMode.YOLO) {
|
|
242
|
+
logger.info('[Task] YOLO mode enabled. Auto-approving all tool calls.');
|
|
243
|
+
toolCalls.forEach((tc) => {
|
|
244
|
+
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
|
245
|
+
tc.confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
|
246
|
+
this.pendingToolConfirmationDetails.delete(tc.request.callId);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const allPendingStatuses = Array.from(this.pendingToolCalls.values());
|
|
252
|
+
const isAwaitingApproval = allPendingStatuses.some((status) => status === 'awaiting_approval');
|
|
253
|
+
const allPendingAreStable = allPendingStatuses.every((status) => status === 'awaiting_approval' ||
|
|
254
|
+
status === 'success' ||
|
|
255
|
+
status === 'error' ||
|
|
256
|
+
status === 'cancelled');
|
|
257
|
+
// 1. Are any pending tool calls awaiting_approval
|
|
258
|
+
// 2. Are all pending tool calls in a stable state (i.e. not in validing or executing)
|
|
259
|
+
// 3. After an inline edit, the edited tool call will send awaiting_approval THEN scheduled. We wait for the next update in this case.
|
|
260
|
+
if (isAwaitingApproval &&
|
|
261
|
+
allPendingAreStable &&
|
|
262
|
+
!this.skipFinalTrueAfterInlineEdit) {
|
|
263
|
+
this.skipFinalTrueAfterInlineEdit = false;
|
|
264
|
+
// We don't need to send another message, just a final status update.
|
|
265
|
+
this.setTaskStateAndPublishUpdate('input-required', { kind: CoderAgentEvent.StateChangeEvent }, undefined, undefined,
|
|
266
|
+
/*final*/ true);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
createScheduler() {
|
|
270
|
+
const scheduler = new CoreToolScheduler({
|
|
271
|
+
outputUpdateHandler: this._schedulerOutputUpdate.bind(this),
|
|
272
|
+
onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this),
|
|
273
|
+
onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this),
|
|
274
|
+
getPreferredEditor: () => 'vscode',
|
|
275
|
+
config: this.config,
|
|
276
|
+
onEditorClose: () => { },
|
|
277
|
+
});
|
|
278
|
+
return scheduler;
|
|
279
|
+
}
|
|
280
|
+
_pickFields(from, ...fields) {
|
|
281
|
+
const ret = {};
|
|
282
|
+
for (const field of fields) {
|
|
283
|
+
if (field in from) {
|
|
284
|
+
ret[field] = from[field];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return ret;
|
|
288
|
+
}
|
|
289
|
+
toolStatusMessage(tc, taskId, contextId) {
|
|
290
|
+
const messageParts = [];
|
|
291
|
+
// Create a serializable version of the ToolCall (pick necesssary
|
|
292
|
+
// properties/avoid methods causing circular reference errors)
|
|
293
|
+
const serializableToolCall = this._pickFields(tc, 'request', 'status', 'confirmationDetails', 'liveOutput', 'response');
|
|
294
|
+
if (tc.tool) {
|
|
295
|
+
serializableToolCall.tool = this._pickFields(tc.tool, 'name', 'displayName', 'description', 'kind', 'isOutputMarkdown', 'canUpdateOutput', 'schema', 'parameterSchema');
|
|
296
|
+
}
|
|
297
|
+
messageParts.push({
|
|
298
|
+
kind: 'data',
|
|
299
|
+
data: serializableToolCall,
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
kind: 'message',
|
|
303
|
+
role: 'agent',
|
|
304
|
+
parts: messageParts,
|
|
305
|
+
messageId: uuidv4(),
|
|
306
|
+
taskId,
|
|
307
|
+
contextId,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async getProposedContent(file_path, old_string, new_string) {
|
|
311
|
+
try {
|
|
312
|
+
const currentContent = fs.readFileSync(file_path, 'utf8');
|
|
313
|
+
return this._applyReplacement(currentContent, old_string, new_string, old_string === '' && currentContent === '');
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (!isNodeError(err) || err.code !== 'ENOENT')
|
|
317
|
+
throw err;
|
|
318
|
+
return '';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
_applyReplacement(currentContent, oldString, newString, isNewFile) {
|
|
322
|
+
if (isNewFile) {
|
|
323
|
+
return newString;
|
|
324
|
+
}
|
|
325
|
+
if (currentContent === null) {
|
|
326
|
+
// Should not happen if not a new file, but defensively return empty or newString if oldString is also empty
|
|
327
|
+
return oldString === '' ? newString : '';
|
|
328
|
+
}
|
|
329
|
+
// If oldString is empty and it's not a new file, do not modify the content.
|
|
330
|
+
if (oldString === '' && !isNewFile) {
|
|
331
|
+
return currentContent;
|
|
332
|
+
}
|
|
333
|
+
// Use intelligent replacement that handles $ sequences safely
|
|
334
|
+
return safeLiteralReplace(currentContent, oldString, newString);
|
|
335
|
+
}
|
|
336
|
+
async scheduleToolCalls(requests, abortSignal) {
|
|
337
|
+
if (requests.length === 0) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const updatedRequests = await Promise.all(requests.map(async (request) => {
|
|
341
|
+
if (request.name === 'replace' &&
|
|
342
|
+
request.args &&
|
|
343
|
+
!request.args['newContent'] &&
|
|
344
|
+
request.args['file_path'] &&
|
|
345
|
+
request.args['old_string'] &&
|
|
346
|
+
request.args['new_string']) {
|
|
347
|
+
const newContent = await this.getProposedContent(request.args['file_path'], request.args['old_string'], request.args['new_string']);
|
|
348
|
+
return { ...request, args: { ...request.args, newContent } };
|
|
349
|
+
}
|
|
350
|
+
return request;
|
|
351
|
+
}));
|
|
352
|
+
logger.info(`[Task] Scheduling batch of ${updatedRequests.length} tool calls.`);
|
|
353
|
+
const stateChange = {
|
|
354
|
+
kind: CoderAgentEvent.StateChangeEvent,
|
|
355
|
+
};
|
|
356
|
+
this.setTaskStateAndPublishUpdate('working', stateChange);
|
|
357
|
+
await this.scheduler.schedule(updatedRequests, abortSignal);
|
|
358
|
+
}
|
|
359
|
+
async acceptAgentMessage(event) {
|
|
360
|
+
const stateChange = {
|
|
361
|
+
kind: CoderAgentEvent.StateChangeEvent,
|
|
362
|
+
};
|
|
363
|
+
switch (event.type) {
|
|
364
|
+
case GeminiEventType.Content:
|
|
365
|
+
logger.info('[Task] Sending agent message content...');
|
|
366
|
+
this._sendTextContent(event.value);
|
|
367
|
+
break;
|
|
368
|
+
case GeminiEventType.ToolCallRequest:
|
|
369
|
+
// This is now handled by the agent loop, which collects all requests
|
|
370
|
+
// and calls scheduleToolCalls once.
|
|
371
|
+
logger.warn('[Task] A single tool call request was passed to acceptAgentMessage. This should be handled in a batch by the agent. Ignoring.');
|
|
372
|
+
break;
|
|
373
|
+
case GeminiEventType.ToolCallResponse:
|
|
374
|
+
// This event type from ServerGeminiStreamEvent might be for when LLM *generates* a tool response part.
|
|
375
|
+
// The actual execution result comes via user message.
|
|
376
|
+
logger.info('[Task] Received tool call response from LLM (part of generation):', event.value);
|
|
377
|
+
break;
|
|
378
|
+
case GeminiEventType.ToolCallConfirmation:
|
|
379
|
+
// This is when LLM requests confirmation, not when user provides it.
|
|
380
|
+
logger.info('[Task] Received tool call confirmation request from LLM:', event.value.request.callId);
|
|
381
|
+
this.pendingToolConfirmationDetails.set(event.value.request.callId, event.value.details);
|
|
382
|
+
// This will be handled by the scheduler and _schedulerToolCallsUpdate will set InputRequired if needed.
|
|
383
|
+
// No direct state change here, scheduler drives it.
|
|
384
|
+
break;
|
|
385
|
+
case GeminiEventType.UserCancelled:
|
|
386
|
+
logger.info('[Task] Received user cancelled event from LLM stream.');
|
|
387
|
+
this.cancelPendingTools('User cancelled via LLM stream event');
|
|
388
|
+
this.setTaskStateAndPublishUpdate('input-required', stateChange, 'Task cancelled by user', undefined, true);
|
|
389
|
+
break;
|
|
390
|
+
case GeminiEventType.Thought:
|
|
391
|
+
logger.info('[Task] Sending agent thought...');
|
|
392
|
+
this._sendThought(event.value);
|
|
393
|
+
break;
|
|
394
|
+
case GeminiEventType.ChatCompressed:
|
|
395
|
+
break;
|
|
396
|
+
case GeminiEventType.Finished:
|
|
397
|
+
logger.info(`[Task ${this.id}] Agent finished its turn.`);
|
|
398
|
+
break;
|
|
399
|
+
case GeminiEventType.Error:
|
|
400
|
+
default: {
|
|
401
|
+
// Block scope for lexical declaration
|
|
402
|
+
const errorEvent = event; // Type assertion
|
|
403
|
+
const errorMessage = errorEvent.value?.error.message ?? 'Unknown error from LLM stream';
|
|
404
|
+
logger.error('[Task] Received error event from LLM stream:', errorMessage);
|
|
405
|
+
let errMessage = 'Unknown error from LLM stream';
|
|
406
|
+
if (errorEvent.value) {
|
|
407
|
+
errMessage = parseAndFormatApiError(errorEvent.value);
|
|
408
|
+
}
|
|
409
|
+
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
|
|
410
|
+
this.setTaskStateAndPublishUpdate(this.taskState, stateChange, `Agent Error, unknown agent message: ${errorMessage}`, undefined, false, errMessage);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async _handleToolConfirmationPart(part) {
|
|
416
|
+
if (part.kind !== 'data' ||
|
|
417
|
+
!part.data ||
|
|
418
|
+
typeof part.data['callId'] !== 'string' ||
|
|
419
|
+
typeof part.data['outcome'] !== 'string') {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const callId = part.data['callId'];
|
|
423
|
+
const outcomeString = part.data['outcome'];
|
|
424
|
+
let confirmationOutcome;
|
|
425
|
+
if (outcomeString === 'proceed_once') {
|
|
426
|
+
confirmationOutcome = ToolConfirmationOutcome.ProceedOnce;
|
|
427
|
+
}
|
|
428
|
+
else if (outcomeString === 'cancel') {
|
|
429
|
+
confirmationOutcome = ToolConfirmationOutcome.Cancel;
|
|
430
|
+
}
|
|
431
|
+
else if (outcomeString === 'proceed_always') {
|
|
432
|
+
confirmationOutcome = ToolConfirmationOutcome.ProceedAlways;
|
|
433
|
+
}
|
|
434
|
+
else if (outcomeString === 'proceed_always_server') {
|
|
435
|
+
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysServer;
|
|
436
|
+
}
|
|
437
|
+
else if (outcomeString === 'proceed_always_tool') {
|
|
438
|
+
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysTool;
|
|
439
|
+
}
|
|
440
|
+
else if (outcomeString === 'modify_with_editor') {
|
|
441
|
+
confirmationOutcome = ToolConfirmationOutcome.ModifyWithEditor;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
logger.warn(`[Task] Unknown tool confirmation outcome: "${outcomeString}" for callId: ${callId}`);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const confirmationDetails = this.pendingToolConfirmationDetails.get(callId);
|
|
448
|
+
if (!confirmationDetails) {
|
|
449
|
+
logger.warn(`[Task] Received tool confirmation for unknown or already processed callId: ${callId}`);
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
logger.info(`[Task] Handling tool confirmation for callId: ${callId} with outcome: ${outcomeString}`);
|
|
453
|
+
try {
|
|
454
|
+
// Temporarily unset GCP environment variables so they do not leak into
|
|
455
|
+
// tool calls.
|
|
456
|
+
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'];
|
|
457
|
+
const gcpCreds = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
|
458
|
+
try {
|
|
459
|
+
delete process.env['GOOGLE_CLOUD_PROJECT'];
|
|
460
|
+
delete process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
|
461
|
+
// This will trigger the scheduler to continue or cancel the specific tool.
|
|
462
|
+
// The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled).
|
|
463
|
+
// If `edit` tool call, pass updated payload if presesent
|
|
464
|
+
if (confirmationDetails.type === 'edit') {
|
|
465
|
+
const payload = part.data['newContent']
|
|
466
|
+
? {
|
|
467
|
+
newContent: part.data['newContent'],
|
|
468
|
+
}
|
|
469
|
+
: undefined;
|
|
470
|
+
this.skipFinalTrueAfterInlineEdit = !!payload;
|
|
471
|
+
await confirmationDetails.onConfirm(confirmationOutcome, payload);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
await confirmationDetails.onConfirm(confirmationOutcome);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
if (gcpProject) {
|
|
479
|
+
process.env['GOOGLE_CLOUD_PROJECT'] = gcpProject;
|
|
480
|
+
}
|
|
481
|
+
if (gcpCreds) {
|
|
482
|
+
process.env['GOOGLE_APPLICATION_CREDENTIALS'] = gcpCreds;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Do not delete if modifying, a subsequent tool confirmation for the same
|
|
486
|
+
// callId will be passed with ProceedOnce/Cancel/etc
|
|
487
|
+
// Note !== ToolConfirmationOutcome.ModifyWithEditor does not work!
|
|
488
|
+
if (confirmationOutcome !== 'modify_with_editor') {
|
|
489
|
+
this.pendingToolConfirmationDetails.delete(callId);
|
|
490
|
+
}
|
|
491
|
+
// If outcome is Cancel, scheduler should update status to 'cancelled', which then resolves the tool.
|
|
492
|
+
// If ProceedOnce, scheduler updates to 'executing', then eventually 'success'/'error', which resolves.
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
logger.error(`[Task] Error during tool confirmation for callId ${callId}:`, error);
|
|
497
|
+
// If confirming fails, we should probably mark this tool as failed
|
|
498
|
+
this._resolveToolCall(callId); // Resolve it as it won't proceed.
|
|
499
|
+
const errorMessageText = error instanceof Error
|
|
500
|
+
? error.message
|
|
501
|
+
: `Error processing tool confirmation for ${callId}`;
|
|
502
|
+
const message = this._createTextMessage(errorMessageText);
|
|
503
|
+
const toolCallUpdate = {
|
|
504
|
+
kind: CoderAgentEvent.ToolCallUpdateEvent,
|
|
505
|
+
};
|
|
506
|
+
const event = this._createStatusUpdateEvent(this.taskState, toolCallUpdate, message, false);
|
|
507
|
+
this.eventBus?.publish(event);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
getAndClearCompletedTools() {
|
|
512
|
+
const tools = [...this.completedToolCalls];
|
|
513
|
+
this.completedToolCalls = [];
|
|
514
|
+
return tools;
|
|
515
|
+
}
|
|
516
|
+
addToolResponsesToHistory(completedTools) {
|
|
517
|
+
logger.info(`[Task] Adding ${completedTools.length} tool responses to history without generating a new response.`);
|
|
518
|
+
const responsesToAdd = completedTools.flatMap((toolCall) => toolCall.response.responseParts);
|
|
519
|
+
for (const response of responsesToAdd) {
|
|
520
|
+
let parts;
|
|
521
|
+
if (Array.isArray(response)) {
|
|
522
|
+
parts = response;
|
|
523
|
+
}
|
|
524
|
+
else if (typeof response === 'string') {
|
|
525
|
+
parts = [{ text: response }];
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
parts = [response];
|
|
529
|
+
}
|
|
530
|
+
this.geminiClient.addHistory({
|
|
531
|
+
role: 'user',
|
|
532
|
+
parts,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async *sendCompletedToolsToLlm(completedToolCalls, aborted) {
|
|
537
|
+
if (completedToolCalls.length === 0) {
|
|
538
|
+
yield* (async function* () { })(); // Yield nothing
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const llmParts = [];
|
|
542
|
+
logger.info(`[Task] Feeding ${completedToolCalls.length} tool responses to LLM.`);
|
|
543
|
+
for (const completedToolCall of completedToolCalls) {
|
|
544
|
+
logger.info(`[Task] Adding tool response for "${completedToolCall.request.name}" (callId: ${completedToolCall.request.callId}) to LLM input.`);
|
|
545
|
+
const responseParts = completedToolCall.response.responseParts;
|
|
546
|
+
if (Array.isArray(responseParts)) {
|
|
547
|
+
llmParts.push(...responseParts);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
llmParts.push(responseParts);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
logger.info('[Task] Sending new parts to agent.');
|
|
554
|
+
const stateChange = {
|
|
555
|
+
kind: CoderAgentEvent.StateChangeEvent,
|
|
556
|
+
};
|
|
557
|
+
// Set task state to working as we are about to call LLM
|
|
558
|
+
this.setTaskStateAndPublishUpdate('working', stateChange);
|
|
559
|
+
// TODO: Determine what it mean to have, then add a prompt ID.
|
|
560
|
+
yield* this.geminiClient.sendMessageStream(llmParts, aborted,
|
|
561
|
+
/*prompt_id*/ '');
|
|
562
|
+
}
|
|
563
|
+
async *acceptUserMessage(requestContext, aborted) {
|
|
564
|
+
const userMessage = requestContext.userMessage;
|
|
565
|
+
const llmParts = [];
|
|
566
|
+
let anyConfirmationHandled = false;
|
|
567
|
+
let hasContentForLlm = false;
|
|
568
|
+
for (const part of userMessage.parts) {
|
|
569
|
+
const confirmationHandled = await this._handleToolConfirmationPart(part);
|
|
570
|
+
if (confirmationHandled) {
|
|
571
|
+
anyConfirmationHandled = true;
|
|
572
|
+
// If a confirmation was handled, the scheduler will now run the tool (or cancel it).
|
|
573
|
+
// We don't send anything to the LLM for this part.
|
|
574
|
+
// The subsequent tool execution will eventually lead to resolveToolCall.
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (part.kind === 'text') {
|
|
578
|
+
llmParts.push({ text: part.text });
|
|
579
|
+
hasContentForLlm = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (hasContentForLlm) {
|
|
583
|
+
logger.info('[Task] Sending new parts to LLM.');
|
|
584
|
+
const stateChange = {
|
|
585
|
+
kind: CoderAgentEvent.StateChangeEvent,
|
|
586
|
+
};
|
|
587
|
+
// Set task state to working as we are about to call LLM
|
|
588
|
+
this.setTaskStateAndPublishUpdate('working', stateChange);
|
|
589
|
+
// TODO: Determine what it mean to have, then add a prompt ID.
|
|
590
|
+
yield* this.geminiClient.sendMessageStream(llmParts, aborted,
|
|
591
|
+
/*prompt_id*/ '');
|
|
592
|
+
}
|
|
593
|
+
else if (anyConfirmationHandled) {
|
|
594
|
+
logger.info('[Task] User message only contained tool confirmations. Scheduler is active. No new input for LLM this turn.');
|
|
595
|
+
// Ensure task state reflects that scheduler might be working due to confirmation.
|
|
596
|
+
// If scheduler is active, it will emit its own status updates.
|
|
597
|
+
// If all pending tools were just confirmed, waitForPendingTools will handle the wait.
|
|
598
|
+
// If some tools are still pending approval, scheduler would have set InputRequired.
|
|
599
|
+
// If not, and no new text, we are just waiting.
|
|
600
|
+
if (this.pendingToolCalls.size > 0 &&
|
|
601
|
+
this.taskState !== 'input-required') {
|
|
602
|
+
const stateChange = {
|
|
603
|
+
kind: CoderAgentEvent.StateChangeEvent,
|
|
604
|
+
};
|
|
605
|
+
this.setTaskStateAndPublishUpdate('working', stateChange); // Reflect potential background activity
|
|
606
|
+
}
|
|
607
|
+
yield* (async function* () { })(); // Yield nothing
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
logger.info('[Task] No relevant parts in user message for LLM interaction or tool confirmation.');
|
|
611
|
+
// If there's no new text and no confirmations, and no pending tools,
|
|
612
|
+
// it implies we might need to signal input required if nothing else is happening.
|
|
613
|
+
// However, the agent.ts will make this determination after waitForPendingTools.
|
|
614
|
+
yield* (async function* () { })(); // Yield nothing
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
_sendTextContent(content) {
|
|
618
|
+
if (content === '') {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
logger.info('[Task] Sending text content to event bus.');
|
|
622
|
+
const message = this._createTextMessage(content);
|
|
623
|
+
const textContent = {
|
|
624
|
+
kind: CoderAgentEvent.TextContentEvent,
|
|
625
|
+
};
|
|
626
|
+
this.eventBus?.publish(this._createStatusUpdateEvent(this.taskState, textContent, message, false));
|
|
627
|
+
}
|
|
628
|
+
_sendThought(content) {
|
|
629
|
+
if (!content.subject && !content.description) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
logger.info('[Task] Sending thought to event bus.');
|
|
633
|
+
const message = {
|
|
634
|
+
kind: 'message',
|
|
635
|
+
role: 'agent',
|
|
636
|
+
parts: [
|
|
637
|
+
{
|
|
638
|
+
kind: 'data',
|
|
639
|
+
data: content,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
messageId: uuidv4(),
|
|
643
|
+
taskId: this.id,
|
|
644
|
+
contextId: this.contextId,
|
|
645
|
+
};
|
|
646
|
+
const thought = {
|
|
647
|
+
kind: CoderAgentEvent.ThoughtEvent,
|
|
648
|
+
};
|
|
649
|
+
this.eventBus?.publish(this._createStatusUpdateEvent(this.taskState, thought, message, false));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
//# sourceMappingURL=task.js.map
|