@adecore/agents 0.39.0 → 0.40.1

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 (172) 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-models.json +3 -2
  31. package/dist/providers/codex.js +0 -1
  32. package/dist/providers/detect.js +1 -1
  33. package/dist/providers/shell-commands.js +1 -2
  34. package/dist/run-process.js +1 -3
  35. package/dist/skills.d.ts +1 -1
  36. package/dist/skills.js +5 -8
  37. package/dist/tasks/background-limit.js +3 -2
  38. package/dist/tasks/task-coordinator.js +11 -17
  39. package/dist/testing/watch.js +0 -1
  40. package/dist/usage/limits/monitor.js +1 -2
  41. package/dist/usage/prices-snapshot.json +8 -0
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +208 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +401 -10
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-models.json +3 -2
  126. package/src/providers/codex-rules.ts +3 -0
  127. package/src/providers/codex.ts +2 -1
  128. package/src/providers/detect.ts +6 -1
  129. package/src/providers/provider.ts +1 -1
  130. package/src/providers/registry.ts +18 -2
  131. package/src/providers/shell-commands.ts +37 -2
  132. package/src/record-directory.ts +19 -0
  133. package/src/run-process.ts +24 -3
  134. package/src/serializer.ts +3 -0
  135. package/src/skills.ts +73 -10
  136. package/src/tasks/background-limit.ts +9 -2
  137. package/src/tasks/end-children.ts +9 -0
  138. package/src/tasks/give-task.ts +8 -0
  139. package/src/tasks/task-coordinator.ts +81 -17
  140. package/src/tasks/task-store.ts +28 -0
  141. package/src/tasks/waiting-child.ts +21 -0
  142. package/src/tasks/wake-parent.ts +26 -0
  143. package/src/tasks/wiring.ts +22 -0
  144. package/src/testing/accounts.ts +2 -0
  145. package/src/testing/fake-acp-capture.ts +16 -0
  146. package/src/testing/fake-acp.ts +66 -0
  147. package/src/testing/fake-claude.ts +177 -0
  148. package/src/testing/fake-cli.ts +29 -0
  149. package/src/testing/fake-codex.ts +122 -0
  150. package/src/testing/manual-clock.ts +2 -0
  151. package/src/testing/watch.ts +7 -1
  152. package/src/title-file.ts +12 -0
  153. package/src/usage/accounts.ts +6 -0
  154. package/src/usage/aggregate.ts +19 -0
  155. package/src/usage/exchange.ts +12 -0
  156. package/src/usage/index-file.ts +27 -0
  157. package/src/usage/limits/monitor.ts +31 -3
  158. package/src/usage/limits/normalize.ts +30 -0
  159. package/src/usage/limits/probe.ts +17 -0
  160. package/src/usage/prices-snapshot.json +8 -0
  161. package/src/usage/pricing.ts +35 -0
  162. package/src/usage/projects.ts +8 -0
  163. package/src/usage/readers/claude.ts +12 -9
  164. package/src/usage/readers/codex.ts +40 -8
  165. package/src/usage/readers/grok.ts +17 -9
  166. package/src/usage/readers/host-log.ts +9 -9
  167. package/src/usage/record.ts +22 -0
  168. package/src/usage/roots.ts +6 -0
  169. package/src/usage/scanner.ts +61 -0
  170. package/src/usage/usage-service.ts +14 -0
  171. package/src/watch-seam.ts +8 -0
  172. package/src/windows-command.ts +12 -0
@@ -10,27 +10,22 @@ import type {
10
10
  } from '@adecore/agent-contracts';
11
11
  import { isRecord } from '@adecore/agent-contracts';
12
12
  import { readClaudeEvent } from '../usage/limits/normalize.ts';
13
+ import { num, str } from '../fields.ts';
13
14
  import { claudeRuntimeMode } from '../providers/claude.ts';
14
15
  import type { ApprovalDecision, BackendEvent } from './backend.ts';
15
16
 
16
17
  type Frame = Record<string, unknown>;
17
18
 
18
- function str(value: unknown): string | null {
19
- return typeof value === 'string' ? value : null;
20
- }
21
-
22
- function num(value: unknown): number {
23
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
24
- }
25
-
26
19
  // A tool result is a string or a list of content blocks; only the text is worth showing.
