@adecore/agents 0.39.0 → 0.40.0

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 (168) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex.js +0 -1
  31. package/dist/providers/detect.js +1 -1
  32. package/dist/providers/shell-commands.js +1 -2
  33. package/dist/run-process.js +1 -3
  34. package/dist/skills.d.ts +1 -1
  35. package/dist/skills.js +5 -8
  36. package/dist/tasks/background-limit.js +3 -2
  37. package/dist/tasks/task-coordinator.js +11 -17
  38. package/dist/testing/watch.js +0 -1
  39. package/dist/usage/limits/monitor.js +1 -2
  40. package/dist/usage/readers/claude.js +3 -10
  41. package/dist/usage/readers/codex.js +2 -9
  42. package/dist/usage/readers/grok.js +3 -10
  43. package/dist/usage/readers/host-log.js +3 -10
  44. package/dist/usage/record.d.ts +2 -0
  45. package/dist/usage/record.js +13 -0
  46. package/package.json +6 -6
  47. package/src/async.ts +23 -5
  48. package/src/chat/acp-backend.ts +121 -0
  49. package/src/chat/acp-protocol.ts +172 -6
  50. package/src/chat/attachment-store.ts +21 -0
  51. package/src/chat/backend.ts +0 -2
  52. package/src/chat/bookmark-store.ts +23 -0
  53. package/src/chat/chat-core.ts +208 -7
  54. package/src/chat/chat-log.ts +19 -0
  55. package/src/chat/chat-process.ts +52 -1
  56. package/src/chat/chat-session.ts +401 -10
  57. package/src/chat/chat-store.ts +50 -0
  58. package/src/chat/chat-title.ts +9 -0
  59. package/src/chat/claude-backend.ts +50 -0
  60. package/src/chat/claude-protocol.ts +155 -8
  61. package/src/chat/claude-title.ts +10 -0
  62. package/src/chat/claude-transcript.ts +47 -4
  63. package/src/chat/codex-backend.ts +131 -0
  64. package/src/chat/codex-protocol.ts +180 -8
  65. package/src/chat/codex-thread.ts +23 -0
  66. package/src/chat/composer-preferences.ts +6 -0
  67. package/src/chat/context-breakdown.ts +11 -0
  68. package/src/chat/delta-coalescer.ts +8 -0
  69. package/src/chat/external-sessions.ts +28 -0
  70. package/src/chat/generated-image.ts +25 -0
  71. package/src/chat/index.ts +5 -0
  72. package/src/chat/input.ts +13 -0
  73. package/src/chat/json-rpc-transport.ts +28 -0
  74. package/src/chat/limit-resume.ts +13 -0
  75. package/src/chat/projector.ts +160 -4
  76. package/src/chat/readable-text.ts +3 -0
  77. package/src/chat/request-summary.ts +18 -4
  78. package/src/chat/subagent-projection.ts +3 -0
  79. package/src/chat/subagent-reader.ts +76 -0
  80. package/src/chat/subagent-settlement.ts +24 -0
  81. package/src/chat/thread-search.ts +13 -0
  82. package/src/chat/thread.ts +31 -0
  83. package/src/chat/ui-queries.ts +122 -0
  84. package/src/chat/ui-replies.ts +34 -0
  85. package/src/chat/visual-store.ts +49 -0
  86. package/src/chat/wake-chat.ts +3 -0
  87. package/src/client-sinks.ts +2 -0
  88. package/src/coded-error.ts +1 -5
  89. package/src/context/argv.ts +18 -2
  90. package/src/context/index.ts +1 -0
  91. package/src/context/refusal.ts +3 -0
  92. package/src/context/verb.ts +32 -4
  93. package/src/coordination/index.ts +10 -0
  94. package/src/error-text.ts +7 -0
  95. package/src/fields.ts +14 -0
  96. package/src/fs.ts +20 -4
  97. package/src/host/agent-host.ts +20 -0
  98. package/src/host/environment.ts +6 -0
  99. package/src/host/handlers.ts +18 -0
  100. package/src/host/memory-port.ts +4 -0
  101. package/src/host/wiring.ts +10 -0
  102. package/src/index.ts +1 -0
  103. package/src/lineage.ts +24 -5
  104. package/src/messages/deliver-message.ts +6 -0
  105. package/src/messages/deliver-notice.ts +13 -0
  106. package/src/messages/notice-store.ts +16 -4
  107. package/src/modes.ts +1 -0
  108. package/src/outbox/outbox-worker.ts +31 -0
  109. package/src/outbox/outbox.ts +5 -0
  110. package/src/providers/accounts/accounts.ts +36 -0
  111. package/src/providers/accounts/acp-account.ts +22 -27
  112. package/src/providers/accounts/launch.ts +4 -0
  113. package/src/providers/accounts/service.ts +110 -12
  114. package/src/providers/accounts/shadow-home.ts +10 -0
  115. package/src/providers/accounts/status.ts +13 -4
  116. package/src/providers/accounts/store.ts +25 -0
  117. package/src/providers/accounts/variables.ts +11 -1
  118. package/src/providers/acp-agent-state.ts +24 -3
  119. package/src/providers/acp-provider.ts +5 -0
  120. package/src/providers/acp-usage-log.ts +2 -0
  121. package/src/providers/catalog.ts +17 -0
  122. package/src/providers/claude.ts +10 -1
  123. package/src/providers/codex-rules.ts +3 -0
  124. package/src/providers/codex.ts +2 -1
  125. package/src/providers/detect.ts +6 -1
  126. package/src/providers/provider.ts +1 -1
  127. package/src/providers/registry.ts +18 -2
  128. package/src/providers/shell-commands.ts +37 -2
  129. package/src/record-directory.ts +19 -0
  130. package/src/run-process.ts +24 -3
  131. package/src/serializer.ts +3 -0
  132. package/src/skills.ts +73 -10
  133. package/src/tasks/background-limit.ts +9 -2
  134. package/src/tasks/end-children.ts +9 -0
  135. package/src/tasks/give-task.ts +8 -0
  136. package/src/tasks/task-coordinator.ts +81 -17
  137. package/src/tasks/task-store.ts +28 -0
  138. package/src/tasks/waiting-child.ts +21 -0
  139. package/src/tasks/wake-parent.ts +26 -0
  140. package/src/tasks/wiring.ts +22 -0
  141. package/src/testing/accounts.ts +2 -0
  142. package/src/testing/fake-acp-capture.ts +16 -0
  143. package/src/testing/fake-acp.ts +66 -0
  144. package/src/testing/fake-claude.ts +177 -0
  145. package/src/testing/fake-cli.ts +29 -0
  146. package/src/testing/fake-codex.ts +122 -0
  147. package/src/testing/manual-clock.ts +2 -0
  148. package/src/testing/watch.ts +7 -1
  149. package/src/title-file.ts +12 -0
  150. package/src/usage/accounts.ts +6 -0
  151. package/src/usage/aggregate.ts +19 -0
  152. package/src/usage/exchange.ts +12 -0
  153. package/src/usage/index-file.ts +27 -0
  154. package/src/usage/limits/monitor.ts +31 -3
  155. package/src/usage/limits/normalize.ts +30 -0
  156. package/src/usage/limits/probe.ts +17 -0
  157. package/src/usage/pricing.ts +35 -0
  158. package/src/usage/projects.ts +8 -0
  159. package/src/usage/readers/claude.ts +12 -9
  160. package/src/usage/readers/codex.ts +40 -8
  161. package/src/usage/readers/grok.ts +17 -9
  162. package/src/usage/readers/host-log.ts +9 -9
  163. package/src/usage/record.ts +22 -0
  164. package/src/usage/roots.ts +6 -0
  165. package/src/usage/scanner.ts +61 -0
  166. package/src/usage/usage-service.ts +14 -0
  167. package/src/watch-seam.ts +8 -0
  168. package/src/windows-command.ts +12 -0
