@tencent-ai/agent-sdk 0.3.42 → 0.3.44

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 (55) hide show
  1. package/cli/CHANGELOG.md +47 -0
  2. package/cli/dist/codebuddy.js +8 -8
  3. package/cli/package.json +1 -1
  4. package/cli/product.cloudhosted.json +19 -6
  5. package/cli/product.internal.json +19 -6
  6. package/cli/product.ioa.json +40 -5
  7. package/cli/product.json +48 -7
  8. package/cli/product.selfhosted.json +15 -2
  9. package/lib/index.d.ts +3 -14
  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/session.d.ts +0 -9
  14. package/lib/session.d.ts.map +1 -1
  15. package/lib/session.js +11 -12
  16. package/lib/session.js.map +1 -1
  17. package/lib/transport/process-transport.d.ts.map +1 -1
  18. package/lib/transport/process-transport.js +9 -0
  19. package/lib/transport/process-transport.js.map +1 -1
  20. package/lib/types.d.ts +22 -66
  21. package/lib/types.d.ts.map +1 -1
  22. package/lib/types.js +0 -1
  23. package/lib/types.js.map +1 -1
  24. package/lib/utils/stream.d.ts +19 -0
  25. package/lib/utils/stream.d.ts.map +1 -1
  26. package/lib/utils/stream.js +31 -0
  27. package/lib/utils/stream.js.map +1 -1
  28. package/lib/utils/type-guards.d.ts.map +1 -1
  29. package/lib/utils/type-guards.js +2 -1
  30. package/lib/utils/type-guards.js.map +1 -1
  31. package/package.json +1 -1
  32. package/lib/acp/agent.d.ts +0 -438
  33. package/lib/acp/agent.d.ts.map +0 -1
  34. package/lib/acp/agent.js +0 -887
  35. package/lib/acp/agent.js.map +0 -1
  36. package/lib/acp/converter.d.ts +0 -189
  37. package/lib/acp/converter.d.ts.map +0 -1
  38. package/lib/acp/converter.js +0 -1135
  39. package/lib/acp/converter.js.map +0 -1
  40. package/lib/acp/index.d.ts +0 -11
  41. package/lib/acp/index.d.ts.map +0 -1
  42. package/lib/acp/index.js +0 -20
  43. package/lib/acp/index.js.map +0 -1
  44. package/lib/acp/server.d.ts +0 -77
  45. package/lib/acp/server.d.ts.map +0 -1
  46. package/lib/acp/server.js +0 -378
  47. package/lib/acp/server.js.map +0 -1
  48. package/lib/acp/session-manager.d.ts +0 -33
  49. package/lib/acp/session-manager.d.ts.map +0 -1
  50. package/lib/acp/session-manager.js +0 -106
  51. package/lib/acp/session-manager.js.map +0 -1
  52. package/lib/acp/session.d.ts +0 -67
  53. package/lib/acp/session.d.ts.map +0 -1
  54. package/lib/acp/session.js +0 -263
  55. package/lib/acp/session.js.map +0 -1
