@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
@@ -11,6 +11,7 @@ import type {
11
11
  import { isRecord } from '@adecore/agent-contracts';
12
12
  import { clipText } from '@adecore/agent-contracts/text';
13
13
  import type { BackendEvent } from './backend.ts';
14
+ import { str } from '../fields.ts';
14
15
  import { estimateContextBreakdown } from './context-breakdown.ts';
15
16
  import type { ChatThread } from './thread.ts';
16
17
 
@@ -41,14 +42,11 @@ function startsAgentTurn(event: BackendEvent): boolean {
41
42
  return isMainAgentOutput(event) || ((event.type === 'approval.requested' || event.type === 'question.requested') && event.background !== true);
42
43
  }
43
44
 
44
- function str(value: unknown): string | null {
45
- return typeof value === 'string' ? value : null;
46
- }
47
-
48
45
  /* A markdown code block around text that may hold backticks of its own. */
49
46
  function fenced(text: string): string {
50
47
  const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
51
48
  const fence = '`'.repeat(Math.max(3, longest + 1));
49
+
52
50
  return `${fence}\n${text}\n${fence}`;
53
51
  }
54
52
 
@@ -81,16 +79,20 @@ const MAX_SUMMARY_CHARS = 80;
81
79
  export function summaryLine(summary: string): string {
82
80
  const first = summary.split('\n').find((line) => line.trim() !== '') ?? '';
83
81
  const text = first.replace(/\s+/g, ' ').trim();
82
+
84
83
  if (text.length <= MAX_SUMMARY_CHARS) {
85
84
  return text;
86
85
  }
86
+
87
87
  const cut = clipText(text, MAX_SUMMARY_CHARS);
88
88
  const space = cut.lastIndexOf(' ');
89
+
89
90
  return `${(space > MAX_SUMMARY_CHARS / 2 ? cut.slice(0, space) : cut).trimEnd()}...`;
90
91
  }
91
92
 
92
93
  function usageField(text: string, name: string): number {
93
94
  const match = new RegExp(`${name}:\\s*(\\d+)`).exec(text);
95
+
94
96
  return match ? Number(match[1]) : 0;
95
97
  }
96
98
 
@@ -99,9 +101,11 @@ export function stripAgentFooter(output: string): { text: string | null; usage:
99
101
  const match = AGENT_FOOTER.exec(output);
100
102
  const text = (match ? output.slice(0, match.index) : output).trim();
101
103
  const block = match?.[1];
104
+
102
105
  const usage = block
103
106
  ? { totalTokens: usageField(block, 'subagent_tokens'), toolUses: usageField(block, 'tool_uses'), durationMs: usageField(block, 'duration_ms') }
104
107
  : null;
108
+
105
109
  return { text: text === '' || text === NO_OUTPUT ? null : text, usage };
106
110
  }
107
111
 
@@ -155,12 +159,15 @@ export class ThreadProjector {
155
159
 
156
160
  project(generation: number, event: BackendEvent): ChatEvent[] {
157
161
  const events: ChatEvent[] = [];
162
+
158
163
  // The CLI talks outside a turn when it wakes the agent itself: a background task it launched
159
164
  // earlier settled. The work needs a turn of its own, or it lands in the thread unattached.
160
165
  if (this.thread.info.activeTurnId === null && startsAgentTurn(event) && !this.restatesCall(generation, event)) {
161
166
  this.openAgentTurn(events);
162
167
  }
168
+
163
169
  const info = this.thread.info;
170
+
164
171
  switch (event.type) {
165
172
  case 'session':
166
173
  events.push(
@@ -174,30 +181,38 @@ export class ThreadProjector {
174
181
  running: true
175
182
  })
176
183
  );
184
+
177
185
  break;
178
186
  case 'thinking.delta':
179
187
  this.appendThinking(generation, event.ref, event.text, false, events);
188
+
180
189
  break;
181
190
  case 'permissions':
182
191
  events.push(this.thread.patchInfo({ permissionMode: event.permissionMode, effectiveRuntimeMode: event.effectiveRuntimeMode }));
192
+
183
193
  break;
184
194
  case 'thinking.done':
185
195
  this.appendThinking(generation, event.ref, event.text, true, events);
196
+
186
197
  break;
187
198
  case 'text.delta':
188
199
  this.closeThinking(events);
189
200
  this.appendText(this.itemId(generation, event.ref), event.text, events, event.phase);
201
+
190
202
  break;
191
203
  case 'text.done': {
192
204
  if (event.parentRef) {
193
205
  if (!this.insideNested(generation, event.parentRef)) {
194
206
  this.appendSubagentText(generation, event.parentRef, event.ref, event.text, events);
195
207
  }
208
+
196
209
  break;
197
210
  }
211
+
198
212
  this.closeThinking(events);
199
213
  const id = this.itemId(generation, event.ref);
200
214
  const existing = this.thread.get(id);
215
+
201
216
  events.push(
202
217
  this.thread.upsert({
203
218
  id,
@@ -214,6 +229,7 @@ export class ThreadProjector {
214
229
  streaming: false
215
230
  })
216
231
  );
232
+
217
233
  break;
218
234
  }
219
235
  case 'tool.started':
@@ -221,23 +237,31 @@ export class ThreadProjector {
221
237
  if (event.parentRef === null) {
222
238
  this.closeThinking(events);
223
239
  }
240
+
224
241
  this.startSubagent(generation, event, events);
242
+
225
243
  break;
226
244
  }
245
+
227
246
  if (event.parentRef === null) {
228
247
  this.closeThinking(events);
229
248
  } else if (this.insideNested(generation, event.parentRef)) {
230
249
  break;
231
250
  }
251
+
232
252
  this.startTool(generation, event, events);
253
+
233
254
  break;
234
255
  case 'tool.progress':
235
256
  this.patchProgress(generation, event, events);
257
+
236
258
  break;
237
259
  case 'workflow.progress': {
238
260
  const tool = this.thread.get(this.itemId(generation, event.ref));
261
+
239
262
  if (tool?.kind === 'tool') {
240
263
  const name = event.workflow.name ?? tool.workflow?.name ?? null;
264
+
241
265
  events.push(
242
266
  this.thread.upsert({
243
267
  ...tool,
@@ -245,32 +269,42 @@ export class ThreadProjector {
245
269
  })
246
270
  );
247
271
  }
272
+
248
273
  break;
249
274
  }
250
275
  case 'tool.output': {
251
276
  const delta = this.thread.appendText(this.itemId(generation, event.ref), event.text);
277
+
252
278
  if (delta) {
253
279
  events.push(delta);
254
280
  }
281
+
255
282
  break;
256
283
  }
257
284
  case 'tool.done': {
258
285
  const target = this.thread.get(this.itemId(generation, event.ref));
286
+
259
287
  if (target?.kind === 'subagent') {
260
288
  this.settleSubagent(target, event, events);
289
+
261
290
  break;
262
291
  }
292
+
263
293
  if (target?.kind === 'tool' && target.name === 'Workflow' && event.state === 'done' && event.output?.startsWith(WORKFLOW_LAUNCHED)) {
264
294
  // The call only launched the workflow; the row runs on past the turn until the workflow's task ends.
265
295
  this.backgroundCalls.add(target.id);
266
296
  events.push(this.thread.upsert({ ...target, output: event.output }));
297
+
267
298
  break;
268
299
  }
300
+
269
301
  this.settleTool(generation, event, events);
302
+
270
303
  break;
271
304
  }
272
305
  case 'approval.requested':
273
306
  this.closeThinking(events);
307
+
274
308
  events.push(
275
309
  this.thread.upsert({
276
310
  id: `approval-${event.requestId}`,
@@ -287,10 +321,13 @@ export class ThreadProjector {
287
321
  decision: 'pending'
288
322
  })
289
323
  );
324
+
290
325
  events.push(this.thread.setStatus('needs-you'));
326
+
291
327
  break;
292
328
  case 'question.requested':
293
329
  this.closeThinking(events);
330
+
294
331
  events.push(
295
332
  this.thread.upsert({
296
333
  id: `question-${event.requestId}`,
@@ -304,55 +341,72 @@ export class ThreadProjector {
304
341
  state: 'pending'
305
342
  })
306
343
  );
344
+
307
345
  events.push(this.thread.setStatus('needs-you'));
346
+
308
347
  break;
309
348
  case 'request.withdrawn':
310
349
  this.withdraw(event.requestId, events);
350
+
311
351
  break;
312
352
  case 'task.started':
313
353
  this.patchSubagentStart(generation, event, events);
354
+
314
355
  break;
315
356
  case 'task.progress':
316
357
  this.patchSubagentProgress(generation, event, events);
358
+
317
359
  break;
318
360
  case 'task.model':
319
361
  this.patchSubagentModel(generation, event, events);
362
+
320
363
  break;
321
364
  case 'task.done': {
322
365
  if (event.ref !== null) {
323
366
  this.pendingMetadata.delete(this.itemId(generation, event.ref));
324
367
  }
368
+
325
369
  // Keep one summary line for the next turn's header; the tool row already has the result. A nested
326
370
  // agent reports to the agent that opened it, so it only names that turn when no row of the thread's own does.
327
371
  const row = this.taskSubagent(generation, event.ref, event.taskId);
328
372
  const nested = row?.parentToolUseId !== undefined || (event.ref !== null && this.unplacedCalls.has(event.ref));
373
+
329
374
  if (event.ref !== null) {
330
375
  this.unplacedCalls.delete(event.ref);
331
376
  }
377
+
332
378
  const line = event.summary === null ? '' : summaryLine(event.summary);
379
+
333
380
  if (line !== '' && (!nested || this.taskSummary === null)) {
334
381
  this.taskSummary = line;
335
382
  }
383
+
336
384
  if (row && (!nested || this.taskToolUseId === null)) {
337
385
  this.taskToolUseId = row.toolUseId;
338
386
  }
387
+
339
388
  this.settleBackgroundSubagent(row, event, events);
340
389
  this.settleLaunchedCall(generation, event, events);
390
+
341
391
  break;
342
392
  }
343
393
  case 'background.started':
344
394
  this.startBackground(generation, event, events);
395
+
345
396
  break;
346
397
  case 'background.ended': {
347
398
  const background = info.background ?? [];
399
+
348
400
  if (background.some((task) => task.id === event.taskId)) {
349
401
  events.push(this.thread.patchInfo({ background: background.filter((task) => task.id !== event.taskId) }));
350
402
  }
403
+
351
404
  break;
352
405
  }
353
406
  case 'usage': {
354
407
  const usage = info.usage;
355
408
  const contextTokens = event.contextTokens ?? usage.contextTokens;
409
+
356
410
  const next = {
357
411
  ...usage,
358
412
  contextTokens,
@@ -366,6 +420,7 @@ export class ThreadProjector {
366
420
  ? {}
367
421
  : { cacheTtlMs: event.cacheTtlMs, cacheExpiresAt: event.cacheExpiresAt })
368
422
  };
423
+
369
424
  if (
370
425
  next.contextTokens !== usage.contextTokens ||
371
426
  next.contextWindow !== usage.contextWindow ||
@@ -375,6 +430,7 @@ export class ThreadProjector {
375
430
  ) {
376
431
  events.push(this.thread.patchInfo({ usage: next }));
377
432
  }
433
+
378
434
  break;
379
435
  }
380
436
  case 'compaction':
@@ -388,35 +444,44 @@ export class ThreadProjector {
388
444
  preTokens: event.preTokens ?? (info.usage.contextTokens > 0 ? info.usage.contextTokens : null)
389
445
  })
390
446
  );
447
+
391
448
  break;
392
449
  case 'model':
393
450
  events.push(this.thread.patchInfo({ model: event.model }));
451
+
394
452
  break;
395
453
  case 'goal': {
396
454
  const goal = event.goal ?? undefined;
455
+
397
456
  if (JSON.stringify(goal) !== JSON.stringify(info.goal)) {
398
457
  events.push(this.thread.patchInfo({ goal }));
399
458
  }
459
+
400
460
  break;
401
461
  }
402
462
  case 'note':
403
463
  events.push(this.note(event.level, event.text));
464
+
404
465
  break;
405
466
  case 'turn.done':
406
467
  this.finishTurn(event.state, event.costUsd, event.error, events, event.native, event.limit);
468
+
407
469
  break;
408
470
  case 'failed':
409
471
  events.push(this.note('error', event.message));
410
472
  this.settleOpenItems(events, !event.processAlive, true);
411
473
  this.closeTurn('error', 0, events);
412
474
  events.push(this.thread.patchInfo({ status: 'error', activeTurnId: null }));
475
+
413
476
  break;
414
477
  case 'exit':
415
478
  this.pendingMetadata.clear();
416
479
  events.push(this.thread.patchInfo({ effectiveRuntimeMode: undefined, permissionMode: undefined }));
417
480
  this.finishProcess(event.exitCode, event.stderr ?? null, events);
481
+
418
482
  break;
419
483
  }
484
+
420
485
  return events;
421
486
  }
422
487
 
@@ -428,6 +493,7 @@ export class ThreadProjector {
428
493
  const taskToolUseId = this.taskToolUseId;
429
494
  this.taskSummary = null;
430
495
  this.taskToolUseId = null;
496
+
431
497
  events.push(
432
498
  this.thread.upsert({
433
499
  id: turnId,
@@ -442,6 +508,7 @@ export class ThreadProjector {
442
508
  costUsd: 0
443
509
  })
444
510
  );
511
+
445
512
  events.push(this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId }));
446
513
  }
447
514
 
@@ -453,6 +520,7 @@ export class ThreadProjector {
453
520
  */
454
521
  private settleOpenItems(events: ChatEvent[], processGone = false, dropRequests = processGone): void {
455
522
  this.thinking = null;
523
+
456
524
  for (const item of this.thread.list()) {
457
525
  if (item.kind === 'assistant' && item.streaming) {
458
526
  events.push(this.thread.upsert({ ...item, streaming: false }));
@@ -475,8 +543,10 @@ export class ThreadProjector {
475
543
  if (item.background) {
476
544
  return true;
477
545
  }
546
+
478
547
  const parentRef = item.parentToolUseId;
479
548
  const parent = parentRef === undefined ? undefined : this.thread.find('subagent', (candidate) => candidate.toolUseId === parentRef);
549
+
480
550
  return parent?.status === 'running' && this.outlivesTurn(parent);
481
551
  }
482
552
 
@@ -503,24 +573,31 @@ export class ThreadProjector {
503
573
  private appendThinking(generation: number, ref: string, text: string, done: boolean, events: ChatEvent[]): void {
504
574
  const key = this.itemId(generation, ref);
505
575
  const open = this.thinking;
576
+
506
577
  if (open?.refs.has(key) && done) {
507
578
  return;
508
579
  }
580
+
509
581
  if (!open && text.trim() === '') {
510
582
  return;
511
583
  }
584
+
512
585
  if (!open) {
513
586
  const id = `${key}:think`;
514
587
  this.thinking = { id, refs: new Set([key]), last: key };
588
+
515
589
  events.push(
516
590
  this.thread.upsert({ id, kind: 'thinking', createdAt: this.now(), turnId: this.thread.info.activeTurnId, text, streaming: true, endedAt: null })
517
591
  );
592
+
518
593
  return;
519
594
  }
595
+
520
596
  const separator = open.last !== null && open.last !== key ? '\n\n' : '';
521
597
  open.refs.add(key);
522
598
  open.last = key;
523
599
  const delta = this.thread.appendText(open.id, `${separator}${text}`);
600
+
524
601
  if (delta) {
525
602
  events.push(delta);
526
603
  }
@@ -531,6 +608,7 @@ export class ThreadProjector {
531
608
  const open = this.thinking;
532
609
  this.thinking = null;
533
610
  const item = open ? this.thread.get(open.id) : undefined;
611
+
534
612
  if (item?.kind === 'thinking' && item.streaming) {
535
613
  events.push(this.thread.upsert({ ...item, streaming: false, endedAt: this.now() }));
536
614
  }
@@ -538,6 +616,7 @@ export class ThreadProjector {
538
616
 
539
617
  private appendText(id: string, text: string, events: ChatEvent[], phase?: string): void {
540
618
  const existing = this.thread.get(id);
619
+
541
620
  if (!existing) {
542
621
  events.push(
543
622
  this.thread.upsert({
@@ -550,9 +629,12 @@ export class ThreadProjector {
550
629
  streaming: true
551
630
  })
552
631
  );
632
+
553
633
  return;
554
634
  }
635
+
555
636
  const delta = text === '' ? null : this.thread.appendText(id, text);
637
+
556
638
  if (delta) {
557
639
  events.push(delta);
558
640
  }
@@ -563,9 +645,11 @@ export class ThreadProjector {
563
645
  const existing = this.thread.get(id);
564
646
  const previous = existing?.kind === 'tool' ? existing : undefined;
565
647
  const parent = event.parentRef === null ? null : this.subagent(generation, event.parentRef);
648
+
566
649
  if (parent && !existing && !this.takeChildSlot(parent, 0, events)) {
567
650
  return;
568
651
  }
652
+
569
653
  const item: ChatToolItem = {
570
654
  id,
571
655
  kind: 'tool',
@@ -578,54 +662,69 @@ export class ThreadProjector {
578
662
  state: previous?.state ?? 'running',
579
663
  parentToolUseId: event.parentRef
580
664
  };
665
+
581
666
  if (previous?.progress) {
582
667
  item.progress = previous.progress;
583
668
  }
669
+
584
670
  const changes = event.changes ?? previous?.changes;
671
+
585
672
  if (changes) {
586
673
  item.changes = changes;
587
674
  }
675
+
588
676
  events.push(this.thread.upsert(item));
589
677
  }
590
678
 
591
679
  private settleTool(generation: number, event: Extract<BackendEvent, { type: 'tool.done' }>, events: ChatEvent[]): void {
592
680
  const tool = this.thread.get(this.itemId(generation, event.ref));
681
+
593
682
  if (tool?.kind !== 'tool') {
594
683
  return;
595
684
  }
685
+
596
686
  this.backgroundCalls.delete(tool.id);
687
+
597
688
  if (tool.parentToolUseId !== null) {
598
689
  const parent = this.subagent(generation, tool.parentToolUseId);
690
+
599
691
  if (parent) {
600
692
  this.budgetFor(parent).textBytes += (event.output ?? '').length;
601
693
  }
602
694
  }
695
+
603
696
  // The result supersedes whatever progress said; a settled call carries none.
604
697
  const settled: ChatToolItem = { ...tool, output: event.output ?? tool.output, state: event.state };
605
698
  delete settled.progress;
699
+
606
700
  if (event.changes) {
607
701
  settled.changes = event.changes;
608
702
  }
703
+
609
704
  events.push(this.thread.upsert(settled));
610
705
  }
611
706
 
612
707
  private patchProgress(generation: number, event: Extract<BackendEvent, { type: 'tool.progress' }>, events: ChatEvent[]): void {
613
708
  const tool = this.thread.get(this.itemId(generation, event.ref));
709
+
614
710
  if (tool?.kind !== 'tool' || tool.state !== 'running') {
615
711
  return;
616
712
  }
713
+
617
714
  // A null field means the CLI said nothing about it this time, so what it said before stands.
618
715
  const progress: ChatToolProgress = {
619
716
  startedAt: event.startedAt ?? tool.progress?.startedAt ?? null,
620
717
  description: event.description ?? tool.progress?.description ?? null,
621
718
  output: tool.progress?.output ?? null
622
719
  };
720
+
623
721
  events.push(this.thread.upsert({ ...tool, progress }));
624
722
  }
625
723
 
626
724
  private subagent(generation: number, ref: string): ChatSubagentItem | null {
627
725
  const id = this.itemId(generation, ref);
628
726
  const item = this.thread.get(id) ?? this.thread.get(this.wokenRows.get(id) ?? '');
727
+
629
728
  return item?.kind === 'subagent' ? item : null;
630
729
  }
631
730
 
@@ -635,9 +734,11 @@ export class ThreadProjector {
635
734
  */
636
735
  private taskSubagent(generation: number, ref: string | null, taskId: string | null | undefined): ChatSubagentItem | null {
637
736
  const byRef = ref === null ? null : this.subagent(generation, ref);
737
+
638
738
  if (byRef || !taskId) {
639
739
  return byRef;
640
740
  }
741
+
641
742
  return this.thread.list().findLast((item): item is ChatSubagentItem => item.kind === 'subagent' && item.native?.agentId === taskId) ?? null;
642
743
  }
643
744
 
@@ -652,6 +753,7 @@ export class ThreadProjector {
652
753
  private budgetFor(parent: ChatSubagentItem): { items: number; textBytes: number } {
653
754
  const budget = this.budgets.get(parent.id) ?? { items: 0, textBytes: 0 };
654
755
  this.budgets.set(parent.id, budget);
756
+
655
757
  return budget;
656
758
  }
657
759
 
@@ -661,14 +763,18 @@ export class ThreadProjector {
661
763
  */
662
764
  private takeChildSlot(parent: ChatSubagentItem, textBytes: number, events: ChatEvent[]): boolean {
663
765
  const budget = this.budgetFor(parent);
766
+
664
767
  if (budget.items >= MAX_SUBAGENT_ITEMS || budget.textBytes >= MAX_SUBAGENT_TEXT) {
665
768
  if (!parent.itemsTruncated) {
666
769
  events.push(this.thread.upsert({ ...parent, itemsTruncated: true }));
667
770
  }
771
+
668
772
  return false;
669
773
  }
774
+
670
775
  budget.items += 1;
671
776
  budget.textBytes += textBytes;
777
+
672
778
  return true;
673
779
  }
674
780
 
@@ -687,6 +793,7 @@ export class ThreadProjector {
687
793
  const model = metadata?.model ?? previous?.model ?? (str(input.model) || undefined);
688
794
  this.pendingMetadata.delete(id);
689
795
  this.unplacedCalls.delete(event.ref);
796
+
690
797
  const item: ChatSubagentItem = {
691
798
  id,
692
799
  kind: 'subagent',
@@ -710,7 +817,9 @@ export class ThreadProjector {
710
817
  ...(previous?.native ? { native: previous.native } : {}),
711
818
  ...(parentToolUseId === undefined ? {} : { parentToolUseId })
712
819
  };
820
+
713
821
  events.push(this.thread.upsert(item));
822
+
714
823
  if (metadata?.start) {
715
824
  this.patchSubagentStart(generation, metadata.start, events);
716
825
  }
@@ -719,25 +828,33 @@ export class ThreadProjector {
719
828
  /* What the CLI itself says about the delegation: which agent it is, and whether it blocks the turn. */
720
829
  private patchSubagentStart(generation: number, event: Extract<BackendEvent, { type: 'task.started' }>, events: ChatEvent[]): void {
721
830
  const existing = this.thread.get(this.itemId(generation, event.ref));
831
+
722
832
  // An agent a subagent opened, whose call the stream never showed, has no row to hang under.
723
833
  if (!existing && (event.depth ?? 1) > 1) {
724
834
  this.unplacedCalls.add(event.ref);
725
835
  this.deferMetadata(generation, event.ref, { start: event });
836
+
726
837
  return;
727
838
  }
839
+
728
840
  if (!existing) {
729
841
  this.startSubagent(generation, { type: 'tool.started', ref: event.ref, name: 'Agent', input: {}, parentRef: null }, events);
730
842
  }
843
+
731
844
  const item = this.taskSubagent(generation, event.ref, event.taskId);
845
+
732
846
  if (!item) {
733
847
  return;
734
848
  }
849
+
735
850
  // A settled agent that starts again under another call was woken by a message, and works on in the background.
736
851
  const resumed = event.resumed === true || (item.status !== 'running' && item.toolUseId !== event.ref);
737
852
  const ownCall = this.itemId(generation, item.toolUseId);
853
+
738
854
  if (resumed && item.id !== ownCall) {
739
855
  this.wokenRows.set(ownCall, item.id);
740
856
  }
857
+
741
858
  events.push(
742
859
  this.thread.upsert({
743
860
  ...item,
@@ -762,6 +879,7 @@ export class ThreadProjector {
762
879
  private deferMetadata(generation: number, ref: string, metadata: { model?: string; start?: Extract<BackendEvent, { type: 'task.started' }> }): void {
763
880
  const id = this.itemId(generation, ref);
764
881
  this.pendingMetadata.set(id, { ...this.pendingMetadata.get(id), ...metadata });
882
+
765
883
  // Only unplaced snapshots are bounded; established child identities stay with their rows.
766
884
  if (this.pendingMetadata.size > 512) {
767
885
  this.pendingMetadata.delete(this.pendingMetadata.keys().next().value!);
@@ -770,10 +888,13 @@ export class ThreadProjector {
770
888
 
771
889
  private patchSubagentModel(generation: number, event: Extract<BackendEvent, { type: 'task.model' }>, events: ChatEvent[]): void {
772
890
  const item = this.subagent(generation, event.ref);
891
+
773
892
  if (!item) {
774
893
  this.deferMetadata(generation, event.ref, { model: event.model });
894
+
775
895
  return;
776
896
  }
897
+
777
898
  if (item && item.model !== event.model) {
778
899
  events.push(this.thread.upsert({ ...item, model: event.model }));
779
900
  }
@@ -781,9 +902,11 @@ export class ThreadProjector {
781
902
 
782
903
  private patchSubagentProgress(generation: number, event: Extract<BackendEvent, { type: 'task.progress' }>, events: ChatEvent[]): void {
783
904
  const item = this.taskSubagent(generation, event.ref, event.taskId);
905
+
784
906
  if (!item || item.status !== 'running') {
785
907
  return;
786
908
  }
909
+
787
910
  events.push(
788
911
  this.thread.upsert({
789
912
  ...item,
@@ -797,14 +920,18 @@ export class ThreadProjector {
797
920
  /* Text a subagent wrote: part of its row, and the candidate for the report it ends with. */
798
921
  private appendSubagentText(generation: number, parentRef: string, ref: string, text: string, events: ChatEvent[]): void {
799
922
  const parent = this.subagent(generation, parentRef);
923
+
800
924
  if (!parent) {
801
925
  return;
802
926
  }
927
+
803
928
  const id = this.itemId(generation, ref);
804
929
  const existing = this.thread.get(id);
930
+
805
931
  if (!existing && !this.takeChildSlot(parent, text.length, events)) {
806
932
  return;
807
933
  }
934
+
808
935
  events.push(
809
936
  this.thread.upsert({
810
937
  id,
@@ -816,7 +943,9 @@ export class ThreadProjector {
816
943
  parentToolUseId: parentRef
817
944
  })
818
945
  );
946
+
819
947
  const open = this.subagent(generation, parentRef);
948
+
820
949
  if (open && text.trim() !== '') {
821
950
  events.push(this.thread.upsert({ ...open, result: text }));
822
951
  }
@@ -825,12 +954,16 @@ export class ThreadProjector {
825
954
  /* The Agent call answered: a foreground agent is done, a background one has only been launched. */
826
955
  private settleSubagent(item: ChatSubagentItem, event: Extract<BackendEvent, { type: 'tool.done' }>, events: ChatEvent[]): void {
827
956
  const output = event.output ?? '';
957
+
828
958
  if (output.startsWith(LAUNCH_PLACEHOLDER)) {
829
959
  const outputFile = /output_file:\s*(\S+)/.exec(output)?.[1];
830
960
  events.push(this.thread.upsert({ ...item, background: true, ...(outputFile ? { outputFile } : {}) }));
961
+
831
962
  return;
832
963
  }
964
+
833
965
  const report = stripAgentFooter(output);
966
+
834
967
  events.push(
835
968
  this.thread.upsert({
836
969
  ...item,
@@ -847,16 +980,20 @@ export class ThreadProjector {
847
980
  if (!item) {
848
981
  return;
849
982
  }
983
+
850
984
  const full = event.summary?.trim() ?? '';
851
985
  const line = full === '' ? null : summaryLine(full);
852
986
  // The row says in one line what the agent did. When the CLI put its whole report in the
853
987
  // summary, that report is the best one this row will ever get: it goes behind the fold.
854
988
  const result = item.result ?? (line !== null && full !== line ? full : null);
989
+
855
990
  // A resumed agent notifies more than once; one that already settled only learns what came of it.
856
991
  if (item.status !== 'running') {
857
992
  events.push(this.thread.upsert({ ...item, summary: line ?? item.summary, result }));
993
+
858
994
  return;
859
995
  }
996
+
860
997
  events.push(
861
998
  this.thread.upsert({
862
999
  ...item,
@@ -873,9 +1010,11 @@ export class ThreadProjector {
873
1010
  /* A call whose work ran on past its own answer, such as a workflow, settled by the task it ran as. */
874
1011
  private settleLaunchedCall(generation: number, event: Extract<BackendEvent, { type: 'task.done' }>, events: ChatEvent[]): void {
875
1012
  const call = event.ref === null ? undefined : this.thread.get(this.itemId(generation, event.ref));
1013
+
876
1014
  if (call?.kind !== 'tool' || call.state !== 'running' || !this.backgroundCalls.has(call.id)) {
877
1015
  return;
878
1016
  }
1017
+
879
1018
  this.backgroundCalls.delete(call.id);
880
1019
  const summary = event.summary?.trim() ?? '';
881
1020
  const output = summary === '' ? call.output : [call.output, summary].filter((part) => part !== null && part !== '').join('\n\n');
@@ -887,15 +1026,20 @@ export class ThreadProjector {
887
1026
  /* A shell or a monitor, told apart by the call that started it when the CLI's frame does not say. */
888
1027
  private startBackground(generation: number, event: Extract<BackendEvent, { type: 'background.started' }>, events: ChatEvent[]): void {
889
1028
  const background = this.thread.info.background ?? [];
1029
+
890
1030
  if (background.some((task) => task.id === event.taskId)) {
891
1031
  return;
892
1032
  }
1033
+
893
1034
  const call = event.ref === null ? undefined : this.thread.get(this.itemId(generation, event.ref));
894
1035
  const tool = call?.kind === 'tool' ? call : null;
1036
+
895
1037
  if (tool !== null) {
896
1038
  this.backgroundCalls.add(tool.id);
897
1039
  }
1040
+
898
1041
  const input = tool && isRecord(tool.input) ? tool.input : {};
1042
+
899
1043
  const task: ChatBackgroundTask = {
900
1044
  id: event.taskId,
901
1045
  kind: event.monitor || event.ref === null || tool?.name === 'Monitor' ? 'monitor' : 'shell',
@@ -903,16 +1047,20 @@ export class ThreadProjector {
903
1047
  command: str(input.command),
904
1048
  startedAt: this.now()
905
1049
  };
1050
+
906
1051
  events.push(this.thread.patchInfo({ background: [...background, task] }));
907
1052
  }
908
1053
 
909
1054
  private withdraw(requestId: string, events: ChatEvent[]): void {
910
1055
  const approval = this.thread.get(`approval-${requestId}`);
1056
+
911
1057
  if (approval?.kind === 'approval' && approval.decision === 'pending') {
912
1058
  events.push(this.thread.upsert({ ...approval, decision: 'cancelled' }));
913
1059
  events.push(this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId)));
914
1060
  }
1061
+
915
1062
  const question = this.thread.get(`question-${requestId}`);
1063
+
916
1064
  if (question?.kind === 'question' && question.state === 'pending') {
917
1065
  events.push(this.thread.upsert({ ...question, state: 'cancelled' }));
918
1066
  events.push(this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId)));
@@ -930,9 +1078,11 @@ export class ThreadProjector {
930
1078
  if (error) {
931
1079
  events.push(this.note('error', error));
932
1080
  }
1081
+
933
1082
  this.settleOpenItems(events, false, state === 'aborted');
934
1083
  this.closeTurn(state, costUsd, events, native, limit);
935
1084
  const usage = this.thread.info.usage;
1085
+
936
1086
  events.push(
937
1087
  this.thread.patchInfo({
938
1088
  status: this.thread.statusFor(null),
@@ -948,12 +1098,15 @@ export class ThreadProjector {
948
1098
  // Waiting on a person between turns is not a turn the process took down with it.
949
1099
  const { status, activeTurnId } = this.thread.info;
950
1100
  const busy = status === 'running' || (status === 'needs-you' && activeTurnId !== null);
1101
+
951
1102
  if (busy || (exitCode !== null && exitCode !== 0)) {
952
1103
  const reason = exitCode === null ? `${this.providerName} stopped` : `${this.providerName} exited with code ${exitCode}`;
953
1104
  // A note shows its first line and folds the rest open as markdown.
954
1105
  events.push(this.note('error', stderr === null ? reason : `${reason}\n\n${fenced(stderr)}`));
955
1106
  }
1107
+
956
1108
  this.closeTurn(busy ? 'error' : 'done', 0, events);
1109
+
957
1110
  // Whatever ran beside the turns went with the process.
958
1111
  events.push(
959
1112
  this.thread.patchInfo({
@@ -974,9 +1127,11 @@ export class ThreadProjector {
974
1127
  ): void {
975
1128
  const turnId = this.thread.info.activeTurnId;
976
1129
  const turn = turnId ? this.thread.get(turnId) : undefined;
1130
+
977
1131
  if (turn?.kind !== 'turn') {
978
1132
  return;
979
1133
  }
1134
+
980
1135
  // The CLI reports what the whole chat cost so far; the turn keeps what it added.
981
1136
  events.push(
982
1137
  this.thread.upsert({
@@ -999,6 +1154,7 @@ export class ThreadProjector {
999
1154
  level,
1000
1155
  text
1001
1156
  };
1157
+
1002
1158
  return this.thread.upsert(item);
1003
1159
  }
1004
1160
  }