@@ -92,6 +92,7 @@ const MAX_EMBEDDED_BYTES = 1024 * 1024;
92
92
 
93
93
  function inside(folder: string, path: string): boolean {
94
94
  const rest = relative(folder, path);
95
+
95
96
  return rest === '' || (!rest.startsWith('..') && !isAbsolute(rest));
96
97
  }
97
98
 
@@ -144,6 +145,7 @@ export class AcpBackend implements ChatBackend {
144
145
  if (this.transport) {
145
146
  return;
146
147
  }
148
+
147
149
  const transport = new JsonRpcTransport({
148
150
  command: this.launch.command,
149
151
  cwd: this.launch.cwd,
@@ -157,7 +159,9 @@ export class AcpBackend implements ChatBackend {
157
159
  onFrame: (frame) => this.handleFrame(transport, frame),
158
160
  onExit: (exitCode, stderr) => this.handleExit(transport, exitCode, stderr)
159
161
  });
162
+
160
163
  this.transport = transport;
164
+
161
165
  try {
162
166
  await this.handshake(transport);
163
167
  } catch (error) {
@@ -166,6 +170,7 @@ export class AcpBackend implements ChatBackend {
166
170
  this.transport = null;
167
171
  void transport.dispose();
168
172
  }
173
+
169
174
  throw error;
170
175
  }
171
176
  }
@@ -178,9 +183,11 @@ export class AcpBackend implements ChatBackend {
178
183
  clientInfo: { name: this.client.name, title: nameOf(this.client), version: this.client.version }
179
184
  })
