@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.
Files changed (54) hide show
  1. package/cli/CHANGELOG.md +56 -0
  2. package/cli/dist/codebuddy.js +6 -6
  3. package/cli/package.json +1 -1
  4. package/cli/product.cloudhosted.json +20 -7
  5. package/cli/product.internal.json +20 -7
  6. package/cli/product.ioa.json +28 -6
  7. package/cli/product.json +49 -8
  8. package/cli/product.selfhosted.json +16 -3
  9. package/lib/index.d.ts +1 -11
  10. package/lib/index.d.ts.map +1 -1
  11. package/lib/index.js +1 -18
  12. package/lib/index.js.map +1 -1
  13. package/lib/query.d.ts.map +1 -1
  14. package/lib/query.js +7 -21
  15. package/lib/query.js.map +1 -1
  16. package/lib/session.d.ts +7 -9
  17. package/lib/session.d.ts.map +1 -1
  18. package/lib/session.js +28 -25
  19. package/lib/session.js.map +1 -1
  20. package/lib/transport/process-transport.d.ts.map +1 -1
  21. package/lib/transport/process-transport.js +9 -0
  22. package/lib/transport/process-transport.js.map +1 -1
  23. package/lib/types.d.ts +8 -9
  24. package/lib/types.d.ts.map +1 -1
  25. package/lib/types.js.map +1 -1
  26. package/lib/utils/stream.d.ts +19 -0
  27. package/lib/utils/stream.d.ts.map +1 -1
  28. package/lib/utils/stream.js +31 -0
  29. package/lib/utils/stream.js.map +1 -1
  30. package/package.json +1 -1
  31. package/lib/acp/agent.d.ts +0 -427
  32. package/lib/acp/agent.d.ts.map +0 -1
  33. package/lib/acp/agent.js +0 -835
  34. package/lib/acp/agent.js.map +0 -1
  35. package/lib/acp/converter.d.ts +0 -179
  36. package/lib/acp/converter.d.ts.map +0 -1
  37. package/lib/acp/converter.js +0 -1094
  38. package/lib/acp/converter.js.map +0 -1
  39. package/lib/acp/index.d.ts +0 -11
  40. package/lib/acp/index.d.ts.map +0 -1
  41. package/lib/acp/index.js +0 -20
  42. package/lib/acp/index.js.map +0 -1
  43. package/lib/acp/server.d.ts +0 -70
  44. package/lib/acp/server.d.ts.map +0 -1
  45. package/lib/acp/server.js +0 -364
  46. package/lib/acp/server.js.map +0 -1
  47. package/lib/acp/session-manager.d.ts +0 -33
  48. package/lib/acp/session-manager.d.ts.map +0 -1
  49. package/lib/acp/session-manager.js +0 -106
  50. package/lib/acp/session-manager.js.map +0 -1
  51. package/lib/acp/session.d.ts +0 -67
  52. package/lib/acp/session.d.ts.map +0 -1
  53. package/lib/acp/session.js +0 -263
  54. 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