27
20
  function resultText(content: unknown): string {
28
21
  if (typeof content === 'string') {
29
22
  return content;
30
23
  }
24
+
31
25
  if (!Array.isArray(content)) {
32
26
  return '';
33
27
  }
28
+
34
29
  return content
35
30
  .map((block) => (isRecord(block) && block.type === 'text' ? str(block.text) : null))
36
31
  .filter((text): text is string => text !== null)
@@ -55,10 +50,13 @@ const CACHE_TTL_MS: Record<string, number> = { ephemeral_5m_input_tokens: 5 * 60
55
50
  */
56
51
  function cacheTtlWritten(usage: unknown): number | null {
57
52
  const creation = isRecord(usage) && isRecord(usage.cache_creation) ? usage.cache_creation : null;
53
+
58
54
  if (creation === null) {
59
55
  return null;
60
56
  }
57
+
61
58
  const written = Object.entries(CACHE_TTL_MS).filter(([field]) => num(creation[field]) > 0);
59
+
62
60
  return written.length === 0 ? null : Math.min(...written.map(([, ttl]) => ttl));
63
61
  }
64
62
 
@@ -83,14 +81,18 @@ function intOrNull(value: unknown): number | null {
83
81
  function parseWorkflowProgress(entries: unknown[]): Pick<ChatWorkflow, 'phases' | 'agents'> {
84
82
  const phases = new Map<number, ChatWorkflowPhase>();
85
83
  const agents = new Map<number, ChatWorkflowAgent>();
84
+
86
85
  for (const entry of entries) {
87
86
  if (!isRecord(entry)) {
88
87
  continue;
89
88
  }
89
+
90
90
  const index = intOrNull(entry.index);
91
+
91
92
  if (index === null) {
92
93
  continue;
93
94
  }
95
+
94
96
  if (entry.type === 'workflow_phase') {
95
97
  phases.set(index, { index, title: str(entry.title) ?? '' });
96
98
  } else if (entry.type === 'workflow_agent') {
@@ -99,6 +101,7 @@ function parseWorkflowProgress(entries: unknown[]): Pick<ChatWorkflow, 'phases'
99
101
  const promptPreview = str(entry.promptPreview);
100
102
  const resultPreview = str(entry.resultPreview);
101
103
  const error = str(entry.error);
104
+
102
105
  agents.set(index, {
103
106
  index,
104
107
  label: str(entry.label) ?? '',
@@ -115,7 +118,9 @@ function parseWorkflowProgress(entries: unknown[]): Pick<ChatWorkflow, 'phases'
115
118
  });
116
119
  }
117
120
  }
121
+
118
122
  const byIndex = (a: { index: number }, b: { index: number }): number => a.index - b.index;
123
+
119
124
  return { phases: [...phases.values()].sort(byIndex), agents: [...agents.values()].sort(byIndex) };
120
125
  }
121
126
 
@@ -124,6 +129,7 @@ function parseQuestions(input: unknown): ChatQuestion[] {
124
129
  if (!isRecord(input) || !Array.isArray(input.questions)) {
125
130
  return [];
126
131
  }
132
+
127
133
  return input.questions.filter(isRecord).map((question, index) => ({
128
134
  id: String(index),
129
135
  header: str(question.header) ?? '',
@@ -151,20 +157,27 @@ const GOAL_GONE = ['Goal cleared: ', 'Goal cleared after ', 'No goal set'];
151
157
 
152
158
  export function claudeGoalFromCommand(text: string, current: ChatGoal | null): ChatGoal | null | undefined {
153
159
  const said = text.trim();
160
+
154
161
  if (said.startsWith(GOAL_SET)) {
155
162
  const objective = said.slice(GOAL_SET.length).trim();
163
+
156
164
  return objective === '' ? undefined : { objective, status: 'active', checks: 0 };
157
165
  }
166
+
158
167
  if (GOAL_GONE.some((prefix) => said.startsWith(prefix))) {
159
168
  return null;
160
169
  }
170
+
161
171
  const active = GOAL_ACTIVE.exec(said);
172
+
162
173
  if (!active) {
163
174
  return undefined;
164
175
  }
176
+
165
177
  const objective = active[1]!;
166
178
  const checks = /\d+/.exec(active[2]!)?.[0];
167
179
  const lastCheck = active[3]?.trim();
180
+
168
181
  return {
169
182
  ...(current?.objective === objective ? current : {}),
170
183
  objective,
@@ -182,9 +195,11 @@ function nextOrdinal(counts: Map<string, number>, messageId: string): number {
182
195
  const ordinal = counts.get(messageId) ?? 0;
183
196
  counts.delete(messageId);
184
197
  counts.set(messageId, ordinal + 1);
198
+
185
199
  if (counts.size > COUNTED_MESSAGES) {
186
200
  counts.delete(counts.keys().next().value!);
187
201
  }
202
+
188
203
  return ordinal;
189
204
  }
190
205
 
@@ -196,11 +211,15 @@ export function claudeGoalCheck(text: string, current: ChatGoal | null): ChatGoa
196
211
  if (current === null) {
197
212
  return undefined;
198
213
  }
214
+
199
215
  const prefix = `Stop hook feedback:\n[${current.objective}]: `;
216
+
200
217
  if (!text.startsWith(prefix)) {
201
218
  return undefined;
202
219
  }
220
+
203
221
  const lastCheck = text.slice(prefix.length).trim();
222
+
204
223
  return { ...current, status: 'active', checks: (current.checks ?? 0) + 1, ...(lastCheck === '' ? {} : { lastCheck }) };
205
224
  }
206
225
 
@@ -284,65 +303,83 @@ export class ClaudeProtocol {
284
303
 
285
304
  handle(frame: unknown): BackendEvent[] {
286
305
  const events: BackendEvent[] = [];
306
+
287
307
  if (!isRecord(frame)) {
288
308
  return events;
289
309
  }
310
+
290
311
  switch (frame.type) {
291
312
  case 'system':
292
313
  this.handleSystem(frame, events);
314
+
293
315
  break;
294
316
  case 'stream_event':
295
317
  this.handleStreamEvent(frame, events);
318
+
296
319
  break;
297
320
  case 'assistant':
298
321
  this.handleAssistant(frame, events);
322
+
299
323
  break;
300
324
  case 'user':
301
325
  this.handleUser(frame, events);
326
+
302
327
  break;
303
328
  case 'result':
304
329
  this.handleResult(frame, events);
330
+
305
331
  break;
306
332
  case 'control_request':
307
333
  this.handleControlRequest(frame, events);
334
+
308
335
  break;
309
336
  case 'control_response':
310
337
  this.handleControlResponse(frame, events);
338
+
311
339
  break;
312
340
  case 'control_cancel_request': {
313
341
  const requestId = str(frame.request_id);
342
+
314
343
  if (requestId) {
315
344
  this.pending.delete(requestId);
316
345
  events.push({ type: 'request.withdrawn', requestId });
317
346
  }
347
+
318
348
  break;
319
349
  }
320
350
  case 'tool_progress':
321
351
  this.handleToolProgress(frame, events);
352
+
322
353
  break;
323
354
  case 'rate_limit_event': {
324
355
  this.noteRefusal(frame.rate_limit_info);
325
356
  // The plan's own numbers, streamed while a turn runs; the usage monitor keeps them.
326
357
  const update = readClaudeEvent(frame.rate_limit_info);
358
+
327
359
  if (update !== null) {
328
360
  events.push({ type: 'limits', update });
329
361
  }
362
+
330
363
  break;
331
364
  }
332
365
  default:
333
366
  break;
334
367
  }
368
+
335
369
  return events;
336
370
  }
337
371
 
338
372
  /* The `control_response` that answers a permission request, or null when nothing waits under that id. */
339
373
  approvalResponse(requestId: string, decision: ApprovalDecision, message?: string): unknown | null {
340
374
  const pending = this.pending.get(requestId);
375
+
341
376
  if (pending?.type !== 'approval') {
342
377
  return null;
343
378
  }
379
+
344
380
  this.pending.delete(requestId);
345
381
  const toolUseID = pending.toolUseId ?? undefined;
382
+
346
383
  const response =
347
384
  decision === 'deny'
348
385
  ? { behavior: 'deny', message: message?.trim() || 'The user declined this action', toolUseID }
@@ -353,25 +390,32 @@ export class ClaudeProtocol {
353
390
  // The CLI's own suggestion of a rule; sending it back is what makes "always" stick.
354
391
  ...(decision === 'allow-always' && pending.suggestions.length > 0 ? { updatedPermissions: pending.suggestions } : {})
355
392
  };
393
+
356
394
  return { type: 'control_response', response: { subtype: 'success', request_id: requestId, response } };
357
395
  }
358
396
 
359
397
  /* The `control_response` that answers an AskUserQuestion, or null when nothing waits under that id. */
360
398
  questionResponse(requestId: string, answers: Record<string, string>): unknown | null {
361
399
  const pending = this.pending.get(requestId);
400
+
362
401
  if (pending?.type !== 'question') {
363
402
  return null;
364
403
  }
404
+
365
405
  this.pending.delete(requestId);
366
406
  // The CLI keys answers by the question text, the client by the question's id.
367
407
  const byText: Record<string, string> = {};
408
+
368
409
  for (const question of parseQuestions(pending.input)) {
369
410
  const given = answers[question.id];
411
+
370
412
  if (given !== undefined) {
371
413
  byText[question.question] = given;
372
414
  }
373
415
  }
416
+
374
417
  const input = isRecord(pending.input) ? pending.input : {};
418
+
375
419
  return {
376
420
  type: 'control_response',
377
421
  response: {
@@ -385,10 +429,13 @@ export class ClaudeProtocol {
385
429
  /* The `control_response` that turns down an approval or a question the CLI still holds, or null when nothing waits under that id. */
386
430
  declineResponse(requestId: string, message: string): unknown | null {
387
431
  const pending = this.pending.get(requestId);
432
+
388
433
  if (pending === undefined) {
389
434
  return null;
390
435
  }
436
+
391
437
  this.pending.delete(requestId);
438
+
392
439
  return {
393
440
  type: 'control_response',
394
441
  response: { subtype: 'success', request_id: requestId, response: { behavior: 'deny', message, toolUseID: pending.toolUseId ?? undefined } }
@@ -404,9 +451,11 @@ export class ClaudeProtocol {
404
451
  const commands = Array.isArray(frame.slash_commands)
405
452
  ? frame.slash_commands.filter((command): command is string => typeof command === 'string')
406
453
  : [];
454
+
407
455
  const skills = Array.isArray(frame.skills) ? frame.skills.filter((skill): skill is string => typeof skill === 'string') : [];
408
456
  const permissionMode = str(frame.permissionMode);
409
457
  const effectiveRuntimeMode = permissionMode === null ? undefined : claudeRuntimeMode(permissionMode);
458
+
410
459
  events.push({
411
460
  type: 'session',
412
461
  agentSessionId: str(frame.session_id),
@@ -425,9 +474,11 @@ export class ClaudeProtocol {
425
474
  this.handleTaskUpdated(frame, events);
426
475
  } else if (frame.subtype === 'task_progress') {
427
476
  const ref = str(frame.tool_use_id);
477
+
428
478
  if (!ref) {
429
479
  return;
430
480
  }
481
+
431
482
  // Only some of a workflow's progress frames carry it; the rest only count what it spent.
432
483
  if (Array.isArray(frame.workflow_progress)) {
433
484
  events.push({
@@ -440,6 +491,7 @@ export class ClaudeProtocol {
440
491
  }
441
492
  });
442
493
  }
494
+
443
495
  // Only a subagent reports what it spent; a task without that is a command with a description.
444
496
  if (frame.task_type === 'local_agent' || str(frame.subagent_type)) {
445
497
  events.push({
@@ -452,21 +504,26 @@ export class ClaudeProtocol {
452
504
  });
453
505
  } else {
454
506
  const description = str(frame.description);
507
+
455
508
  if (description) {
456
509
  events.push({ type: 'tool.progress', ref, startedAt: null, description });
457
510
  }
458
511
  }
459
512
  } else if (frame.subtype === 'task_notification') {
460
513
  const ref = str(frame.tool_use_id);
514
+
461
515
  if (ref) {
462
516
  this.subagentModels.delete(ref);
463
517
  }
518
+
464
519
  const taskId = str(frame.task_id);
520
+
465
521
  if (taskId) {
466
522
  this.endBackground(taskId, events);
467
523
  this.workflows.delete(taskId);
468
524
  this.backgroundAgents.delete(taskId);
469
525
  }
526
+
470
527
  // The CLI wakes the main agent itself when a background task settles; this frame is the only
471
528
  // thing that says what it was about, and it arrives before the turn nobody asked for.
472
529
  events.push({
@@ -487,11 +544,14 @@ export class ClaudeProtocol {
487
544
 
488
545
  private handleTaskStarted(frame: Frame, events: BackendEvent[]): void {
489
546
  const ref = str(frame.tool_use_id);
547
+
490
548
  if (frame.task_type === 'local_agent') {
491
549
  const agentTask = str(frame.task_id);
550
+
492
551
  if (agentTask && frame.is_backgrounded === true) {
493
552
  this.backgroundAgents.add(agentTask);
494
553
  }
554
+
495
555
  if (ref) {
496
556
  events.push({
497
557
  type: 'task.started',
@@ -504,14 +564,18 @@ export class ClaudeProtocol {
504
564
  ...(typeof frame.spawn_depth === 'number' ? { depth: frame.spawn_depth } : {})
505
565
  });
506
566
  }
567
+
507
568
  return;
508
569
  }
570
+
509
571
  const taskId = str(frame.task_id);
510
572
  const description = str(frame.description);
573
+
511
574
  if (taskId && ref && frame.task_type === 'local_workflow') {
512
575
  this.workflows.set(taskId, ref);
513
576
  events.push({ type: 'workflow.progress', ref, workflow: { name: str(frame.workflow_name), taskId, phases: [], agents: [] } });
514
577
  }
578
+
515
579
  if (taskId && typeof frame.task_type === 'string' && BACKGROUND_TASK_TYPES.has(frame.task_type) && frame.ambient !== true) {
516
580
  if (frame.is_backgrounded === false) {
517
581
  this.foregroundShells.set(taskId, { ref, description });
@@ -519,6 +583,7 @@ export class ClaudeProtocol {
519
583
  this.startBackground(taskId, ref, frame.task_type !== 'local_bash', description, events);
520
584
  }
521
585
  }
586
+
522
587
  // A Bash call became a task; its description is the CLI's own words for the work.
523
588
  if (ref && description) {
524
589
  events.push({ type: 'tool.progress', ref, startedAt: null, description });
@@ -529,15 +594,20 @@ export class ClaudeProtocol {
529
594
  private handleTaskUpdated(frame: Frame, events: BackendEvent[]): void {
530
595
  const taskId = str(frame.task_id);
531
596
  const patch = isRecord(frame.patch) ? frame.patch : {};
597
+
532
598
  if (!taskId) {
533
599
  return;
534
600
  }
601
+
535
602
  const shell = this.foregroundShells.get(taskId);
603
+
536
604
  if (shell && patch.is_backgrounded === true) {
537
605
  this.foregroundShells.delete(taskId);
538
606
  this.startBackground(taskId, shell.ref, false, str(patch.description) ?? shell.description, events);
539
607
  }
608
+
540
609
  const status = str(patch.status);
610
+
541
611
  if (status !== null && ENDED_TASK_STATUSES.has(status)) {
542
612
  this.foregroundShells.delete(taskId);
543
613
  this.backgroundAgents.delete(taskId);
@@ -550,6 +620,7 @@ export class ClaudeProtocol {
550
620
  if (this.backgroundTasks.has(taskId)) {
551
621
  return;
552
622
  }
623
+
553
624
  this.backgroundTasks.add(taskId);
554
625
  events.push({ type: 'background.started', taskId, ref, monitor, description });
555
626
  }
@@ -566,9 +637,11 @@ export class ClaudeProtocol {
566
637
  */
567
638
  private endWorkflow(taskId: string, summary: string | null, ok: boolean, events: BackendEvent[]): void {
568
639
  const ref = this.workflows.get(taskId);
640
+
569
641
  if (ref === undefined) {
570
642
  return;
571
643
  }
644
+
572
645
  this.workflows.delete(taskId);
573
646
  events.push({ type: 'task.done', ref, taskId, summary, ok });
574
647
  }
@@ -577,6 +650,7 @@ export class ClaudeProtocol {
577
650
  stopTaskRequest(taskId: string): unknown {
578
651
  const requestId = `stop-task-${taskId}-${Date.now()}`;
579
652
  this.stopRequests.set(requestId, taskId);
653
+
580
654
  return { type: 'control_request', request_id: requestId, request: { subtype: 'stop_task', task_id: taskId } };
581
655
  }
582
656
 
@@ -585,10 +659,13 @@ export class ClaudeProtocol {
585
659
  const response = isRecord(frame.response) ? frame.response : null;
586
660
  const requestId = str(response?.request_id);
587
661
  const taskId = requestId === null ? undefined : this.stopRequests.get(requestId);
662
+
588
663
  if (response === null || requestId === null || taskId === undefined) {
589
664
  return;
590
665
  }
666
+
591
667
  this.stopRequests.delete(requestId);
668
+
592
669
  if (response.subtype === 'error') {
593
670
  events.push({ type: 'stop.refused', taskId, message: str(response.error) ?? 'The CLI refused to stop it' });
594
671
  }
@@ -596,9 +673,11 @@ export class ClaudeProtocol {
596
673
 
597
674
  private handleStreamEvent(frame: Frame, events: BackendEvent[]): void {
598
675
  const event = frame.event;
676
+
599
677
  if (!isRecord(event) || str(frame.parent_tool_use_id)) {
600
678
  return;
601
679
  }
680
+
602
681
  if (event.type === 'message_start') {
603
682
  const message = isRecord(event.message) ? event.message : {};
604
683
  this.noteCache(str(message.id), message.usage, events);
@@ -606,29 +685,38 @@ export class ClaudeProtocol {
606
685
  this.streamTextCount = 0;
607
686
  this.streamThinkingCount = 0;
608
687
  this.streamBlocks.clear();
688
+
609
689
  return;
610
690
  }
691
+
611
692
  const index = num(event.index);
693
+
612
694
  if (event.type === 'content_block_start' && isRecord(event.content_block) && this.streamMessageId) {
613
695
  if (event.content_block.type === 'text') {
614
696
  const ref = this.textRef(this.streamMessageId, this.streamTextCount++);
615
697
  this.streamBlocks.set(index, ref);
616
698
  events.push({ type: 'text.delta', ref, text: str(event.content_block.text) ?? '' });
699
+
617
700
  return;
618
701
  }
702
+
619
703
  if (event.content_block.type === 'thinking') {
620
704
  const ref = this.thinkingRef(this.streamMessageId, this.streamThinkingCount++);
621
705
  this.streamBlocks.set(index, ref);
622
706
  events.push({ type: 'thinking.delta', ref, text: str(event.content_block.thinking) ?? '' });
707
+
623
708
  return;
624
709
  }
625
710
  }
711
+
626
712
  if (event.type === 'content_block_delta' && isRecord(event.delta)) {
627
713
  const ref = this.streamBlocks.get(index);
628
714
  const text = str(event.delta.text) ?? str(event.delta.thinking);
715
+
629
716
  if (!ref || !text) {
630
717
  return;
631
718
  }
719
+
632
720
  if (event.delta.type === 'text_delta') {
633
721
  events.push({ type: 'text.delta', ref, text });
634
722
  } else if (event.delta.type === 'thinking_delta') {
@@ -646,13 +734,16 @@ export class ClaudeProtocol {
646
734
  if (messageId === null || messageId === this.cacheMessageId || !isRecord(usage)) {
647
735
  return;
648
736
  }
737
+
649
738
  this.cacheMessageId = messageId;
650
739
  const written = cacheTtlWritten(usage);
740
+
651
741
  if (written !== null) {
652
742
  this.cacheTtlMs = written;
653
743
  } else if (num(usage.cache_read_input_tokens) === 0) {
654
744
  return;
655
745
  }
746
+
656
747
  if (this.cacheTtlMs !== null) {
657
748
  events.push({ type: 'usage', cacheTtlMs: this.cacheTtlMs, cacheExpiresAt: this.now() + this.cacheTtlMs });
658
749
  }
@@ -664,25 +755,32 @@ export class ClaudeProtocol {
664
755
  const parentRef = str(frame.parent_tool_use_id);
665
756
  const content = Array.isArray(message.content) ? message.content : [];
666
757
  const uuid = str(frame.uuid);
758
+
667
759
  if (!parentRef && uuid) {
668
760
  this.lastUuid = uuid;
669
761
  }
762
+
670
763
  if (!parentRef && typeof frame.error === 'string') {
671
764
  this.apiError = frame.error;
672
765
  }
766
+
673
767
  const model = str(message.model);
768
+
674
769
  if (!parentRef) {
675
770
  this.trackGoal(model, content, events);
676
771
  }
772
+
677
773
  // The CLI answers some frames itself and names them `<synthetic>`; no model wrote those.
678
774
  if (parentRef && model && model !== '<synthetic>' && this.subagentModels.get(parentRef) !== model) {
679
775
  this.subagentModels.set(parentRef, model);
680
776
  events.push({ type: 'task.model', ref: parentRef, model });
681
777
  }
778
+
682
779
  for (const block of content) {
683
780
  if (!isRecord(block)) {
684
781
  continue;
685
782
  }
783
+
686
784
  if (block.type === 'text') {
687
785
  const ordinal = nextOrdinal(this.frameTextCount, messageId);
688
786
  // A subagent's text is its own report, not the thread's answer; the parent says whose row it is.
@@ -700,14 +798,19 @@ export class ClaudeProtocol {
700
798
  });
701
799
  }
702
800
  }
801
+
703
802
  const tokens = parentRef ? 0 : contextTokens(message.usage);
803
+
704
804
  if (tokens > 0) {
705
805
  events.push({ type: 'usage', contextTokens: tokens });
706
806
  }
807
+
707
808
  if (!parentRef) {
708
809
  this.noteCache(str(message.id), message.usage, events);
709
810
  }
811
+
710
812
  const error = str(frame.error);
813
+
711
814
  if (error) {
712
815
  events.push({ type: 'note', level: 'error', text: `The request failed: ${error.replaceAll('_', ' ')}` });
713
816
  }
@@ -716,15 +819,20 @@ export class ClaudeProtocol {
716
819
  private trackGoal(model: string | null, content: unknown[], events: BackendEvent[]): void {
717
820
  if (model === '<synthetic>') {
718
821
  const next = claudeGoalFromCommand(resultText(content), this.goal);
822
+
719
823
  if (next !== undefined) {
720
824
  this.setGoal(next, events);
721
825
  }
826
+
722
827
  return;
723
828
  }
829
+
724
830
  if (this.goal === null) {
725
831
  return;
726
832
  }
833
+
727
834
  this.goalAnswered = true;
835
+
728
836
  // A message after a stop takes the goal up again; its hook never went away.
729
837
  if (this.goal.status === 'paused') {
730
838
  this.goal = { ...this.goal, status: 'active' };
@@ -742,9 +850,11 @@ export class ClaudeProtocol {
742
850
  const goal = this.goal;
743
851
  const interrupted = this.interrupting;
744
852
  this.interrupting = false;
853
+
745
854
  if (goal?.status !== 'active') {
746
855
  return;
747
856
  }
857
+
748
858
  if (failed || interrupted) {
749
859
  this.setGoal({ ...goal, status: 'paused' }, events);
750
860
  } else if (this.goalAnswered && this.backgroundTasks.size === 0 && this.workflows.size === 0 && this.backgroundAgents.size === 0) {
@@ -760,9 +870,11 @@ export class ClaudeProtocol {
760
870
  private handleToolProgress(frame: Frame, events: BackendEvent[]): void {
761
871
  const elapsed = frame.elapsed_time_seconds;
762
872
  const ref = str(frame.tool_use_id);
873
+
763
874
  if (!ref || typeof elapsed !== 'number' || !Number.isFinite(elapsed) || elapsed < 0) {
764
875
  return;
765
876
  }
877
+
766
878
  events.push({ type: 'tool.progress', ref, startedAt: Date.now() - Math.round(elapsed * 1000), description: null });
767
879
  }
768
880
 
@@ -770,28 +882,36 @@ export class ClaudeProtocol {
770
882
  if (this.promptId !== null && frame.uuid === this.promptId) {
771
883
  events.push({ type: 'turn.accepted', promptId: this.promptId });
772
884
  }
885
+
773
886
  const message = isRecord(frame.message) ? frame.message : {};
774
887
  const content = Array.isArray(message.content) ? message.content : [];
888
+
775
889
  if (frame.isSynthetic === true && !str(frame.parent_tool_use_id)) {
776
890
  const checked = claudeGoalCheck(resultText(message.content), this.goal);
891
+
777
892
  if (checked !== undefined) {
778
893
  this.setGoal(checked, events);
779
894
  events.push({ type: 'note', level: 'info', text: `Goal not met yet: ${checked.lastCheck ?? checked.objective}` });
780
895
  }
781
896
  }
897
+
782
898
  for (const block of content) {
783
899
  if (!isRecord(block) || block.type !== 'tool_result') {
784
900
  continue;
785
901
  }
902
+
786
903
  const ref = str(block.tool_use_id);
904
+
787
905
  if (ref) {
788
906
  this.subagentModels.delete(ref);
789
907
  const output = resultText(block.content);
790
908
  const workflow = WORKFLOW_LAUNCHED.exec(output)?.[1];
909
+
791
910
  if (workflow) {
792
911
  this.workflows.set(workflow, ref);
793
912
  events.push({ type: 'workflow.progress', ref, workflow: { name: null, taskId: workflow, phases: [], agents: [] } });
794
913
  }
914
+
795
915
  events.push({ type: 'tool.done', ref, output, state: block.is_error === true ? 'error' : 'done' });
796
916
  }
797
917
  }
@@ -800,6 +920,7 @@ export class ClaudeProtocol {
800
920
  private handleResult(frame: Frame, events: BackendEvent[]): void {
801
921
  const echoed = Array.isArray(frame.user_message_uuids) ? frame.user_message_uuids.filter((value): value is string => typeof value === 'string') : [];
802
922
  const origin = str(frame.origin) ?? (isRecord(frame.origin) ? str(frame.origin.kind) : null);
923
+
803
924
  if (
804
925
  this.promptId !== null &&
805
926
  (echoed.length > 0
@@ -809,29 +930,37 @@ export class ClaudeProtocol {
809
930
  if (typeof frame.total_cost_usd === 'number') {
810
931
  events.push({ type: 'usage', costUsd: num(frame.total_cost_usd) });
811
932
  }
933
+
812
934
  if (frame.is_error === true) {
813
935
  events.push({ type: 'note', level: 'warning', text: 'Claude could not finish a turn in the background. Your message is still waiting.' });
814
936
  }
937
+
815
938
  return;
816
939
  }
940
+
817
941
  const promptId = this.promptId !== null && echoed.includes(this.promptId) ? this.promptId : null;
818
942
  this.promptId = null;
943
+
819
944
  for (const id of echoed) {
820
945
  this.steerIds.delete(id);
821
946
  }
947
+
822
948
  const failed = frame.is_error === true || (typeof frame.subtype === 'string' && frame.subtype.startsWith('error'));
823
949
  const errors = Array.isArray(frame.errors) ? frame.errors.filter((error): error is string => typeof error === 'string') : [];
824
950
  const lastUuid = this.lastUuid;
825
951
  this.lastUuid = null;
826
952
  const limit = failed ? this.limit() : null;
827
953
  const replaceProcess = failed && this.apiError === 'authentication_failed';
954
+
828
955
  if (limit?.kind !== 'usage') {
829
956
  this.refusedResets.clear();
830
957
  }
958
+
831
959
  this.apiError = null;
832
960
  this.refusedWindows.clear();
833
961
  // Before the turn ends, so whoever settles work on that end already reads where the goal stands.
834
962
  this.settleGoal(failed, events);
963
+
835
964
  events.push({
836
965
  type: 'turn.done',
837
966
  ...(promptId === null ? {} : { promptId }),
@@ -848,16 +977,20 @@ export class ClaudeProtocol {
848
977
  if (!isRecord(info) || typeof info.status !== 'string') {
849
978
  return;
850
979
  }
980
+
851
981
  this.expireRefusedResets();
852
982
  const namedWindow = str(info.rateLimitType);
853
983
  const window = namedWindow ?? 'unknown';
984
+
854
985
  const overageAllowed =
855
986
  info.overageStatus === 'allowed' || info.overageStatus === 'allowed_warning' || info.isUsingOverage === true || info.overageInUse === true;
987
+
856
988
  if (info.status === 'rejected' && !overageAllowed) {
857
989
  const resetsAt = typeof info.resetsAt === 'number' ? info.resetsAt * 1000 : NaN;
858
990
  const validReset = Number.isFinite(resetsAt) && resetsAt > this.now() && resetsAt < 8.64e15 ? resetsAt : null;
859
991
  const reset = validReset ?? (info.resetsAt === undefined && namedWindow ? (this.refusedResets.get(window) ?? null) : null);
860
992
  this.refusedWindows.set(window, reset);
993
+
861
994
  if (namedWindow && reset !== null && reset > this.now()) {
862
995
  this.refusedResets.set(window, reset);
863
996
  } else {
@@ -871,6 +1004,7 @@ export class ClaudeProtocol {
871
1004
 
872
1005
  private expireRefusedResets(): void {
873
1006
  const now = this.now();
1007
+
874
1008
  for (const [window, reset] of this.refusedResets) {
875
1009
  if (reset <= now) {
876
1010
  this.refusedResets.delete(window);
@@ -886,40 +1020,53 @@ export class ClaudeProtocol {
886
1020
  private limit(): ChatTurnLimit | null {
887
1021
  if (this.refusedWindows.size > 0) {
888
1022
  let resetsAt = 0;
1023
+
889
1024
  for (const reset of this.refusedWindows.values()) {
890
1025
  if (reset === null || reset <= this.now()) {
891
1026
  return { kind: 'usage' };
892
1027
  }
1028
+
893
1029
  resetsAt = Math.max(resetsAt, reset);
894
1030
  }
1031
+
895
1032
  return { kind: 'usage', resetsAt };
896
1033
  }
1034
+
897
1035
  if (this.apiError === 'rate_limit' || this.apiError === 'overloaded') {
898
1036
  return { kind: 'overload' };
899
1037
  }
1038
+
900
1039
  return null;
901
1040
  }
902
1041
 
903
1042
  private handleControlRequest(frame: Frame, events: BackendEvent[]): void {
904
1043
  const request = isRecord(frame.request) ? frame.request : {};
905
1044
  const requestId = str(frame.request_id);
1045
+
906
1046
  if (!requestId || request.subtype !== 'can_use_tool') {
907
1047
  return;
908
1048
  }
1049
+
909
1050
  const toolUseId = str(request.tool_use_id);
910
1051
  const toolName = str(request.tool_name) ?? 'tool';
911
1052
  const background = Boolean(str(request.agent_id));
1053
+
912
1054
  if (toolName === 'AskUserQuestion') {
913
1055
  const questions = parseQuestions(request.input);
1056
+
914
1057
  if (questions.length === 0) {
915
1058
  return;
916
1059
  }
1060
+
917
1061
  this.pending.set(requestId, { type: 'question', toolUseId, input: request.input });
918
1062
  events.push({ type: 'question.requested', requestId, questions, ...(background ? { background: true } : {}) });
1063
+
919
1064
  return;
920
1065
  }
1066
+
921
1067
  const suggestions = Array.isArray(request.permission_suggestions) ? request.permission_suggestions : [];
922
1068
  this.pending.set(requestId, { type: 'approval', toolUseId, input: request.input ?? {}, suggestions });
1069
+
923
1070
  events.push({
924
1071
  type: 'approval.requested',
925
1072
  ...(background ? { background: true } : {}),