180
185
  );
186
+
181
187
  if (capabilities.protocolVersion !== ACP_PROTOCOL_VERSION) {
182
188
  throw new Error(`${this.spec.name} speaks ACP version ${capabilities.protocolVersion}; only version ${ACP_PROTOCOL_VERSION} is supported`);
183
189
  }
190
+
184
191
  this.capabilities = capabilities;
185
192
  const info = await this.openSession(transport, capabilities);
186
193
  const modeId = await this.applyMode(transport, info);
@@ -188,6 +195,7 @@ export class AcpBackend implements ChatBackend {
188
195
  this.model = model;
189
196
  await this.applyThoughtLevel(transport, thoughtLevel);
190
197
  this.spec.onSession?.(info);
198
+
191
199
  this.emit({
192
200
  type: 'session',
193
201
  agentSessionId: this.sessionId,
@@ -195,6 +203,7 @@ export class AcpBackend implements ChatBackend {
195
203
  effectiveRuntimeMode: this.launch.runtimeMode,
196
204
  ...(modeId === null ? {} : { permissionMode: modeId })
197
205
  });
206
+
198
207
  for (const params of this.early.splice(0)) {
199
208
  this.emitAll(this.protocol.update(this.sessionId, params));
200
209
  }
@@ -202,6 +211,7 @@ export class AcpBackend implements ChatBackend {
202
211
 
203
212
  private sessionParams(): Record<string, unknown> {
204
213
  const folders = this.launch.folders ?? [];
214
+
205
215
  return {
206
216
  cwd: this.launch.cwd,
207
217
  mcpServers: [],
@@ -211,8 +221,10 @@ export class AcpBackend implements ChatBackend {
211
221
 
212
222
  private async openSession(transport: JsonRpcTransport, capabilities: AcpAgentCapabilities): Promise<AcpSessionInfo> {
213
223
  const resume = this.launch.resume;
224
+
214
225
  if (resume) {
215
226
  const method = capabilities.resumeSession ? 'session/resume' : capabilities.loadSession ? 'session/load' : null;
227
+
216
228
  if (method === null) {
217
229
  this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} cannot continue a session it ran before. Started a new one.` });
218
230
  } else {
@@ -221,12 +233,14 @@ export class AcpBackend implements ChatBackend {
221
233
  const result = await this.authenticated(transport, method, { sessionId: resume, ...this.sessionParams() });
222
234
  this.sessionId = resume;
223
235
  this.contextPending = [this.launch.resumeInstructions ?? null, this.launch.resumeNote].filter((part) => part !== null).join('\n\n') || null;
236
+
224
237
  return parseSessionInfo(result, resume, capabilities.agent);
225
238
  } catch (error) {
226
239
  // A refusal means the agent no longer has the session and a fresh one keeps the chat usable; a silence or an exit says nothing about it.
227
240
  if (!(error instanceof JsonRpcError)) {
228
241
  throw error;
229
242
  }
243
+
230
244
  this.emit({
231
245
  type: 'note',
232
246
  level: 'warning',
@@ -237,13 +251,17 @@ export class AcpBackend implements ChatBackend {
237
251
  }
238
252
  }
239
253
  }
254
+
240
255
  const result = await this.authenticated(transport, 'session/new', this.sessionParams());
241
256
  const sessionId = sessionIdOf(result);
257
+
242
258
  if (sessionId === null) {
243
259
  throw new Error(`session/new failed: ${this.spec.name} answered without a session id`);
244
260
  }
261
+
245
262
  this.sessionId = sessionId;
246
263
  this.contextPending = this.launch.instructions;
264
+
247
265
  return parseSessionInfo(result, sessionId, capabilities.agent);
248
266
  }
249
267
 
@@ -255,12 +273,16 @@ export class AcpBackend implements ChatBackend {
255
273
  if (!(error instanceof JsonRpcError) || error.code !== ACP_AUTH_REQUIRED) {
256
274
  throw error;
257
275
  }
276
+
258
277
  const methodId = this.spec.authMethod ?? null;
259
278
  const offered = this.capabilities?.authMethods ?? [];
279
+
260
280
  if (methodId === null || (offered.length > 0 && !offered.includes(methodId))) {
261
281
  throw new Error(`${this.spec.name} is not signed in.${this.spec.loginHint ? ` ${this.spec.loginHint}` : ''}`, { cause: error });
262
282
  }
283
+
263
284
  await transport.request('authenticate', { methodId }, { timeoutMs: null });
285
+
264
286
  return transport.request(method, params);
265
287
  }
266
288
  }
@@ -268,31 +290,40 @@ export class AcpBackend implements ChatBackend {
268
290
  /* Puts the session in the agent mode the runtime mode maps to; answers the mode it runs in, or null for an agent without modes. */
269
291
  private async applyMode(transport: JsonRpcTransport, info: AcpSessionInfo): Promise<string | null> {
270
292
  const modes = info.modes;
293
+
271
294
  if (modes === null || modes.modes.length === 0) {
272
295
  return null;
273
296
  }
297
+
274
298
  const map = this.spec.modes ?? {};
275
299
  const requested = this.launch.runtimeMode;
300
+
276
301
  const resolved = resolveAcpMode(
277
302
  requested,
278
303
  map,
279
304
  modes.modes.map((mode) => mode.id)
280
305
  );
306
+
281
307
  if (resolved === null) {
282
308
  const current = runtimeModeOf(modes.current, map);
309
+
283
310
  if (current !== null && MODE_ORDER.indexOf(current) > MODE_ORDER.indexOf(requested)) {
284
311
  throw new Error(`${this.spec.name} runs in ${modes.current}, which allows more than ${requested}, and offers no narrower mode`);
285
312
  }
313
+
286
314
  return modes.current;
287
315
  }
316
+
288
317
  if (resolved.modeId === modes.current) {
289
318
  return modes.current;
290
319
  }
320
+
291
321
  if (modes.configId === null) {
292
322
  await transport.request('session/set_mode', { sessionId: this.sessionId, modeId: resolved.modeId });
293
323
  } else {
294
324
  await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: modes.configId, value: resolved.modeId });
295
325
  }
326
+
296
327
  return resolved.modeId;
297
328
  }
298
329
 
@@ -304,30 +335,39 @@ export class AcpBackend implements ChatBackend {
304
335
  const models = info.models;
305
336
  const thoughtLevel = info.thoughtLevel;
306
337
  const wanted = this.spec.model ? this.spec.model(this.launch.selection) : this.launch.selection.model;
338
+
307
339
  if (models === null || wanted === null || wanted === models.current) {
308
340
  return { model: models?.current ?? wanted, thoughtLevel };
309
341
  }
342
+
310
343
  if (!models.models.some((model) => model.id === wanted)) {
311
344
  this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} does not offer ${this.launch.modelName}. It runs ${models.current}.` });
345
+
312
346
  return { model: models.current, thoughtLevel };
313
347
  }
348
+
314
349
  try {
315
350
  if (models.configId === null) {
316
351
  await transport.request('session/set_model', { sessionId: this.sessionId, modelId: wanted });
352
+
317
353
  return { model: wanted, thoughtLevel };
318
354
  }
355
+
319
356
  const result = await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: models.configId, value: wanted });
320
357
  const after = thoughtLevelOf(result);
358
+
321
359
  return { model: wanted, thoughtLevel: after === undefined ? thoughtLevel : after };
322
360
  } catch (error) {
323
361
  if (!(error instanceof JsonRpcError)) {
324
362
  throw error;
325
363
  }
364
+
326
365
  this.emit({
327
366
  type: 'note',
328
367
  level: 'warning',
329
368
  text: `${this.spec.name} did not switch to ${this.launch.modelName} (${errorText(error)}). It runs ${models.current}.`
330
369
  });
370
+
331
371
  return { model: models.current, thoughtLevel };
332
372
  }
333
373
  }
@@ -336,15 +376,18 @@ export class AcpBackend implements ChatBackend {
336
376
  private async applyThoughtLevel(transport: JsonRpcTransport, level: AcpThoughtLevel | null): Promise<void> {
337
377
  const effort = this.launch.selection.options.effort;
338
378
  const choice = level?.choices.find((entry) => entry.id === effort);
379
+
339
380
  if (level === null || choice === undefined || effort === level.current) {
340
381
  return;
341
382
  }
383
+
342
384
  try {
343
385
  await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: level.configId, value: choice.id });
344
386
  } catch (error) {
345
387
  if (!(error instanceof JsonRpcError)) {
346
388
  throw error;
347
389
  }
390
+
348
391
  const current = level.choices.find((entry) => entry.id === level.current)?.name ?? level.current;
349
392
  this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} kept its reasoning at ${current} instead of ${choice.name}.` });
350
393
  }
@@ -353,20 +396,25 @@ export class AcpBackend implements ChatBackend {
353
396
  private promptBlocks(input: TurnInput): unknown[] {
354
397
  const sendsImages = this.capabilities?.images === true;
355
398
  const images = sendsImages ? input.attachments.filter((attachment) => attachmentImageMime(attachment) !== null) : [];
399
+
356
400
  const blocks = images.map((attachment) => {
357
401
  let data: string;
402
+
358
403
  try {
359
404
  data = readFileSync(attachment.path).toString('base64');
360
405
  } catch {
361
406
  throw new Error(`Could not read attached image "${attachment.name}". Attach it again and retry.`);
362
407
  }
408
+
363
409
  return { type: 'image', mimeType: attachmentImageMime(attachment), data };
364
410
  });
411
+
365
412
  const named: ChatAttachment[] = input.attachments.filter((attachment) => !images.includes(attachment));
366
413
  const parts = [this.contextPending, input.preamble, input.text, attachmentNote(named)].filter((part): part is string => part !== null && part !== '');
367
414
  this.contextPending = null;
368
415
  const text = parts.join('\n\n');
369
416
  this.protocol.prompted(text);
417
+
370
418
  return [{ type: 'text', text }, ...blocks, ...this.mentionBlocks(input.mentions)];
371
419
  }
372
420
 
@@ -379,6 +427,7 @@ export class AcpBackend implements ChatBackend {
379
427
  if (this.capabilities?.embeddedContext !== true || mentions.length === 0) {
380
428
  return [];
381
429
  }
430
+
382
431
  const real = (path: string): string | null => {
383
432
  try {
384
433
  return realpathSync(path);
@@ -386,42 +435,56 @@ export class AcpBackend implements ChatBackend {
386
435
  return null;
387
436
  }
388
437
  };
438
+
389
439
  const folders = [this.launch.cwd, ...(this.launch.folders ?? [])].flatMap((folder) => real(folder) ?? []);
390
440
  const blocks: unknown[] = [];
391
441
  let budget = MAX_EMBEDDED_BYTES;
442
+
392
443
  for (const mention of new Set(mentions)) {
393
444
  const path = real(resolve(this.launch.cwd, mention));
445
+
394
446
  if (path === null || !folders.some((folder) => inside(folder, path))) {
395
447
  continue;
396
448
  }
449
+
397
450
  let text: string;
451
+
398
452
  try {
399
453
  const stat = statSync(path);
454
+
400
455
  if (!stat.isFile() || stat.size > MAX_EMBEDDED_FILE_BYTES || stat.size > budget) {
401
456
  continue;
402
457
  }
458
+
403
459
  text = readFileSync(path, 'utf8');
404
460
  } catch {
405
461
  continue;
406
462
  }
463
+
407
464
  if (text.includes('\u0000')) {
408
465
  continue;
409
466
  }
467
+
410
468
  budget -= Buffer.byteLength(text);
411
469
  blocks.push({ type: 'resource', resource: { uri: pathToFileURL(path).href, text } });
412
470
  }
471
+
413
472
  return blocks;
414
473
  }
415
474
 
416
475
  sendTurn(input: TurnInput): void {
417
476
  const transport = this.transport;
477
+
418
478
  if (!transport) {
419
479
  this.emit({ type: 'failed', message: `${this.spec.name} is not running` });
480
+
420
481
  return;
421
482
  }
483
+
422
484
  const prompt = this.promptBlocks(input);
423
485
  const turn = { promptId: input.promptId, cancelled: false };
424
486
  this.prompt = turn;
487
+
425
488
  transport
426
489
  .request('session/prompt', { sessionId: this.sessionId, prompt }, { timeoutMs: null })
427
490
  .then((result) => {
@@ -445,8 +508,10 @@ export class AcpBackend implements ChatBackend {
445
508
  compact(): void {
446
509
  if (this.commands.includes('compact')) {
447
510
  this.sendTurn({ text: '/compact', preamble: null, attachments: [], mentions: [], skills: [] });
511
+
448
512
  return;
449
513
  }
514
+
450
515
  this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} cannot compact its context.` });
