@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
@@ -46,23 +46,28 @@ export interface TaskCoordinatorDeps {
46
46
  function answerOf(items: readonly ChatItem[], turnId: string): string | null {
47
47
  for (let i = items.length - 1; i >= 0; i--) {
48
48
  const item = items[i]!;
49
+
49
50
  if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId) {
50
51
  const text = readableAssistantText(item).trim();
52
+
51
53
  if (text !== '') {
52
54
  return text;
53
55
  }
54
56
  }
55
57
  }
58
+
56
59
  return null;
57
60
  }
58
61
 
59
62
  function lastNote(items: readonly ChatItem[], turnId: string, levels: readonly string[]): string | null {
60
63
  for (let i = items.length - 1; i >= 0; i--) {
61
64
  const item = items[i]!;
65
+
62
66
  if (item.kind === 'note' && item.turnId === turnId && levels.includes(item.level)) {
63
67
  return item.text;
64
68
  }
65
69
  }
70
+
66
71
  return null;
67
72
  }
68
73
 
@@ -87,23 +92,24 @@ export function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at:
87
92
  if (turn.state === 'done') {
88
93
  return { status: 'done', result: { text: answerOf(items, turn.id) ?? 'It ended its turn without an answer.', source: 'turn', at } };
89
94
  }
95
+
90
96
  if (turn.state === 'aborted') {
91
97
  const text = abortedByMachine(turn, items)
92
98
  ? (lastNote(items, turn.id, ['warning']) ?? 'The machine stopped it.')
93
99
  : 'It was stopped before it finished.';
100
+
94
101
  return { status: 'failed', result: { text, source: 'turn', at } };
95
102
  }
103
+
96
104
  return { status: 'failed', result: { text: lastNote(items, turn.id, ['error']) ?? 'Its turn ended in an error.', source: 'turn', at } };
97
105
  }
98
106
 
99
107
  /*
100
108
  * Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
101
109
  * in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
102
- * child settles when a turn ends with none of its own tasks still open or waiting to wake it, no goal
103
- * still active and no work of its CLI's still running in the background, or once the commands it runs
104
- * there outlast their limit;
105
- * an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
106
- * ends without one.
110
+ * child settles when a turn ends with no task of its own open or waiting to wake it, no active goal and
111
+ * no background work running, or once its background commands outlast their limit. An agent that is
112
+ * not a chat settles only on `done`, or fails when it ends without one.
107
113
  */