package/lib/acp/agent.js DELETED
@@ -1,887 +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
- // Send user message echo to client before processing
260
- // This ensures the echo is sent immediately with the original ACP blocks
261
- await this.sendUserMessageEcho(sessionId, prompt);
262
- // Convert ACP content blocks to SDK format
263
- const userMessage = this.convertAcpPromptToUserMessage(sessionId, prompt);
264
- // Send prompt to session
265
- await session.send(userMessage);
266
- // Stream responses back to client
267
- for await (const message of session.stream()) {
268
- // Check for cancellation
269
- if ((_a = this.promptAbortController) === null || _a === void 0 ? void 0 : _a.signal.aborted) {
270
- return { stopReason: 'cancelled' };
271
- }
272
- // Handle system init message - send custom notification with model/permissionMode
273
- if (message.type === 'system' && message.subtype === 'init') {
274
- await this.sendSystemInfoNotification(sessionId, message);
275
- }
276
- // Skip user message from CLI stream (we already sent the echo)
277
- if (message.type === 'user') {
278
- continue;
279
- }
280
- // Convert SDK message to ACP format and send updates (stream mode)
281
- await this.sendUpdates(sessionId, message, 'stream');
282
- // Check for result message (end of turn)
283
- if (message.type === 'result') {
284
- return { stopReason: 'end_turn' };
285
- }
286
- // Check for error message
287
- if (message.type === 'error') {
288
- return { stopReason: 'end_turn' };
289
- }
290
- }
291
- return { stopReason: 'end_turn' };
292
- }
293
- finally {
294
- this.activePromptSessionId = null;
295
- this.promptAbortController = null;
296
- }
297
- }
298
- /**
299
- * Cancel an ongoing prompt.
300
- */
301
- async cancel(params) {
302
- const { sessionId } = params;
303
- if (this.activePromptSessionId === sessionId) {
304
- // Abort local controller to break out of the prompt loop
305
- if (this.promptAbortController) {
306
- this.promptAbortController.abort();
307
- }
308
- // Send interrupt to the underlying session/CLI
309
- const session = this.sessions.get(sessionId);
310
- if (session) {
311
- try {
312
- await session.interrupt();
313
- }
314
- catch (_a) {
315
- // Ignore interrupt errors
316
- }
317
- }
318
- }
319
- }
320
- /**
321
- * Set the operational mode for a session.
322
- *
323
- * Maps ACP SessionModeId to SDK PermissionMode and delegates to Session.setPermissionMode().
324
- * Errors are silently ignored - the mode just won't take effect.
325
- */
326
- async setSessionMode(params) {
327
- const { sessionId, modeId } = params;
328
- // Wait for any in-progress session loading to complete
329
- // This prevents race conditions when setSessionMode is called immediately after loadSession
330
- const loadingPromise = this.sessionLoadingPromises.get(sessionId);
331
- if (loadingPromise) {
332
- await loadingPromise;
333
- }
334
- const session = this.sessions.get(sessionId);
335
- if (!session) {
336
- return {};
337
- }
338
- try {
339
- // Map ACP modeId to SDK PermissionMode
340
- await session.setPermissionMode(modeId);
341
- }
342
- catch (_a) {
343
- // Silently ignore errors
344
- }
345
- return {};
346
- }
347
- /**
348
- * Set the model for a session.
349
- *
350
- * Delegates to Session.setModel().
351
- * Errors are silently ignored - the model just won't take effect.
352
- *
353
- * @experimental This capability is not part of the spec yet.
354
- */
355
- async unstable_setSessionModel(params) {
356
- const { sessionId, modelId } = params;
357
- // Wait for any in-progress session loading to complete
358
- // This prevents race conditions when setSessionModel is called immediately after loadSession
359
- const loadingPromise = this.sessionLoadingPromises.get(sessionId);
360
- if (loadingPromise) {
361
- await loadingPromise;
362
- }
363
- const session = this.sessions.get(sessionId);
364
- if (!session) {
365
- return {};
366
- }
367
- try {
368
- await session.setModel(modelId);
369
- }
370
- catch (_a) {
371
- // Silently ignore errors
372
- }
373
- return {};
374
- }
375
- // ============= Optional Agent Interface Methods =============
376
- async extMethod(_method, _params) {
377
- return { handled: false };
378
- }
379
- async extNotification(_method, _params) {
380
- // Handle custom extension notifications if needed
381
- }
382
- // ============= Public Accessors =============
383
- /**
384
- * Get a session by ID, or the first session if no ID provided.
385
- */
386
- getSession(sessionId) {
387
- if (sessionId) {
388
- return this.sessions.get(sessionId);
389
- }
390
- // Return first session if no ID provided
391
- return this.sessions.values().next().value;
392
- }
393
- /**
394
- * Alias for getSession() for compatibility.
395
- */
396
- getCurrentSession() {
397
- return this.getSession();
398
- }
399
- /**
400
- * Get all session IDs.
401
- */
402
- getSessionIds() {
403
- return Array.from(this.sessions.keys());
404
- }
405
- /**
406
- * Get the number of active sessions.
407
- */
408
- getSessionCount() {
409
- return this.sessions.size;
410
- }
411
- /**
412
- * Remove a session by ID and free associated resources.
413
- * @param sessionId - The session ID to remove
414
- * @param closeSession - Whether to close the session (default: true)
415
- * @returns true if the session was removed, false if not found
416
- */
417
- removeSession(sessionId, closeSession = true) {
418
- const session = this.sessions.get(sessionId);
419
- if (!session) {
420
- return false;
421
- }
422
- // Close the session if requested
423
- if (closeSession) {
424
- try {
425
- session.close();
426
- }
427
- catch (_a) {
428
- // Ignore close errors
429
- }
430
- }
431
- // Clean up commands notification handler if exists
432
- // Must unsubscribe from session even if not closing, to prevent duplicate handlers
433
- // when the same session is reused (e.g., in session/load with AcpServer)
434
- const handler = this.sessionCommandsHandlers.get(sessionId);
435
- if (handler) {
436
- session.unsubscribeFromCommands(handler);
437
- this.sessionCommandsHandlers.delete(sessionId);
438
- }
439
- // Clean up all associated resources
440
- this.sessions.delete(sessionId);
441
- this.sessionConverters.delete(sessionId);
442
- this.sessionLastActivity.delete(sessionId);
443
- return true;
444
- }
445
- /**
446
- * Remove all sessions and free resources.
447
- * @param closeSessions - Whether to close sessions (default: true)
448
- */
449
- removeAllSessions(closeSessions = true) {
450
- for (const sessionId of this.sessions.keys()) {
451
- this.removeSession(sessionId, closeSessions);
452
- }
453
- }
454
- /**
455
- * Get memory statistics for debugging.
456
- */
457
- getMemoryStats() {
458
- let oldestTime = null;
459
- for (const time of this.sessionLastActivity.values()) {
460
- if (oldestTime === null || time < oldestTime) {
461
- oldestTime = time;
462
- }
463
- }
464
- return {
465
- sessionCount: this.sessions.size,
466
- converterCount: this.sessionConverters.size,
467
- oldestSessionAge: oldestTime ? Date.now() - oldestTime : null,
468
- };
469
- }
470
- // ============= Private Helper Methods =============
471
- /**
472
- * Register a session with memory management.
473
- * Injects canUseTool handler for AskUserQuestion support.
474
- */
475
- registerSession(sessionId, session) {
476
- // Enforce maxSessions limit if configured
477
- if (this.options.maxSessions && this.sessions.size >= this.options.maxSessions) {
478
- this.evictOldestSession();
479
- }
480
- // Get the original handler before wrapping (preserves user-configured handlers)
481
- const originalHandler = session.getCanUseTool();
482
- // Inject canUseTool handler for AskUserQuestion support
483
- // This wraps any existing handler and intercepts AskUserQuestion requests
484
- const wrappedHandler = this.createCanUseToolHandler(sessionId, originalHandler);
485
- session.setCanUseTool(wrappedHandler);
486
- this.sessions.set(sessionId, session);
487
- this.sessionConverters.set(sessionId, new converter_1.AcpConverter());
488
- this.sessionLastActivity.set(sessionId, Date.now());
489
- }
490
- /**
491
- * Update session activity timestamp.
492
- */
493
- touchSession(sessionId) {
494
- if (this.sessions.has(sessionId)) {
495
- this.sessionLastActivity.set(sessionId, Date.now());
496
- }
497
- }
498
- /**
499
- * Evict the oldest inactive session (LRU).
500
- */
501
- evictOldestSession() {
502
- let oldestId = null;
503
- let oldestTime = Infinity;
504
- for (const [id, time] of this.sessionLastActivity.entries()) {
505
- // Don't evict the active prompt session
506
- if (id === this.activePromptSessionId) {
507
- continue;
508
- }
509
- if (time < oldestTime) {
510
- oldestTime = time;
511
- oldestId = id;
512
- }
513
- }
514
- if (oldestId) {
515
- this.removeSession(oldestId, true);
516
- }
517
- }
518
- /**
519
- * Get session modes configuration.
520
- * Returns the available modes and current mode from the given session.
521
- * @param session - Optional session to get current mode from. If not provided, uses default.
522
- */
523
- async getSessionModes(session) {
524
- var _a, _b;
525
- // Use configured modes or default basic set
526
- const availableModes = (_a = this.options.availableModes) !== null && _a !== void 0 ? _a : (session ? await session.getAvailableModes().catch(() => []) : []);
527
- // Get current mode from the provided session or fall back to default
528
- const currentModeId = (_b = session === null || session === void 0 ? void 0 : session.getPermissionMode()) !== null && _b !== void 0 ? _b : 'default';
529
- return {
530
- availableModes,
531
- currentModeId,
532
- };
533
- }
534
- /**
535
- * Get session models configuration.
536
- * Returns the available models and current model from the given session.
537
- * Fetches available models from the session if supported.
538
- * Full raw model configurations are included in _meta['codebuddy.ai'].availableModels.
539
- * @param session - Optional session to get current model and available models from.
540
- */
541
- async getSessionModels(session) {
542
- var _a;
543
- // Attempt to fetch available models from session
544
- let availableModels = [];
545
- let rawModels = [];
546
- try {
547
- if (session) {
548
- // Fetch both simplified and raw models in parallel
549
- const [models, rawModelsResult] = await Promise.all([
550
- session.getAvailableModels(),
551
- session.getAvailableModelsRaw().catch(() => []),
552
- ]);
553
- rawModels = rawModelsResult;
554
- availableModels = models.map(m => ({
555
- modelId: m.modelId,
556
- name: m.name,
557
- description: m.description,
558
- }));
559
- }
560
- }
561
- catch (_b) {
562
- // CLI may not support getAvailableModels, silently fall back to empty array
563
- }
564
- // Get current model from the provided session
565
- // If no model is set, use undefined (no default fallback)
566
- const currentModelId = (_a = session === null || session === void 0 ? void 0 : session.getModel()) !== null && _a !== void 0 ? _a : undefined;
567
- return {
568
- availableModels,
569
- currentModelId: currentModelId,
570
- // Include full raw model configs in _meta under codebuddy.ai namespace
571
- _meta: rawModels.length > 0
572
- ? { 'codebuddy.ai': { availableModels: rawModels } }
573
- : undefined,
574
- };
575
- }
576
- /**
577
- * Subscribe to commands channel and forward all updates to ACP client.
578
- * Sets up a persistent listener that sends available_commands_update for each notification.
579
- * This ensures no updates are missed when commands change multiple times.
580
- * @param sessionId - The session ID to send the updates for
581
- * @param session - The session to subscribe to commands channel
582
- */
583
- async subscribeToCommands(sessionId, session) {
584
- try {
585
- // Clean up existing handler if present (e.g., when session/load is called multiple times)
586
- // This prevents duplicate handlers from accumulating on the transport
587
- const existingHandler = this.sessionCommandsHandlers.get(sessionId);
588
- if (existingHandler) {
589
- session.unsubscribeFromCommands(existingHandler);
590
- this.sessionCommandsHandlers.delete(sessionId);
591
- }
592
- // Create handler that forwards all commands notifications to ACP client
593
- const handler = (notification) => {
594
- const data = notification.data;
595
- const commands = data.commands.map(cmd => {
596
- const result = {
597
- // Remove leading '/' from command name if present
598
- name: cmd.name.startsWith('/') ? cmd.name.substring(1) : cmd.name,
599
- description: cmd.description || '',
600
- };
601
- if (cmd.argumentHint) {
602
- result.input = { hint: cmd.argumentHint };
603
- }
604
- return result;
605
- });
606
- // Send update to ACP client (fire-and-forget)
607
- this.connection.sessionUpdate({
608
- sessionId,
609
- update: {
610
- sessionUpdate: 'available_commands_update',
611
- availableCommands: commands,
612
- },
613
- }).catch(error => {
614
- console.error('[AcpAgent] Failed to send available commands update:', error);
615
- });
616
- };
617
- // Store handler for cleanup
618
- this.sessionCommandsHandlers.set(sessionId, handler);
619
- // Subscribe to commands channel via session's subscribeToCommands method
620
- await session.subscribeToCommands(handler);
621
- }
622
- catch (error) {
623
- // Silently ignore errors - available commands are optional
624
- console.error('[AcpAgent] Failed to subscribe to commands channel:', error);
625
- }
626
- }
627
- /**
628
- * Convert ACP ContentBlock[] to SDK UserMessage.
629
- * Delegates to AcpConverter for the actual content block conversion.
630
- */
631
- convertAcpPromptToUserMessage(sessionId, prompt) {
632
- const sdkContent = converter_1.AcpConverter.convertAcpPromptToSdk(prompt);
633
- return {
634
- type: 'user',
635
- session_id: sessionId,
636
- message: {
637
- role: 'user',
638
- content: sdkContent,
639
- },
640
- parent_tool_use_id: null,
641
- };
642
- }
643
- /**
644
- * Get or create a converter for a session.
645
- */
646
- getSessionConverter(sessionId) {
647
- let converter = this.sessionConverters.get(sessionId);
648
- if (!converter) {
649
- converter = new converter_1.AcpConverter();
650
- this.sessionConverters.set(sessionId, converter);
651
- }
652
- return converter;
653
- }
654
- /**
655
- * Send session updates to the client via AgentSideConnection.
656
- *
657
- * @param sessionId - Session ID
658
- * @param message - SDK message to convert and send
659
- * @param mode - Conversion mode: 'stream' for prompt(), 'history' for loadSession()
660
- */
661
- async sendUpdates(sessionId, message, mode = 'stream') {
662
- var _a, _b;
663
- const converter = this.getSessionConverter(sessionId);
664
- const updates = converter.convertToUpdates(message, { mode });
665
- for (const { update, _meta } of updates) {
666
- const meta = {
667
- mode,
668
- toolName: (_a = _meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) === null || _a === void 0 ? void 0 : _a.toolName,
669
- requestId: (_b = _meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) === null || _b === void 0 ? void 0 : _b.requestId,
670
- };
671
- // If onSessionUpdate callback is provided, call it for broadcasting
672
- if (this.options.onSessionUpdate) {
673
- this.options.onSessionUpdate(sessionId, update, meta);
674
- }
675
- // Always send to own connection
676
- try {
677
- await this.connection.sessionUpdate({
678
- sessionId,
679
- update,
680
- _meta: {
681
- ..._meta,
682
- // Mark message mode for router layer to decide whether to broadcast
683
- 'codebuddy.ai': {
684
- ...((_meta === null || _meta === void 0 ? void 0 : _meta['codebuddy.ai']) || {}),
685
- mode,
686
- },
687
- },
688
- });
689
- }
690
- catch (error) {
691
- console.error('[AcpAgent] Error sending session update:', error);
692
- }
693
- }
694
- }
695
- /**
696
- * Send system info notification when CLI reports model/permissionMode.
697
- * Uses custom notification '_codebuddy.ai/system_init' with model and permissionMode.
698
- */
699
- async sendSystemInfoNotification(sessionId, sysMsg) {
700
- try {
701
- await this.connection.extNotification('_codebuddy.ai/system_init', {
702
- sessionId,
703
- model: sysMsg.model,
704
- permissionMode: sysMsg.permissionMode,
705
- });
706
- }
707
- catch (_a) {
708
- // Silently ignore notification errors
709
- }
710
- }
711
- /**
712
- * Send user message echo to client.
713
- *
714
- * This sends user_message_chunk updates for the original ACP prompt blocks.
715
- * The text is formatted as @{uri} for resource_link blocks.
716
- * The original blocks are preserved in _meta['codebuddy.ai'].sourceContentBlocks.
717
- *
718
- * @param sessionId - The session ID
719
- * @param prompt - The original ACP content blocks from the prompt
720
- */
721
- async sendUserMessageEcho(sessionId, prompt) {
722
- try {
723
- // Convert ACP blocks to echo text format
724
- const echoText = converter_1.AcpConverter.convertAcpPromptToEchoText(prompt);
725
- // Send user_message_chunk with echo text and sourceContentBlocks
726
- await this.connection.sessionUpdate({
727
- sessionId,
728
- update: {
729
- sessionUpdate: 'user_message_chunk',
730
- content: { type: 'text', text: echoText },
731
- },
732
- _meta: {
733
- 'codebuddy.ai': {
734
- sourceContentBlocks: prompt,
735
- mode: 'stream',
736
- },
737
- },
738
- });
739
- }
740
- catch (error) {
741
- console.error('[AcpAgent] Error sending user message echo:', error);
742
- }
743
- }
744
- /**
745
- * Filter history messages: only keep the last topic message.
746
- */
747
- filterHistoryMessages(messages) {
748
- const result = [];
749
- let foundLastTopic = false;
750
- for (let i = messages.length - 1; i >= 0; i--) {
751
- const msg = messages[i];
752
- if ('type' in msg && msg.type === 'topic') {
753
- if (foundLastTopic) {
754
- continue; // Skip earlier topic messages
755
- }
756
- foundLastTopic = true;
757
- }
758
- result.unshift(msg);
759
- }
760
- return result;
761
- }
762
- // ============= AskUserQuestion Support =============
763
- /**
764
- * Create a canUseTool handler that wraps the original handler
765
- * and intercepts AskUserQuestion requests.
766
- *
767
- * @param sessionId - The session ID for this handler
768
- * @param originalHandler - Optional original canUseTool handler to delegate to
769
- * @returns A wrapped canUseTool handler
770
- */
771
- createCanUseToolHandler(sessionId, originalHandler) {
772
- return async (toolName, input, options) => {
773
- // Intercept AskUserQuestion tool
774
- if (toolName === 'AskUserQuestion') {
775
- return this.handleAskUserQuestion(sessionId, input, options);
776
- }
777
- // Delegate other tools to original handler
778
- if (originalHandler) {
779
- return originalHandler(toolName, input, options);
780
- }
781
- // Default: allow if no handler
782
- return { behavior: 'allow', updatedInput: input };
783
- };
784
- }
785
- /**
786
- * Handle AskUserQuestion tool requests.
787
- *
788
- * If onAskUserQuestion is provided, uses that handler.
789
- * Otherwise, sends the request to the client via ACP extMethod (_codebuddy.ai/question).
790
- *
791
- * @param sessionId - The session ID
792
- * @param input - The tool input (AskUserQuestionInput)
793
- * @param options - Permission options including toolUseID
794
- * @returns Permission result with answers or denial
795
- */
796
- async handleAskUserQuestion(sessionId, input, options) {
797
- var _a;
798
- const askInput = input;
799
- // Build high-level request for custom handler
800
- const askRequest = {
801
- sessionId,
802
- toolUseId: options.toolUseID,
803
- questions: askInput.questions,
804
- };
805
- try {
806
- let answers = null;
807
- // Use custom handler if provided
808
- if (this.options.onAskUserQuestion) {
809
- const response = await this.options.onAskUserQuestion(askRequest);
810
- answers = (_a = response === null || response === void 0 ? void 0 : response.answers) !== null && _a !== void 0 ? _a : null;
811
- }
812
- else {
813
- // Build ToolInputRequest for ACP protocol
814
- const toolInputRequest = {
815
- sessionId,
816
- toolCallId: options.toolUseID,
817
- inputType: 'question',
818
- schema: {
819
- questions: askInput.questions.map((q, idx) => ({
820
- id: `q_${idx}`,
821
- question: q.question,
822
- header: q.header,
823
- options: q.options,
824
- multiSelect: q.multiSelect,
825
- })),
826
- },
827
- };
828
- // Send to client via ACP extMethod
829
- const result = await this.connection.extMethod(exports.ACP_EXT_METHOD_QUESTION, toolInputRequest);
830
- // Parse ToolInputResponse
831
- const toolInputResponse = result;
832
- if (toolInputResponse.outcome.outcome === 'submitted') {
833
- // Convert QuestionInputData answers to simple Record<string, string>
834
- const data = toolInputResponse.outcome.data;
835
- answers = {};
836
- // Build id -> question text mapping for better model readability
837
- const questionMap = new Map(askInput.questions.map((q, idx) => [`q_${idx}`, q.question]));
838
- for (const [key, value] of Object.entries(data.answers)) {
839
- // Map q_0, q_1... back to actual question text
840
- const questionText = questionMap.get(key) || key;
841
- // Handle both string and string[] answers
842
- answers[questionText] = Array.isArray(value) ? value.join(', ') : value;
843
- }
844
- }
845
- }
846
- // Check for valid answers
847
- if (answers && Object.keys(answers).length > 0) {
848
- return {
849
- behavior: 'allow',
850
- updatedInput: {
851
- questions: askInput.questions,
852
- answers,
853
- },
854
- };
855
- }
856
- else {
857
- // User declined or no answers provided
858
- return {
859
- behavior: 'deny',
860
- message: 'User declined to answer questions',
861
- };
862
- }
863
- }
864
- catch (error) {
865
- // Handle errors (e.g., client doesn't support extMethod)
866
- return {
867
- behavior: 'deny',
868
- message: error instanceof Error
869
- ? error.message
870
- : 'Failed to get user answers',
871
- };
872
- }
873
- }
874
- /**
875
- * Clean up when the connection closes.
876
- * Removes all sessions and frees memory.
877
- */
878
- cleanup() {
879
- this.activePromptSessionId = null;
880
- this.promptAbortController = null;
881
- // Clean up all sessions when connection closes
882
- // closeSession=true to properly release resources
883
- this.removeAllSessions(true);
884
- }
885
- }
886
- exports.AcpAgent = AcpAgent;
887
- //# sourceMappingURL=agent.js.map