451
516
  this.emit({ type: 'turn.done', state: 'done', costUsd: 0 });
452
517
  }
@@ -454,19 +519,25 @@ export class AcpBackend implements ChatBackend {
454
519
  /* Cancels the turn: every permission request still open is answered as cancelled, and the turn ends on the prompt's response. */
455
520
  interrupt(): void {
456
521
  const transport = this.transport;
522
+
457
523
  if (!transport) {
458
524
  return;
459
525
  }
526
+
460
527
  if (this.prompt === null) {
461
528
  this.emit({ type: 'turn.done', state: 'aborted', costUsd: 0 });
529
+
462
530
  return;
463
531
  }
532
+
464
533
  this.prompt.cancelled = true;
534
+
465
535
  try {
466
536
  for (const cancelled of this.protocol.cancelRequests()) {
467
537
  transport.respond(cancelled.rpcId, cancelled.result);
468
538
  this.emit({ type: 'request.withdrawn', requestId: cancelled.requestId });
469
539
  }
540
+
470
541
  transport.notify('session/cancel', { sessionId: this.sessionId });
471
542
  } catch (error) {
472
543
  this.fail(error);
@@ -475,33 +546,43 @@ export class AcpBackend implements ChatBackend {
475
546
 
476
547
  respondApproval(requestId: string, decision: ApprovalDecision): boolean {
477
548
  const answer = this.protocol.answer(requestId, decision);
549
+
478
550
  if (!answer) {
479
551
  return false;
480
552
  }
553
+
481
554
  if (!this.respond(requestId, answer.rpcId, answer.result)) {
482
555
  return false;
483
556
  }
557
+
484
558
  this.emitAll(answer.events);
559
+
485
560
  return true;
486
561
  }
487
562
 
488
563
  respondQuestion(requestId: string, answers: Record<string, string>): boolean {
489
564
  const answer = this.protocol.answerQuestion(requestId, answers);
565
+
490
566
  if (!answer) {
491
567
  return false;
492
568
  }
569
+
493
570
  if (!this.respond(requestId, answer.rpcId, answer.result)) {
494
571
  return false;
495
572
  }
573
+
496
574
  this.emitAll(answer.events);
575
+
497
576
  return true;
498
577
  }
499
578
 
500
579
  declineRequest(requestId: string): boolean {
501
580
  if (this.protocol.hasQuestion(requestId)) {
502
581
  const cancelled = this.protocol.cancelQuestion(requestId);
582
+
503
583
  return cancelled !== null && this.respond(requestId, cancelled.rpcId, cancelled.result);
504
584
  }
585
+
505
586
  return this.protocol.hasPermission(requestId) && this.respondApproval(requestId, 'deny');
506
587
  }
507
588
 
@@ -512,6 +593,7 @@ export class AcpBackend implements ChatBackend {
512
593
  dispose(): Promise<void> {
513
594
  const transport = this.transport;
514
595
  this.transport = null;
596
+
515
597
  return transport?.dispose() ?? Promise.resolve();
516
598
  }
517
599
 
@@ -520,11 +602,14 @@ export class AcpBackend implements ChatBackend {
520
602
  if (!this.transport) {
521
603
  throw new Error(`${this.spec.name} is not running`);
522
604
  }
605
+
523
606
  this.transport.respond(rpcId, result);
607
+
524
608
  return true;
525
609
  } catch (error) {
526
610
  this.emit({ type: 'request.withdrawn', requestId });
527
611
  this.fail(error);
612
+
528
613
  return false;
529
614
  }
530
615
  }
@@ -537,58 +622,84 @@ export class AcpBackend implements ChatBackend {
537
622
  if (this.transport !== transport) {
538
623
  return;
539
624
  }
625
+
540
626
  const id = typeof frame.id === 'number' || typeof frame.id === 'string' ? frame.id : null;
627
+
541
628
  if (frame.method === 'session/update' && id === null) {
542
629
  if (this.sessionId === '' || this.replaying) {
543
630
  this.holdEarly(frame.params);
631
+
544
632
  return;
545
633
  }
634
+
546
635
  this.emitAll(this.protocol.update(this.sessionId, frame.params));
636
+
547
637
  return;
548
638
  }
639
+
549
640
  if (id === null) {
550
641
  return;
551
642
  }
643
+
552
644
  if (typeof frame.method === 'string' && GROK_QUESTION_METHODS.includes(frame.method)) {
553
645
  const question = this.protocol.question(id, this.sessionId, frame.params);
646
+
554
647
  if (question === null) {
555
648
  transport.respondError(id, -32602, 'Not a question of this session');
649
+
556
650
  return;
557
651
  }
652
+
558
653
  this.ask(transport, question);
654
+
559
655
  return;
560
656
  }
657
+
561
658
  if (frame.method !== 'session/request_permission') {
562
659
  transport.respondError(id, -32601, `${nameOf(this.client)} does not handle ${String(frame.method)}`);
660
+
563
661
  return;
564
662
  }
663
+
565
664
  // Before any runtime mode answers it: a question is never the agent's to allow.
566
665
  const interaction = this.protocol.interaction(id, this.sessionId, frame.params);
666
+
567
667
  if (interaction !== null) {
568
668
  this.ask(transport, interaction);
669
+
569
670
  return;
570
671
  }
672
+
571
673
  const permission = this.protocol.permission(id, this.sessionId, frame.params);
674
+
572
675
  if (permission === null) {
573
676
  transport.respondError(id, -32602, 'Not a permission request of this session');
677
+
574
678
  return;
575
679
  }
680
+
576
681
  if (this.prompt?.cancelled) {
577
682
  this.emitAll(permission.events.filter((event) => event.type !== 'approval.requested'));
578
683
  const answer = this.protocol.cancelRequests().find((cancelled) => cancelled.requestId === permission.requestId);
684
+
579
685
  if (answer) {
580
686
  transport.respond(answer.rpcId, answer.result);
581
687
  }
688
+
582
689
  return;
583
690
  }
691
+
584
692
  if (autoAllows(this.launch.runtimeMode, permission.kind) || this.hostAllows(permission.request)) {
585
693
  this.emitAll(permission.events.filter((event) => event.type !== 'approval.requested'));
586
694
  const answer = this.protocol.answer(permission.requestId, 'allow');
695
+
587
696
  if (answer) {
588
697
  this.respond(permission.requestId, answer.rpcId, answer.result);
589
698
  }
699
+
590
700
  return;
591
701
  }
702
+
592
703
  this.emitAll(permission.events);
593
704
  }
594
705
 
@@ -597,17 +708,21 @@ export class AcpBackend implements ChatBackend {
597
708
  if (this.prompt?.cancelled) {
598
709
  this.emitAll(question.events.filter((event) => event.type !== 'question.requested'));
599
710
  const cancelled = this.protocol.cancelQuestion(question.requestId);
711
+
600
712
  if (cancelled) {
601
713
  transport.respond(cancelled.rpcId, cancelled.result);
602
714
  }
715
+
603
716
  return;
604
717
  }
718
+
605
719
  this.emitAll(question.events);
606
720
  }
607
721
 
608
722
  /* Holds an update until the session is known; of a history a load replays, only the commands, since the thread has the rest. */
609
723
  private holdEarly(params: unknown): void {
610
724
  const update = sessionUpdateOf(params);
725
+
611
726
  if (update !== null && (!this.replaying || update.commands !== null)) {
612
727
  this.early.push(params);
613
728
  }
@@ -625,6 +740,7 @@ export class AcpBackend implements ChatBackend {
625
740
  if (this.transport !== transport) {
626
741
  return;
627
742
  }
743
+
628
744
  this.transport = null;
629
745
  this.prompt = null;
630
746
  this.protocol.cancelRequests();
@@ -634,9 +750,11 @@ export class AcpBackend implements ChatBackend {
634
750
  /* A cancelled turn still spent what it reports. A host that fails to keep it costs the record, never the end of the turn. */
635
751
  private report(result: unknown): void {
636
752
  const usage = this.spec.onUsage === undefined ? null : promptUsageOf(result);
753
+
637
754
  if (usage === null) {
638
755
  return;
639
756
  }
757
+
640
758
  try {
641
759
  this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
642
760
  } catch (error) {
@@ -654,14 +772,17 @@ export class AcpBackend implements ChatBackend {
654
772
  if (event.type === 'model') {
655
773
  this.model = event.model;
656
774
  }
775
+
657
776
  if (event.type === 'session' && event.slashCommands !== undefined) {
658
777
  this.commands = event.slashCommands;
659
778
  this.spec.onCommands?.(event.slashCommands);
660
779
  }
780
+
661
781
  if (event.type === 'usage' && event.contextWindow !== undefined && !this.contextReported) {
662
782
  this.contextReported = true;
663
783
  this.spec.onContextReport?.();
664
784
  }
785
+
665
786
  this.host.onEvent(event);
666
787
  }
667
788
  }