@tencent-ai/agent-sdk 0.3.41 → 0.3.43
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/cli/CHANGELOG.md +56 -0
- package/cli/dist/codebuddy.js +6 -6
- package/cli/package.json +1 -1
- package/cli/product.cloudhosted.json +20 -7
- package/cli/product.internal.json +20 -7
- package/cli/product.ioa.json +28 -6
- package/cli/product.json +49 -8
- package/cli/product.selfhosted.json +16 -3
- package/lib/index.d.ts +1 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -18
- package/lib/index.js.map +1 -1
- package/lib/query.d.ts.map +1 -1
- package/lib/query.js +7 -21
- package/lib/query.js.map +1 -1
- package/lib/session.d.ts +7 -9
- package/lib/session.d.ts.map +1 -1
- package/lib/session.js +28 -25
- package/lib/session.js.map +1 -1
- package/lib/transport/process-transport.d.ts.map +1 -1
- package/lib/transport/process-transport.js +9 -0
- package/lib/transport/process-transport.js.map +1 -1
- package/lib/types.d.ts +8 -9
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/utils/stream.d.ts +19 -0
- package/lib/utils/stream.d.ts.map +1 -1
- package/lib/utils/stream.js +31 -0
- package/lib/utils/stream.js.map +1 -1
- package/package.json +1 -1
- package/lib/acp/agent.d.ts +0 -427
- package/lib/acp/agent.d.ts.map +0 -1
- package/lib/acp/agent.js +0 -835
- package/lib/acp/agent.js.map +0 -1
- package/lib/acp/converter.d.ts +0 -179
- package/lib/acp/converter.d.ts.map +0 -1
- package/lib/acp/converter.js +0 -1094
- package/lib/acp/converter.js.map +0 -1
- package/lib/acp/index.d.ts +0 -11
- package/lib/acp/index.d.ts.map +0 -1
- package/lib/acp/index.js +0 -20
- package/lib/acp/index.js.map +0 -1
- package/lib/acp/server.d.ts +0 -70
- package/lib/acp/server.d.ts.map +0 -1
- package/lib/acp/server.js +0 -364
- package/lib/acp/server.js.map +0 -1
- package/lib/acp/session-manager.d.ts +0 -33
- package/lib/acp/session-manager.d.ts.map +0 -1
- package/lib/acp/session-manager.js +0 -106
- package/lib/acp/session-manager.js.map +0 -1
- package/lib/acp/session.d.ts +0 -67
- package/lib/acp/session.d.ts.map +0 -1
- package/lib/acp/session.js +0 -263
- package/lib/acp/session.js.map +0 -1
package/lib/acp/agent.js
DELETED
|
@@ -1,835 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* AcpAgent - Implementation of the official ACP Agent interface
|
|
4
|
-
*
|
|
5
|
-
* This class bridges the @agentclientprotocol/sdk Agent interface with
|
|
6
|
-
* the Genie Session for handling ACP protocol requests.
|
|
7
|
-
*
|
|
8
|
-
* Usage: Create via session.asAcpAgent(connection)
|
|
9
|
-
*
|
|
10
|
-
* Architecture:
|
|
11
|
-
* Browser -> HTTP -> AcpHttpTransport -> AgentSideConnection -> AcpAgent -> Session
|
|
12
|
-
*/
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.AcpAgent = exports.ACP_EXT_METHOD_QUESTION = void 0;
|
|
15
|
-
const converter_1 = require("./converter");
|
|
16
|
-
// ============= ACP Extension Constants =============
|
|
17
|
-
// These should stay in sync with @genie/agent-client-protocol types.ts
|
|
18
|
-
/**
|
|
19
|
-
* Extension method for tool input requests (e.g., AskUserQuestion).
|
|
20
|
-
* Matches ExtensionMethod.QUESTION from agent-client-protocol.
|
|
21
|
-
*/
|
|
22
|
-
exports.ACP_EXT_METHOD_QUESTION = '_codebuddy.ai/question';
|
|
23
|
-
/**
|
|
24
|
-
* AcpAgent - Implements the official ACP Agent interface
|
|
25
|
-
*
|
|
26
|
-
* Created via session.asAcpAgent(connection). The session must be
|
|
27
|
-
* created and connected before creating the AcpAgent.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* const session = createSession({ cwd: '/workspace' });
|
|
32
|
-
* await session.connect();
|
|
33
|
-
*
|
|
34
|
-
* const connection = new AgentSideConnection(
|
|
35
|
-
* (conn) => session.asAcpAgent(conn),
|
|
36
|
-
* stream
|
|
37
|
-
* );
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
class AcpAgent {
|
|
41
|
-
/**
|
|
42
|
-
* Create an AcpAgent.
|
|
43
|
-
*
|
|
44
|
-
* @param connection - The AgentSideConnection for communication
|
|
45
|
-
* @param session - Optional default session. If provided, it will be returned by newSession().
|
|
46
|
-
* If null, onNewSession must be provided in options.
|
|
47
|
-
* @param options - Configuration options including optional onNewSession
|
|
48
|
-
*/
|
|
49
|
-
constructor(connection, session, options = {}) {
|
|
50
|
-
var _a, _b;
|
|
51
|
-
this.sessions = new Map();
|
|
52
|
-
this.activePromptSessionId = null;
|
|
53
|
-
this.promptAbortController = null;
|
|
54
|
-
/** Per-session converters for proper state management */
|
|
55
|
-
this.sessionConverters = new Map();
|
|
56
|
-
/** Track session last activity time for LRU eviction */
|
|
57
|
-
this.sessionLastActivity = new Map();
|
|
58
|
-
/** Track in-progress session loading to prevent race conditions */
|
|
59
|
-
this.sessionLoadingPromises = new Map();
|
|
60
|
-
/** Track commands notification handlers per session for cleanup */
|
|
61
|
-
this.sessionCommandsHandlers = new Map();
|
|
62
|
-
this.connection = connection;
|
|
63
|
-
this.defaultSession = session;
|
|
64
|
-
this.options = {
|
|
65
|
-
protocolVersion: (_a = options.protocolVersion) !== null && _a !== void 0 ? _a : 1,
|
|
66
|
-
agentInfo: (_b = options.agentInfo) !== null && _b !== void 0 ? _b : {
|
|
67
|
-
name: 'codebuddy-code',
|
|
68
|
-
version: '1.0.0',
|
|
69
|
-
},
|
|
70
|
-
...options,
|
|
71
|
-
};
|
|
72
|
-
// If a default session is provided, register it using its own sessionId
|
|
73
|
-
// Session now auto-generates sessionId, so we use that instead of generating a new one
|
|
74
|
-
if (session) {
|
|
75
|
-
const sessionId = session.sessionId;
|
|
76
|
-
this.registerSession(sessionId, session);
|
|
77
|
-
}
|
|
78
|
-
// Clean up when connection closes
|
|
79
|
-
// Note: connection.signal may not be available during construction
|
|
80
|
-
// (when called from AgentSideConnection factory), so we defer this
|
|
81
|
-
queueMicrotask(() => {
|
|
82
|
-
if (connection.signal) {
|
|
83
|
-
connection.signal.addEventListener('abort', () => {
|
|
84
|
-
this.cleanup();
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// ============= Required Agent Interface Methods =============
|
|
90
|
-
/**
|
|
91
|
-
* Initialize the connection with the client.
|
|
92
|
-
* Returns protocol version and agent capabilities.
|
|
93
|
-
*/
|
|
94
|
-
async initialize(_params) {
|
|
95
|
-
const defaultCapabilities = {
|
|
96
|
-
// Always support loadSession - works for in-memory sessions,
|
|
97
|
-
// and for resuming sessions if onLoadSession is provided
|
|
98
|
-
loadSession: true,
|
|
99
|
-
};
|
|
100
|
-
return {
|
|
101
|
-
protocolVersion: this.options.protocolVersion,
|
|
102
|
-
agentCapabilities: {
|
|
103
|
-
...defaultCapabilities,
|
|
104
|
-
...this.options.capabilities,
|
|
105
|
-
},
|
|
106
|
-
agentInfo: this.options.agentInfo,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Authenticate the client.
|
|
111
|
-
* Currently a no-op as we don't require authentication.
|
|
112
|
-
*/
|
|
113
|
-
async authenticate(_params) {
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Create or return a session.
|
|
118
|
-
*
|
|
119
|
-
* Behavior depends on configuration:
|
|
120
|
-
* 1. If onNewSession is provided: Creates a new session via factory
|
|
121
|
-
* 2. If defaultSession is provided: Returns the first registered session
|
|
122
|
-
* 3. Otherwise: Throws an error
|
|
123
|
-
*/
|
|
124
|
-
async newSession(params) {
|
|
125
|
-
// If we have a session factory, use it to create the session
|
|
126
|
-
// Session auto-generates sessionId, so we get it from the created session
|
|
127
|
-
if (this.options.onNewSession) {
|
|
128
|
-
// Pass params without sessionId - Session will auto-generate it
|
|
129
|
-
// sessionId is kept as optional parameter for backward compatibility
|
|
130
|
-
const session = await this.options.onNewSession(params);
|
|
131
|
-
const sessionId = session.sessionId; // Get the auto-generated sessionId
|
|
132
|
-
this.registerSession(sessionId, session);
|
|
133
|
-
// Subscribe to commands channel for available_commands_update (async, after return)
|
|
134
|
-
setTimeout(() => this.subscribeToCommands(sessionId, session), 0);
|
|
135
|
-
return {
|
|
136
|
-
sessionId,
|
|
137
|
-
modes: await this.getSessionModes(session),
|
|
138
|
-
models: await this.getSessionModels(session),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// If we have a default session, return the first registered sessionId
|
|
142
|
-
if (this.defaultSession && this.sessions.size > 0) {
|
|
143
|
-
const firstSessionId = this.sessions.keys().next().value;
|
|
144
|
-
// Subscribe to commands channel for available_commands_update (async, after return)
|
|
145
|
-
setTimeout(() => this.subscribeToCommands(firstSessionId, this.defaultSession), 0);
|
|
146
|
-
return {
|
|
147
|
-
sessionId: firstSessionId,
|
|
148
|
-
modes: await this.getSessionModes(this.defaultSession),
|
|
149
|
-
models: await this.getSessionModels(this.defaultSession),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
throw new Error('No session available. Provide either a session or onNewSession.');
|
|
153
|
-
}
|
|
154
|
-
// 加载/恢复已有会话
|
|
155
|
-
// 根据 ACP 规范:Agent 必须先通过 session/update 流式传输所有历史消息,
|
|
156
|
-
// 然后才能发送 session/load 响应
|
|
157
|
-
// 参考:https://agentclientprotocol.com/protocol/session-setup#loading-a-session
|
|
158
|
-
async loadSession(params) {
|
|
159
|
-
const { sessionId } = params;
|
|
160
|
-
// If another loadSession is in progress for this sessionId, wait for it
|
|
161
|
-
const existingPromise = this.sessionLoadingPromises.get(sessionId);
|
|
162
|
-
if (existingPromise) {
|
|
163
|
-
const session = await existingPromise;
|
|
164
|
-
// Subscribe to commands channel for available_commands_update (async, after return)
|
|
165
|
-
setTimeout(() => this.subscribeToCommands(sessionId, session), 0);
|
|
166
|
-
return {
|
|
167
|
-
modes: await this.getSessionModes(session),
|
|
168
|
-
models: await this.getSessionModels(session),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
// If we have a session loader, always use it to load/resume the session
|
|
172
|
-
// This supports multi-window scenarios where each window needs fresh history
|
|
173
|
-
if (this.options.onLoadSession) {
|
|
174
|
-
// Create the loading promise and track it BEFORE any async operations
|
|
175
|
-
// This prevents race conditions where concurrent requests see no session
|
|
176
|
-
const loadPromise = Promise.resolve(this.options.onLoadSession(params));
|
|
177
|
-
this.sessionLoadingPromises.set(sessionId, loadPromise);
|
|
178
|
-
try {
|
|
179
|
-
// Remove existing session if present (allows re-loading with fresh history)
|
|
180
|
-
// Do this AFTER setting the loading promise so concurrent requests wait
|
|
181
|
-
if (this.sessions.has(sessionId)) {
|
|
182
|
-
this.removeSession(sessionId, false); // Don't close - onLoadSession handles this
|
|
183
|
-
}
|
|
184
|
-
const session = await loadPromise;
|
|
185
|
-
this.registerSession(sessionId, session);
|
|
186
|
-
// 根据 ACP 规范,必须先完成所有历史消息的流式传输,
|
|
187
|
-
// 然后才能发送 session/load 响应
|
|
188
|
-
// "When all the conversation entries have been streamed to the Client,
|
|
189
|
-
// the Agent MUST respond to the original session/load request."
|
|
190
|
-
if (session.hasPendingHistory()) {
|
|
191
|
-
try {
|
|
192
|
-
// Collect all messages first
|
|
193
|
-
const messages = [];
|
|
194
|
-
for await (const message of session.stream()) {
|
|
195
|
-
messages.push(message);
|
|
196
|
-
if (message.type === 'result' || message.type === 'error') {
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// Filter: only keep the last topic message
|
|
201
|
-
const filtered = this.filterHistoryMessages(messages);
|
|
202
|
-
// Send updates
|
|
203
|
-
for (const message of filtered) {
|
|
204
|
-
await this.sendUpdates(sessionId, message, 'history');
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (_a) {
|
|
208
|
-
// ignore streaming errors, still return response
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// Subscribe to commands channel for available_commands_update (async, after return)
|
|
212
|
-
setTimeout(() => this.subscribeToCommands(sessionId, session), 0);
|
|
213
|
-
return {
|
|
214
|
-
modes: await this.getSessionModes(session),
|
|
215
|
-
models: await this.getSessionModels(session),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
this.sessionLoadingPromises.delete(sessionId);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
// No session loader - check if session is already loaded
|
|
223
|
-
if (this.sessions.has(sessionId)) {
|
|
224
|
-
const session = this.sessions.get(sessionId);
|
|
225
|
-
// Subscribe to commands channel for available_commands_update (async, after return)
|
|
226
|
-
if (session) {
|
|
227
|
-
setTimeout(() => this.subscribeToCommands(sessionId, session), 0);
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
modes: await this.getSessionModes(session),
|
|
231
|
-
models: await this.getSessionModels(session),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
throw new Error(`Session not found: ${sessionId}. Provide onLoadSession to support resuming sessions from storage.`);
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Process a prompt request.
|
|
238
|
-
* Sends the prompt to the session and streams updates back to the client.
|
|
239
|
-
*/
|
|
240
|
-
async prompt(params) {
|
|
241
|
-
var _a;
|
|
242
|
-
const { sessionId, prompt } = params;
|
|
243
|
-
// Wait for any in-progress session loading to complete
|
|
244
|
-
// This prevents race conditions when prompt is called immediately after loadSession
|
|
245
|
-
const loadingPromise = this.sessionLoadingPromises.get(sessionId);
|
|
246
|
-
if (loadingPromise) {
|
|
247
|
-
await loadingPromise;
|
|
248
|
-
}
|
|
249
|
-
const session = this.sessions.get(sessionId);
|
|
250
|
-
if (!session) {
|
|
251
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
252
|
-
}
|
|
253
|
-
// Update session activity
|
|
254
|
-
this.touchSession(sessionId);
|
|
255
|
-
// Track active prompt for cancellation
|
|
256
|
-
this.activePromptSessionId = sessionId;
|
|
257
|
-
this.promptAbortController = new AbortController();
|
|
258
|
-
try {
|
|
259
|
-
// Convert ACP content blocks to SDK format
|
|
260
|
-
const userMessage = this.convertAcpPromptToUserMessage(sessionId, prompt);
|
|
261
|
-
// Send prompt to session
|
|
262
|
-
await session.send(userMessage);
|
|
263
|
-
// Stream responses back to client
|
|
264
|
-
for await (const message of session.stream()) {
|
|
265
|
-
// Check for cancellation
|
|
266
|
-
if ((_a = this.promptAbortController) === null || _a === void 0 ? void 0 : _a.signal.aborted) {
|
|
267
|
-
return { stopReason: 'cancelled' };
|
|
268
|
-
}
|
|
269
|
-
// Handle system init message - send custom notification with model/permissionMode
|
|
270
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
271
|
-
await this.sendSystemInfoNotification(sessionId, message);
|
|
272
|
-
}
|
|
273
|
-
// Convert SDK message to ACP format and send updates (stream mode)
|
|
274
|
-
await this.sendUpdates(sessionId, message, 'stream');
|
|
275
|
-
// Check for result message (end of turn)
|
|
276
|
-
if (message.type === 'result') {
|
|
277
|
-
return { stopReason: 'end_turn' };
|
|
278
|
-
}
|
|
279
|
-
// Check for error message
|
|
280
|
-
if (message.type === 'error') {
|
|
281
|
-
return { stopReason: 'end_turn' };
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return { stopReason: 'end_turn' };
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
this.activePromptSessionId = null;
|
|
288
|
-
this.promptAbortController = null;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Cancel an ongoing prompt.
|
|
293
|
-
*/
|
|
294
|
-
async cancel(params) {
|
|
295
|
-
const { sessionId } = params;
|
|
296
|
-
if (this.activePromptSessionId === sessionId) {
|
|
297
|
-
// Abort local controller to break out of the prompt loop
|
|
298
|
-
if (this.promptAbortController) {
|
|
299
|
-
this.promptAbortController.abort();
|
|
300
|
-
}
|
|
301
|
-
// Send interrupt to the underlying session/CLI
|
|
302
|
-
const session = this.sessions.get(sessionId);
|
|
303
|
-
if (session) {
|
|
304
|
-
try {
|
|
305
|
-
await session.interrupt();
|
|
306
|
-
}
|
|
307
|
-
catch (_a) {
|
|
308
|
-
// Ignore interrupt errors
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Set the operational mode for a session.
|
|
315
|
-
*
|
|
316
|
-
* Maps ACP SessionModeId to SDK PermissionMode and delegates to Session.setPermissionMode().
|
|
317
|
-
* Errors are silently ignored - the mode just won't take effect.
|
|
318
|
-
*/
|
|
319
|
-
async setSessionMode(params) {
|
|
320
|
-
const { sessionId, modeId } = params;
|
|
321
|
-
// Wait for any in-progress session loading to complete
|
|
322
|
-
// This prevents race conditions when setSessionMode is called immediately after loadSession
|
|
323
|
-
const loadingPromise = this.sessionLoadingPromises.get(sessionId);
|
|
324
|
-
if (loadingPromise) {
|
|
325
|
-
await loadingPromise;
|
|
326
|
-
}
|
|
327
|
-
const session = this.sessions.get(sessionId);
|
|
328
|
-
if (!session) {
|
|
329
|
-
return {};
|
|
330
|
-
}
|
|
331
|
-
try {
|
|
332
|
-
// Map ACP modeId to SDK PermissionMode
|
|
333
|
-
await session.setPermissionMode(modeId);
|
|
334
|
-
}
|
|
335
|
-
catch (_a) {
|
|
336
|
-
// Silently ignore errors
|
|
337
|
-
}
|
|
338
|
-
return {};
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Set the model for a session.
|
|
342
|
-
*
|
|
343
|
-
* Delegates to Session.setModel().
|
|
344
|
-
* Errors are silently ignored - the model just won't take effect.
|
|
345
|
-
*
|
|
346
|
-
* @experimental This capability is not part of the spec yet.
|
|
347
|
-
*/
|
|
348
|
-
async unstable_setSessionModel(params) {
|
|
349
|
-
const { sessionId, modelId } = params;
|
|
350
|
-
// Wait for any in-progress session loading to complete
|
|
351
|
-
// This prevents race conditions when setSessionModel is called immediately after loadSession
|
|
352
|
-
const loadingPromise = this.sessionLoadingPromises.get(sessionId);
|
|
353
|
-
if (loadingPromise) {
|
|
354
|
-
await loadingPromise;
|
|
355
|
-
}
|
|
356
|
-
const session = this.sessions.get(sessionId);
|
|
357
|
-
if (!session) {
|
|
358
|
-
return {};
|
|
359
|
-
}
|
|
360
|
-
try {
|
|
361
|
-
await session.setModel(modelId);
|
|
362
|
-
}
|
|
363
|
-
catch (_a) {
|
|
364
|
-
// Silently ignore errors
|
|
365
|
-
}
|
|
366
|
-
return {};
|
|
367
|
-
}
|
|
368
|
-
// ============= Optional Agent Interface Methods =============
|
|
369
|
-
async extMethod(_method, _params) {
|
|
370
|
-
return { handled: false };
|
|
371
|
-
}
|
|
372
|
-
async extNotification(_method, _params) {
|
|
373
|
-
// Handle custom extension notifications if needed
|
|
374
|
-
}
|
|
375
|
-
// ============= Public Accessors =============
|
|
376
|
-
/**
|
|
377
|
-
* Get a session by ID, or the first session if no ID provided.
|
|
378
|
-
*/
|
|
379
|
-
getSession(sessionId) {
|
|
380
|
-
if (sessionId) {
|
|
381
|
-
return this.sessions.get(sessionId);
|
|
382
|
-
}
|
|
383
|
-
// Return first session if no ID provided
|
|
384
|
-
return this.sessions.values().next().value;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Alias for getSession() for compatibility.
|
|
388
|
-
*/
|
|
389
|
-
getCurrentSession() {
|
|
390
|
-
return this.getSession();
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Get all session IDs.
|
|
394
|
-
*/
|
|
395
|
-
getSessionIds() {
|
|
396
|
-
return Array.from(this.sessions.keys());
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Get the number of active sessions.
|
|
400
|
-
*/
|
|
401
|
-
getSessionCount() {
|
|
402
|
-
return this.sessions.size;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Remove a session by ID and free associated resources.
|
|
406
|
-
* @param sessionId - The session ID to remove
|
|
407
|
-
* @param closeSession - Whether to close the session (default: true)
|
|
408
|
-
* @returns true if the session was removed, false if not found
|
|
409
|
-
*/
|
|
410
|
-
removeSession(sessionId, closeSession = true) {
|
|
411
|
-
const session = this.sessions.get(sessionId);
|
|
412
|
-
if (!session) {
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
// Close the session if requested
|
|
416
|
-
if (closeSession) {
|
|
417
|
-
try {
|
|
418
|
-
session.close();
|
|
419
|
-
}
|
|
420
|
-
catch (_a) {
|
|
421
|
-
// Ignore close errors
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
// Clean up commands notification handler if exists
|
|
425
|
-
// Note: We don't need to explicitly unsubscribe from CLI since session is closing
|
|
426
|
-
this.sessionCommandsHandlers.delete(sessionId);
|
|
427
|
-
// Clean up all associated resources
|
|
428
|
-
this.sessions.delete(sessionId);
|
|
429
|
-
this.sessionConverters.delete(sessionId);
|
|
430
|
-
this.sessionLastActivity.delete(sessionId);
|
|
431
|
-
return true;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Remove all sessions and free resources.
|
|
435
|
-
* @param closeSessions - Whether to close sessions (default: true)
|
|
436
|
-
*/
|
|
437
|
-
removeAllSessions(closeSessions = true) {
|
|
438
|
-
for (const sessionId of this.sessions.keys()) {
|
|
439
|
-
this.removeSession(sessionId, closeSessions);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Get memory statistics for debugging.
|
|
444
|
-
*/
|
|
445
|
-
getMemoryStats() {
|
|
446
|
-
let oldestTime = null;
|
|
447
|
-
for (const time of this.sessionLastActivity.values()) {
|
|
448
|
-
if (oldestTime === null || time < oldestTime) {
|
|
449
|
-
oldestTime = time;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return {
|
|
453
|
-
sessionCount: this.sessions.size,
|
|
454
|
-
converterCount: this.sessionConverters.size,
|
|
455
|
-
oldestSessionAge: oldestTime ? Date.now() - oldestTime : null,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
// ============= Private Helper Methods =============
|
|
459
|
-
/**
|
|
460
|
-
* Register a session with memory management.
|
|
461
|
-
* Injects canUseTool handler for AskUserQuestion support.
|
|
462
|
-
*/
|
|
463
|
-
registerSession(sessionId, session) {
|
|
464
|
-
// Enforce maxSessions limit if configured
|
|
465
|
-
if (this.options.maxSessions && this.sessions.size >= this.options.maxSessions) {
|
|
466
|
-
this.evictOldestSession();
|
|
467
|
-
}
|
|
468
|
-
// Get the original handler before wrapping (preserves user-configured handlers)
|
|
469
|
-
const originalHandler = session.getCanUseTool();
|
|
470
|
-
// Inject canUseTool handler for AskUserQuestion support
|
|
471
|
-
// This wraps any existing handler and intercepts AskUserQuestion requests
|
|
472
|
-
const wrappedHandler = this.createCanUseToolHandler(sessionId, originalHandler);
|
|
473
|
-
session.setCanUseTool(wrappedHandler);
|
|
474
|
-
this.sessions.set(sessionId, session);
|
|
475
|
-
this.sessionConverters.set(sessionId, new converter_1.AcpConverter());
|
|
476
|
-
this.sessionLastActivity.set(sessionId, Date.now());
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Update session activity timestamp.
|
|
480
|
-
*/
|
|
481
|
-
touchSession(sessionId) {
|
|
482
|
-
if (this.sessions.has(sessionId)) {
|
|
483
|
-
this.sessionLastActivity.set(sessionId, Date.now());
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Evict the oldest inactive session (LRU).
|
|
488
|
-
*/
|
|
489
|
-
evictOldestSession() {
|
|
490
|
-
let oldestId = null;
|
|
491
|
-
let oldestTime = Infinity;
|
|
492
|
-
for (const [id, time] of this.sessionLastActivity.entries()) {
|
|
493
|
-
// Don't evict the active prompt session
|
|
494
|
-
if (id === this.activePromptSessionId) {
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (time < oldestTime) {
|
|
498
|
-
oldestTime = time;
|
|
499
|
-
oldestId = id;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (oldestId) {
|
|
503
|
-
this.removeSession(oldestId, true);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Get session modes configuration.
|
|
508
|
-
* Returns the available modes and current mode from the given session.
|
|
509
|
-
* @param session - Optional session to get current mode from. If not provided, uses default.
|
|
510
|
-
*/
|
|
511
|
-
async getSessionModes(session) {
|
|
512
|
-
var _a, _b;
|
|
513
|
-
// Use configured modes or default basic set
|
|
514
|
-
const availableModes = (_a = this.options.availableModes) !== null && _a !== void 0 ? _a : (session ? await session.getAvailableModes().catch(() => []) : []);
|
|
515
|
-
// Get current mode from the provided session or fall back to default
|
|
516
|
-
const currentModeId = (_b = session === null || session === void 0 ? void 0 : session.getPermissionMode()) !== null && _b !== void 0 ? _b : 'default';
|
|
517
|
-
return {
|
|
518
|
-
availableModes,
|
|
519
|
-
currentModeId,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Get session models configuration.
|
|
524
|
-
* Returns the available models and current model from the given session.
|
|
525
|
-
* Fetches available models from the session if supported.
|
|
526
|
-
* Full raw model configurations are included in _meta['codebuddy.ai'].availableModels.
|
|
527
|
-
* @param session - Optional session to get current model and available models from.
|
|
528
|
-
*/
|
|
529
|
-
async getSessionModels(session) {
|
|
530
|
-
var _a;
|
|
531
|
-
// Attempt to fetch available models from session
|
|
532
|
-
let availableModels = [];
|
|
533
|
-
let rawModels = [];
|
|
534
|
-
try {
|
|
535
|
-
if (session) {
|
|
536
|
-
// Fetch both simplified and raw models in parallel
|
|
537
|
-
const [models, rawModelsResult] = await Promise.all([
|
|
538
|
-
session.getAvailableModels(),
|
|
539
|
-
session.getAvailableModelsRaw().catch(() => []),
|
|
540
|
-
]);
|
|
541
|
-
rawModels = rawModelsResult;
|
|
542
|
-
availableModels = models.map(m => ({
|
|
543
|
-
modelId: m.modelId,
|
|
544
|
-
name: m.name,
|
|
545
|
-
description: m.description,
|
|
546
|
-
}));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
catch (_b) {
|
|
550
|
-
// CLI may not support getAvailableModels, silently fall back to empty array
|
|
551
|
-
}
|
|
552
|
-
// Get current model from the provided session
|
|
553
|
-
// If no model is set, use undefined (no default fallback)
|
|
554
|
-
const currentModelId = (_a = session === null || session === void 0 ? void 0 : session.getModel()) !== null && _a !== void 0 ? _a : undefined;
|
|
555
|
-
return {
|
|
556
|
-
availableModels,
|
|
557
|
-
currentModelId: currentModelId,
|
|
558
|
-
// Include full raw model configs in _meta under codebuddy.ai namespace
|
|
559
|
-
_meta: rawModels.length > 0
|
|
560
|
-
? { 'codebuddy.ai': { availableModels: rawModels } }
|
|
561
|
-
: undefined,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Subscribe to commands channel and forward all updates to ACP client.
|
|
566
|
-
* Sets up a persistent listener that sends available_commands_update for each notification.
|
|
567
|
-
* This ensures no updates are missed when commands change multiple times.
|
|
568
|
-
* @param sessionId - The session ID to send the updates for
|
|
569
|
-
* @param session - The session to subscribe to commands channel
|
|
570
|
-
*/
|
|
571
|
-
async subscribeToCommands(sessionId, session) {
|
|
572
|
-
try {
|
|
573
|
-
// Create handler that forwards all commands notifications to ACP client
|
|
574
|
-
const handler = (notification) => {
|
|
575
|
-
const data = notification.data;
|
|
576
|
-
const commands = data.commands.map(cmd => {
|
|
577
|
-
const result = {
|
|
578
|
-
// Remove leading '/' from command name if present
|
|
579
|
-
name: cmd.name.startsWith('/') ? cmd.name.substring(1) : cmd.name,
|
|
580
|
-
description: cmd.description || '',
|
|
581
|
-
};
|
|
582
|
-
if (cmd.argumentHint) {
|
|
583
|
-
result.input = { hint: cmd.argumentHint };
|
|
584
|
-
}
|
|
585
|
-
return result;
|
|
586
|
-
});
|
|
587
|
-
// Send update to ACP client (fire-and-forget)
|
|
588
|
-
this.connection.sessionUpdate({
|
|
589
|
-
sessionId,
|
|
590
|
-
update: {
|
|
591
|
-
sessionUpdate: 'available_commands_update',
|
|
592
|
-
availableCommands: commands,
|
|
593
|
-
},
|
|
594
|
-
}).catch(error => {
|
|
595
|
-
console.error('[AcpAgent] Failed to send available commands update:', error);
|
|
596
|
-
});
|
|
597
|
-
};
|
|
598
|
-
// Store handler for cleanup
|
|
599
|
-
this.sessionCommandsHandlers.set(sessionId, handler);
|
|
600
|
-
// Subscribe to commands channel via session's subscribeToCommands method
|
|
601
|
-
await session.subscribeToCommands(handler);
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
// Silently ignore errors - available commands are optional
|
|
605
|
-
console.error('[AcpAgent] Failed to subscribe to commands channel:', error);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* Convert ACP ContentBlock[] to SDK UserMessage.
|
|
610
|
-
* Delegates to AcpConverter for the actual content block conversion.
|
|
611
|
-
*/
|
|
612
|
-
convertAcpPromptToUserMessage(sessionId, prompt) {
|
|
613
|
-
const sdkContent = converter_1.AcpConverter.convertAcpPromptToSdk(prompt);
|
|
614
|
-
return {
|
|
615
|
-
type: 'user',
|
|
616
|
-
session_id: sessionId,
|
|
617
|
-
message: {
|
|
618
|
-
role: 'user',
|
|
619
|
-
content: sdkContent,
|
|
620
|
-
},
|
|
621
|
-
parent_tool_use_id: null,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Get or create a converter for a session.
|
|
626
|
-
*/
|
|
627
|
-
getSessionConverter(sessionId) {
|
|
628
|
-
let converter = this.sessionConverters.get(sessionId);
|
|
629
|
-
if (!converter) {
|
|
630
|
-
converter = new converter_1.AcpConverter();
|
|
631
|
-
this.sessionConverters.set(sessionId, converter);
|
|
632
|
-
}
|
|
633
|
-
return converter;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Send session updates to the client via AgentSideConnection.
|
|
637
|
-
*
|
|
638
|
-
* @param sessionId - Session ID
|
|
639
|
-
* @param message - SDK message to convert and send
|
|
640
|
-
* @param mode - Conversion mode: 'stream' for prompt(), 'history' for loadSession()
|
|
641
|
-
*/
|
|
642
|
-
async sendUpdates(sessionId, message, mode = 'stream') {
|
|
643
|
-
var _a, _b;
|
|
644
|
-
const converter = this.getSessionConverter(sessionId);
|
|
645
|
-
const updates = converter.convertToUpdates(message, { mode });
|
|
646
|
-
for (const { update, _meta } of updates) {
|
|
647
|
-
const meta = {
|
|
648
|
-
mode,
|
|
649
|
-
toolName: (_a = _meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) === null || _a === void 0 ? void 0 : _a.toolName,
|
|
650
|
-
requestId: (_b = _meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) === null || _b === void 0 ? void 0 : _b.requestId,
|
|
651
|
-
};
|
|
652
|
-
// If onSessionUpdate callback is provided, call it for broadcasting
|
|
653
|
-
if (this.options.onSessionUpdate) {
|
|
654
|
-
this.options.onSessionUpdate(sessionId, update, meta);
|
|
655
|
-
}
|
|
656
|
-
// Always send to own connection
|
|
657
|
-
try {
|
|
658
|
-
await this.connection.sessionUpdate({
|
|
659
|
-
sessionId,
|
|
660
|
-
update,
|
|
661
|
-
_meta: {
|
|
662
|
-
..._meta,
|
|
663
|
-
// Mark message mode for router layer to decide whether to broadcast
|
|
664
|
-
'codebuddy.ai': {
|
|
665
|
-
...((_meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) || {}),
|
|
666
|
-
mode,
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
console.error('[AcpAgent] Error sending session update:', error);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Send system info notification when CLI reports model/permissionMode.
|
|
678
|
-
* Uses custom notification '_codebuddy.ai/system_init' with model and permissionMode.
|
|
679
|
-
*/
|
|
680
|
-
async sendSystemInfoNotification(sessionId, sysMsg) {
|
|
681
|
-
try {
|
|
682
|
-
await this.connection.extNotification('_codebuddy.ai/system_init', {
|
|
683
|
-
sessionId,
|
|
684
|
-
model: sysMsg.model,
|
|
685
|
-
permissionMode: sysMsg.permissionMode,
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
catch (_a) {
|
|
689
|
-
// Silently ignore notification errors
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Filter history messages: only keep the last topic message.
|
|
694
|
-
*/
|
|
695
|
-
filterHistoryMessages(messages) {
|
|
696
|
-
const result = [];
|
|
697
|
-
let foundLastTopic = false;
|
|
698
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
699
|
-
const msg = messages[i];
|
|
700
|
-
if ('type' in msg && msg.type === 'topic') {
|
|
701
|
-
if (foundLastTopic) {
|
|
702
|
-
continue; // Skip earlier topic messages
|
|
703
|
-
}
|
|
704
|
-
foundLastTopic = true;
|
|
705
|
-
}
|
|
706
|
-
result.unshift(msg);
|
|
707
|
-
}
|
|
708
|
-
return result;
|
|
709
|
-
}
|
|
710
|
-
// ============= AskUserQuestion Support =============
|
|
711
|
-
/**
|
|
712
|
-
* Create a canUseTool handler that wraps the original handler
|
|
713
|
-
* and intercepts AskUserQuestion requests.
|
|
714
|
-
*
|
|
715
|
-
* @param sessionId - The session ID for this handler
|
|
716
|
-
* @param originalHandler - Optional original canUseTool handler to delegate to
|
|
717
|
-
* @returns A wrapped canUseTool handler
|
|
718
|
-
*/
|
|
719
|
-
createCanUseToolHandler(sessionId, originalHandler) {
|
|
720
|
-
return async (toolName, input, options) => {
|
|
721
|
-
// Intercept AskUserQuestion tool
|
|
722
|
-
if (toolName === 'AskUserQuestion') {
|
|
723
|
-
return this.handleAskUserQuestion(sessionId, input, options);
|
|
724
|
-
}
|
|
725
|
-
// Delegate other tools to original handler
|
|
726
|
-
if (originalHandler) {
|
|
727
|
-
return originalHandler(toolName, input, options);
|
|
728
|
-
}
|
|
729
|
-
// Default: allow if no handler
|
|
730
|
-
return { behavior: 'allow', updatedInput: input };
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* Handle AskUserQuestion tool requests.
|
|
735
|
-
*
|
|
736
|
-
* If onAskUserQuestion is provided, uses that handler.
|
|
737
|
-
* Otherwise, sends the request to the client via ACP extMethod (_codebuddy.ai/question).
|
|
738
|
-
*
|
|
739
|
-
* @param sessionId - The session ID
|
|
740
|
-
* @param input - The tool input (AskUserQuestionInput)
|
|
741
|
-
* @param options - Permission options including toolUseID
|
|
742
|
-
* @returns Permission result with answers or denial
|
|
743
|
-
*/
|
|
744
|
-
async handleAskUserQuestion(sessionId, input, options) {
|
|
745
|
-
var _a;
|
|
746
|
-
const askInput = input;
|
|
747
|
-
// Build high-level request for custom handler
|
|
748
|
-
const askRequest = {
|
|
749
|
-
sessionId,
|
|
750
|
-
toolUseId: options.toolUseID,
|
|
751
|
-
questions: askInput.questions,
|
|
752
|
-
};
|
|
753
|
-
try {
|
|
754
|
-
let answers = null;
|
|
755
|
-
// Use custom handler if provided
|
|
756
|
-
if (this.options.onAskUserQuestion) {
|
|
757
|
-
const response = await this.options.onAskUserQuestion(askRequest);
|
|
758
|
-
answers = (_a = response === null || response === void 0 ? void 0 : response.answers) !== null && _a !== void 0 ? _a : null;
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
// Build ToolInputRequest for ACP protocol
|
|
762
|
-
const toolInputRequest = {
|
|
763
|
-
sessionId,
|
|
764
|
-
toolCallId: options.toolUseID,
|
|
765
|
-
inputType: 'question',
|
|
766
|
-
schema: {
|
|
767
|
-
questions: askInput.questions.map((q, idx) => ({
|
|
768
|
-
id: `q_${idx}`,
|
|
769
|
-
question: q.question,
|
|
770
|
-
header: q.header,
|
|
771
|
-
options: q.options,
|
|
772
|
-
multiSelect: q.multiSelect,
|
|
773
|
-
})),
|
|
774
|
-
},
|
|
775
|
-
};
|
|
776
|
-
// Send to client via ACP extMethod
|
|
777
|
-
const result = await this.connection.extMethod(exports.ACP_EXT_METHOD_QUESTION, toolInputRequest);
|
|
778
|
-
// Parse ToolInputResponse
|
|
779
|
-
const toolInputResponse = result;
|
|
780
|
-
if (toolInputResponse.outcome.outcome === 'submitted') {
|
|
781
|
-
// Convert QuestionInputData answers to simple Record<string, string>
|
|
782
|
-
const data = toolInputResponse.outcome.data;
|
|
783
|
-
answers = {};
|
|
784
|
-
// Build id -> question text mapping for better model readability
|
|
785
|
-
const questionMap = new Map(askInput.questions.map((q, idx) => [`q_${idx}`, q.question]));
|
|
786
|
-
for (const [key, value] of Object.entries(data.answers)) {
|
|
787
|
-
// Map q_0, q_1... back to actual question text
|
|
788
|
-
const questionText = questionMap.get(key) || key;
|
|
789
|
-
// Handle both string and string[] answers
|
|
790
|
-
answers[questionText] = Array.isArray(value) ? value.join(', ') : value;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
// Check for valid answers
|
|
795
|
-
if (answers && Object.keys(answers).length > 0) {
|
|
796
|
-
return {
|
|
797
|
-
behavior: 'allow',
|
|
798
|
-
updatedInput: {
|
|
799
|
-
questions: askInput.questions,
|
|
800
|
-
answers,
|
|
801
|
-
},
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
// User declined or no answers provided
|
|
806
|
-
return {
|
|
807
|
-
behavior: 'deny',
|
|
808
|
-
message: 'User declined to answer questions',
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
// Handle errors (e.g., client doesn't support extMethod)
|
|
814
|
-
return {
|
|
815
|
-
behavior: 'deny',
|
|
816
|
-
message: error instanceof Error
|
|
817
|
-
? error.message
|
|
818
|
-
: 'Failed to get user answers',
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Clean up when the connection closes.
|
|
824
|
-
* Removes all sessions and frees memory.
|
|
825
|
-
*/
|
|
826
|
-
cleanup() {
|
|
827
|
-
this.activePromptSessionId = null;
|
|
828
|
-
this.promptAbortController = null;
|
|
829
|
-
// Clean up all sessions when connection closes
|
|
830
|
-
// closeSession=true to properly release resources
|
|
831
|
-
this.removeAllSessions(true);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
exports.AcpAgent = AcpAgent;
|
|
835
|
-
//# sourceMappingURL=agent.js.map
|