108
114
  export class TaskCoordinator {
109
115
  private readonly deps: TaskCoordinatorDeps;
@@ -123,6 +129,7 @@ export class TaskCoordinator {
123
129
  while (this.pending.size > 0) {
124
130
  await Promise.allSettled([...this.pending]);
125
131
  }
132
+
126
133
  await this.deps.tasks.settled();
127
134
  }
128
135
 
@@ -133,9 +140,11 @@ export class TaskCoordinator {
133
140
  */
134
141
  async settleFromElsewhere(taskId: string, result: TaskResult, status: 'done' | 'failed' = 'done'): Promise<Task | null> {
135
142
  const task = this.deps.tasks.get(taskId);
143
+
136
144
  if (task?.machine === undefined || task.status !== 'open') {
137
145
  return null;
138
146
  }
147
+
139
148
  return this.settleNow(task, status, result);
140
149
  }
141
150
 
@@ -143,10 +152,13 @@ export class TaskCoordinator {
143
152
  if (this.stopped || event.event !== 'chat.event') {
144
153
  return;
145
154
  }
155
+
146
156
  const { chatId, event: chatEvent } = event.payload;
157
+
147
158
  if (this.deps.loading?.(chatId)) {
148
159
  return;
149
160
  }
161
+
150
162
  if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn' && chatEvent.item.state === 'running') {
151
163
  this.turnStarted(chatId);
152
164
  } else if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn') {
@@ -164,14 +176,9 @@ export class TaskCoordinator {
164
176
  * which says how that kind of agent reports back.
165
177
  */
166
178
  agentEnded(childId: string, text: string): void {
167
- if (this.stopped) {
168
- return;
179
+ if (this.deps.placed(childId)) {
180
+ this.failed(childId, text);
169
181
  }
170
- const task = this.localTask(childId);
171
- if (!task || !this.deps.placed(childId)) {
172
- return;
173
- }
174
- this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
175
182
  }
176
183
 
177
184
  startFailed(childId: string, error: unknown): void {
@@ -186,9 +193,11 @@ export class TaskCoordinator {
186
193
  /* `done` from the child itself, which wins over whatever else was about to settle it. */
187
194
  async done(childId: string, text: string): Promise<Task | null> {
188
195
  const task = this.localTask(childId);
196
+
189
197
  if (!task) {
190
198
  return null;
191
199
  }
200
+
192
201
  return this.settleNow(task, 'done', { text, source: 'done', at: this.deps.now() });
193
202
  }
194
203
 
@@ -200,17 +209,23 @@ export class TaskCoordinator {
200
209
  async outlasted(childId: string, commands: readonly string[], cause: 'limit' | 'restart' = 'limit'): Promise<Task | null> {
201
210
  const task = this.localTask(childId);
202
211
  const items = this.deps.chatItems(childId);
212
+
203
213
  if (this.stopped || !task || items === null || this.delegating(childId)) {
204
214
  return null;
205
215
  }
216
+
206
217
  const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
218
+
207
219
  if (!last || last.state === 'running' || !this.answers(items, last, task)) {
208
220
  return null;
209
221
  }
222
+
210
223
  const { status, result } = resultOfTurn(last, items, this.deps.now());
224
+
211
225
  if (cause === 'restart') {
212
226
  return this.settleNow(task, 'failed', { ...result, text: `${result.text}\n\n${restartedNote(commands)}`, source: 'exit' });
213
227
  }
228
+
214
229
  return this.settleNow(task, status, { ...result, text: `${result.text}\n\n${outlastedNote(commands)}` });
215
230
  }
216
231
 
@@ -224,15 +239,18 @@ export class TaskCoordinator {
224
239
  /* The open task a chat here works on; one whose child runs on another machine is settled from there. */
225
240
  private localTask(childId: string): Task | undefined {
226
241
  const task = this.deps.tasks.openFor(childId);
242
+
227
243
  return task?.machine === undefined ? task : undefined;
228
244
  }
229
245
 
230
246
  /* An open task of this child that nothing can carry any more. */
231
247
  private failed(childId: string, text: string): void {
232
248
  const task = this.localTask(childId);
249
+
233
250
  if (!task || this.stopped) {
234
251
  return;
235
252
  }
253
+
236
254
  this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
237
255
  }
238
256
 
@@ -241,41 +259,53 @@ export class TaskCoordinator {
241
259
  * gets a turn of its own that names it; the turn that was in its way answers the person who sent it,
242
260
  * not the task, and nothing settles while that turn is still owed. A turn after it answers too, as the
243
261
  * one the CLI opens about work that turn left running. A child opened with its task has no turn of that
244
- * kind, so its first turn settles it, as it always did.
262
+ * kind, so its first turn settles it.
245
263
  */
246
264
  private answers(items: readonly ChatItem[], turn: ChatTurnItem, task: Task): boolean {
247
265
  if ((turn.taskIds ?? []).includes(task.id)) {
248
266
  return true;
249
267
  }
268
+
250
269
  if (this.deps.owedTurn(task.id)) {
251
270
  return false;
252
271
  }
272
+
253
273
  const own = items.findIndex((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id));
274
+
254
275
  return own === -1 ? task.requiresTaskTurn !== true && turn.createdAt >= task.createdAt : own < items.findIndex((item) => item.id === turn.id);
255
276
  }
256
277
 
257
278
  private turnEnded(chatId: string, turn: ChatTurnItem): void {
258
279
  const task = this.localTask(chatId);
259
280
  const items = this.deps.chatItems(chatId);
281
+
260
282
  if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
261
283
  return;
262
284
  }
285
+
263
286
  const paused = this.pauseFor(chatId, turn, items);
287
+
264
288
  if (paused !== null) {
265
289
  this.hold(task, paused);
290
+
266
291
  return;
267
292
  }
293
+
268
294
  // A goal holds the task with no limit, and the parent sleeps on through the turns its CLI opens toward it.
269
295
  const goal = turn.state === 'done' ? (this.deps.activeGoal?.(chatId) ?? null) : null;
296
+
270
297
  if (goal?.running === true) {
271
298
  return;
272
299
  }
300
+
273
301
  if (goal !== null) {
274
302
  const { result } = resultOfTurn(turn, items, this.deps.now());
275
303
  this.settle(task, 'failed', { ...result, text: `${result.text}\n\n${goalLostNote(goal.objective)}`, source: 'exit' });
304
+
276
305
  return;
277
306
  }
278
- // A turn a person stopped or that failed ends the task as it always did, whatever still runs beside it.
307
+
308
+ // A turn a person stopped or that failed ends the task, whatever still runs beside it.
279
309
  if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
280
310
  this.track(
281
311
  this.deps.tasks.holdBackground(task.id, {
@@ -285,21 +315,27 @@ export class TaskCoordinator {
285
315
  }),
286
316
  `Saving the background work of task ${task.id} failed`
287
317
  );
318
+
288
319
  return;
289
320
  }
321
+
290
322
  if (task.background?.turnId === turn.id) {
291
323
  const after = this.deps.afterBackgroundWork(chatId);
324
+
292
325
  if (after === 'reports') {
293
326
  return;
294
327
  }
328
+
295
329
  if (after === 'gone') {
296
330
  this.settleGone(task, task.background, turn, items);
331
+
297
332
  return;
298
333
  }
299
334
  } else if (turn.state === 'done' && this.deps.limit.owed(task.id) && this.deps.afterBackgroundWork(chatId) === 'gone') {
300
335
  // After a restart nothing here remembers the turn was held, but the limit it owes does, and that settles it.
301
336
  return;
302
337
  }
338
+
303
339
  const { status, result } = resultOfTurn(turn, items, this.deps.now());
304
340
  this.settle(task, status, result);
305
341
  }
@@ -309,27 +345,38 @@ export class TaskCoordinator {
309
345
  if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
310
346
  return;
311
347
  }
348
+
349
+ const rowOf = (id: string): ChatItem | undefined => items.find((item) => item.id === id);
350
+
312
351
  const lost = background.itemIds.filter((id) => {
313
- const item = items.find((item) => item.id === id);
352
+ const item = rowOf(id);
353
+
314
354
  return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
315
355
  });
356
+
316
357
  if (lost.length > 0 || background.commands.length > 0) {
317
358
  const names = lost.map((id) => {
318
- const row = items.find((item) => item.id === id);
359
+ const row = rowOf(id);
360
+
319
361
  return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
320
362
  });
363
+
321
364
  this.settle(task, 'failed', {
322
365
  text: `The agent ended before its background work completed: ${[...names, ...background.commands].join(', ')}. This work will not send a result.`,
323
366
  source: 'exit',
324
367
  at: this.deps.now()
325
368
  });
369
+
326
370
  return;
327
371
  }
372
+
328
373
  const reports = background.itemIds.flatMap((id) => {
329
- const row = items.find((item) => item.id === id);
374
+ const row = rowOf(id);
330
375
  const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
376
+
331
377
  return report ? [report] : [];
332
378
  });
379
+
333
380
  this.settle(task, 'done', {
334
381
  text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
335
382
  source: 'turn',
@@ -347,17 +394,22 @@ export class TaskCoordinator {
347
394
  if (this.deps.limit.owed(task.id)) {
348
395
  void this.deps.limit.lapse(task.id).catch((e: unknown) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
349
396
  }
397
+
350
398
  return true;
351
399
  }
400
+
352
401
  const commands = this.deps.commandsOf(chatId);
402
+
353
403
  if (commands.length === 0) {
354
404
  return false;
355
405
  }
406
+
356
407
  if (!this.deps.limit.owed(task.id)) {
357
408
  void this.deps.limit
358
409
  .owe(task, commands.map(commandLabel), this.deps.now() + BACKGROUND_COMMAND_LIMIT_MS)
359
410
  .catch((e: unknown) => console.error(`Owing the limit of task ${task.id} failed:`, errorText(e)));
360
411
  }
412
+
361
413
  return true;
362
414
  }
363
415
 
@@ -367,19 +419,24 @@ export class TaskCoordinator {
367
419
  */
368
420
  private pauseFor(chatId: string, turn: ChatTurnItem, items: readonly ChatItem[]): NonNullable<Task['paused']> | null {
369
421
  const limit = turn.state === 'error' ? turn.limit : undefined;
422
+
370
423
  if (limit?.kind === 'usage') {
371
424
  return { kind: 'usage', ...(limit.resetsAt === undefined ? {} : { until: limit.resetsAt }) };
372
425
  }
426
+
373
427
  if (limit?.kind === 'overload') {
374
428
  const at = this.deps.retryAt?.(chatId, turn, items) ?? null;
429
+
375
430
  return at === null ? null : { kind: 'overload', until: at };
376
431
  }
432
+
377
433
  return null;
378
434
  }
379
435
 
380
436
  /* A child that took up work again is no longer waiting out a limit. */
381
437
  private turnStarted(chatId: string): void {
382
438
  const task = this.localTask(chatId);
439
+
383
440
  if (task?.paused !== undefined) {
384
441
  this.hold(task, null);
385
442
  }
@@ -392,10 +449,13 @@ export class TaskCoordinator {
392
449
  private recheck(chatId: string): void {
393
450
  const task = this.localTask(chatId);
394
451
  const items = this.deps.chatItems(chatId);
452
+
395
453
  if (!task || items === null || this.delegating(chatId)) {
396
454
  return;
397
455
  }
456
+
398
457
  const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
458
+
399
459
  if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
400
460
  this.turnEnded(chatId, last);
401
461
  }
@@ -417,13 +477,17 @@ export class TaskCoordinator {
417
477
 
418
478
  private async settleNow(task: Task, status: 'done' | 'failed', result: TaskResult): Promise<Task | null> {
419
479
  const settled = await this.deps.tasks.settle(task.id, status, result, this.deps.now());
480
+
420
481
  if (!settled) {
421
482
  return null;
422
483
  }
484
+
423
485
  await this.deps.oweWake(settled);
486
+
424
487
  if (status === 'failed') {
425
488
  this.deps.alert(settled.childId, `Task failed: ${settled.title}`, clipText(result.text, 500));
426
489
  }
490
+
427
491
  return settled;
428
492
  }
429
493
  }
@@ -40,6 +40,7 @@ export class TaskStore {
40
40
  /* Told about every task that is written, after it is on disk. */
41
41
  onChange(listener: TaskListener): () => void {
42
42
  this.listeners.add(listener);
43
+
43
44
  return () => {
44
45
  this.listeners.delete(listener);
45
46
  };
@@ -72,7 +73,9 @@ export class TaskStore {
72
73
  settledAt: null,
73
74
  wake: 'pending'
74
75
  };
76
+
75
77
  await this.write(task);
78
+
76
79
  return task;
77
80
  }
78
81
 
@@ -88,9 +91,11 @@ export class TaskStore {
88
91
 
89
92
  async holdBackground(id: string, background: NonNullable<Task['background']>): Promise<void> {
90
93
  const task = this.get(id);
94
+
91
95
  if (!task || task.status !== 'open' || JSON.stringify(task.background) === JSON.stringify(background)) {
92
96
  return;
93
97
  }
98
+
94
99
  await this.write({ ...task, background });
95
100
  }
96
101
 
@@ -122,46 +127,57 @@ export class TaskStore {
122
127
  */
123
128
  async settle(id: string, status: 'done' | 'failed' | 'cancelled', result: TaskResult | null, now: number): Promise<Task | null> {
124
129
  const task = this.tasks.get(id);
130
+
125
131
  if (!task || task.status !== 'open') {
126
132
  return null;
127
133
  }
134
+
128
135
  const { paused: _paused, ...open } = task;
129
136
  const settled: Task = { ...open, status, result, settledAt: now, wake: status === 'cancelled' ? 'none' : task.wake };
130
137
  await this.write(settled);
138
+
131
139
  return settled;
132
140
  }
133
141
 
134
142
  /* Holds an open task while its child waits out a limit, or lets it go on with null; false when it was not open. */
135
143
  async pause(id: string, paused: NonNullable<Task['paused']> | null): Promise<boolean> {
136
144
  const task = this.tasks.get(id);
145
+
137
146
  if (!task || task.status !== 'open') {
138
147
  return false;
139
148
  }
149
+
140
150
  if (JSON.stringify(task.paused ?? null) === JSON.stringify(paused)) {
141
151
  return true;
142
152
  }
153
+
143
154
  const { paused: _was, ...rest } = task;
144
155
  await this.write(paused === null ? rest : { ...rest, paused });
156
+
145
157
  return true;
146
158
  }
147
159
 
148
160
  /* Cancels the open tasks of these children, with the reason as the result; a cancelled task wakes nobody. */
149
161
  async cancelOpen(childIds: ReadonlySet<string>, reason: string, now: number): Promise<Task[]> {
150
162
  const cancelled: Task[] = [];
163
+
151
164
  for (const task of this.sorted()) {
152
165
  if (task.status === 'open' && childIds.has(task.childId)) {
153
166
  const settled = await this.settle(task.id, 'cancelled', { text: reason, source: 'exit', at: now }, now);
167
+
154
168
  if (settled) {
155
169
  cancelled.push(settled);
156
170
  }
157
171
  }
158
172
  }
173
+
159
174
  return cancelled;
160
175
  }
161
176
 
162
177
  async markWoken(ids: readonly string[]): Promise<void> {
163
178
  for (const id of ids) {
164
179
  const task = this.tasks.get(id);
180
+
165
181
  if (task && task.wake === 'pending') {
166
182
  await this.write({ ...task, wake: 'sent' });
167
183
  }
@@ -178,12 +194,14 @@ export class TaskStore {
178
194
  */
179
195
  readyWake(parentId: string): Task[] {
180
196
  const open = new Set(this.openBatches(parentId));
197
+
181
198
  return this.pendingWake(parentId).filter((task) => task.batchId === undefined || !open.has(task.batchId));
182
199
  }
183
200
 
184
201
  /* The batches of this parent with a task still open, oldest first. */
185
202
  openBatches(parentId: string): string[] {
186
203
  const ids = this.ofParent(parentId).flatMap((task) => (task.status === 'open' && task.batchId !== undefined ? [task.batchId] : []));
204
+
187
205
  return [...new Set(ids)];
188
206
  }
189
207
 
@@ -194,6 +212,7 @@ export class TaskStore {
194
212
  async dropWake(parentId: string): Promise<void> {
195
213
  for (const { id } of this.ofParent(parentId)) {
196
214
  const task = this.tasks.get(id);
215
+
197
216
  if (task && task.wake === 'pending') {
198
217
  await this.write({ ...task, wake: 'none' });
199
218
  }
@@ -206,21 +225,27 @@ export class TaskStore {
206
225
  */
207
226
  async prune(projectId: string, ids: ReadonlySet<string>, now: number): Promise<Task[]> {
208
227
  const cancelled: Task[] = [];
228
+
209
229
  for (const task of this.sorted()) {
210
230
  if (task.projectId !== projectId) {
211
231
  continue;
212
232
  }
233
+
213
234
  if (!ids.has(task.parentId)) {
214
235
  await this.tasks.remove(task.id);
236
+
215
237
  continue;
216
238
  }
239
+
217
240
  if (!ids.has(task.childId) && task.status === 'open') {
218
241
  const settled = await this.settle(task.id, 'cancelled', null, now);
242
+
219
243
  if (settled) {
220
244
  cancelled.push(settled);
221
245
  }
222
246
  }
223
247
  }
248
+
224
249
  return cancelled;
225
250
  }
226
251
 
@@ -233,6 +258,7 @@ export class TaskStore {
233
258
  const write = this.writeNow(task);
234
259
  this.writes.add(write);
235
260
  void write.finally(() => this.writes.delete(write)).catch(() => undefined);
261
+
236
262
  return write;
237
263
  }
238
264
 
@@ -241,9 +267,11 @@ export class TaskStore {
241
267
  if (!(await this.tasks.write(task))) {
242
268
  return;
243
269
  }
270
+
244
271
  for (const listener of this.listeners) {
245
272
  listener(task);
246
273
  }
274
+
247
275
  this.sinks.emit({ event: 'task.changed', payload: { task } });
248
276
  }
249
277
  }
@@ -29,12 +29,14 @@ export function chatRequests(chats: Pick<ChatCore, 'get' | 'answer'>): ChatReque
29
29
  // The projector writes each under the kind it is.
30
30
  const thread = chats.get(nodeId)?.thread;
31
31
  const item = thread?.get(`question-${requestId}`) ?? thread?.get(`approval-${requestId}`);
32
+
32
33
  return item?.kind === 'question' || item?.kind === 'approval' ? item : null;
33
34
  },
34
35
  waiting: (nodeId) => (chats.get(nodeId)?.thread.list() ?? []).filter(isPendingRequest),
35
36
  answer: (nodeId, requestId, answers) => {
36
37
  try {
37
38
  chats.answer(nodeId, requestId, answers);
39
+
38
40
  return true;
39
41
  } catch {
40
42
  return false;
@@ -86,16 +88,22 @@ export class WaitingObserver {
86
88
  if (this.stopped || event.event !== 'chat.event') {
87
89
  return;
88
90
  }
91
+
89
92
  const { chatId, event: chatEvent } = event.payload;
93
+
90
94
  if (chatEvent.type !== 'item' || !isPendingRequest(chatEvent.item)) {
91
95
  return;
92
96
  }
97
+
93
98
  const task = this.deps.openTask(chatId);
94
99
  const key = `${chatId}\n${chatEvent.item.requestId}`;
100
+
95
101
  if (task === undefined || this.owed.has(key)) {
96
102
  return;
97
103
  }
104
+
98
105
  this.owed.add(key);
106
+
99
107
  void this.deps
100
108
  .enqueue(
101
109
  task.projectId,
@@ -120,16 +128,19 @@ const NOTE_QUESTION_LENGTH = 200;
120
128
 
121
129
  function quotedShort(text: string): string {
122
130
  const line = oneLine(text);
131
+
123
132
  return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
124
133
  }
125
134
 
126
135
  /* One question as the parent's CLI reads it, with the choices it may pick from. */
127
136
  function questionLine(question: ChatQuestionItem['questions'][number]): string {
128
137
  const choices = question.choices.map((choice) => (choice.description === '' ? choice.label : `${choice.label} (${choice.description})`));
138
+
129
139
  const how =
130
140
  choices.length === 0
131
141
  ? 'answer in your own words'
132
142
  : `choices: ${choices.join(' | ')}; ${question.multiSelect ? 'pick one or more' : 'pick one'}, or answer in your own words`;
143
+
133
144
  return `- question ${question.id}: ${quoted(question.question)} (${how})`;
134
145
  }
135
146
 
@@ -146,9 +157,12 @@ export interface WaitingWords {
146
157
  function answerCall(cli: string, childId: string, item: ChatQuestionItem): string {
147
158
  if (item.questions.length === 1) {
148
159
  const multi = item.questions[0]!.multiSelect ? ', several labels in one --answer separated by a comma' : '';
160
+
149
161
  return `${cli} answer ${childId} ${item.requestId} --answer '<a choice label as written, or your own words>'${multi}`;
150
162
  }
163
+
151
164
  const shape = Object.fromEntries(item.questions.map((question) => [question.id, '...']));
165
+
152
166
  return `${cli} answer ${childId} ${item.requestId} --answers '${JSON.stringify(shape)}', one answer per question id`;
153
167
  }
154
168
 
@@ -158,8 +172,10 @@ function answerCall(cli: string, childId: string, item: ChatQuestionItem): strin
158
172
  */
159
173
  export function waitingTexts(child: { id: string; title: string }, item: WaitingRequest, words: WaitingWords): { note: string; preamble: string } {
160
174
  const who = `${child.title} (node ${child.id})`;
175
+
161
176
  if (item.kind === 'approval') {
162
177
  const what = item.description === null ? item.toolName : `${item.toolName}: ${item.description}`;
178
+
163
179
  return {
164
180
  note: `${who} waits for a person to approve ${what}`,
165
181
  preamble: [
@@ -168,11 +184,14 @@ export function waitingTexts(child: { id: string; title: string }, item: Waiting
168
184
  ].join('\n')
169
185
  };
170
186
  }
187
+
171
188
  const asked = item.questions.length === 1 ? quotedShort(item.questions[0]!.question) : `${item.questions.length} questions`;
189
+
172
190
  const how = [
173
191
  ...item.questions.map(questionLine),
174
192
  `A person can answer it in that node, and this chat with ${answerCall(words.cli, child.id, item)}. Whoever answers first wins; the verb refuses a question that was answered already.`
175
193
  ];
194
+
176
195
  return {
177
196
  note: [`${who} waits for an answer to ${asked}`, '', ...how].join('\n'),
178
197
  preamble: [
@@ -197,9 +216,11 @@ export function deliverWaitingHandler(deps: DeliverWaitingDeps) {
197
216
  return async (entry: DeliverWaitingEntry): Promise<OutboxOutcome> => {
198
217
  const { childId, requestId } = entry.payload;
199
218
  const item = deps.request(childId, requestId);
219
+
200
220
  if (!isPendingRequest(item)) {
201
221
  return;
202
222
  }
223
+
203
224
  const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
204
225
  await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), from: childId, ...texts });
205
226
  };