@aragon-agent/cli 0.5.12 → 0.6.2

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 (63) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +322 -6
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +32 -0
  6. package/dist/agent/controller.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +16 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +143 -2
  10. package/dist/config/ui-state.d.ts +29 -0
  11. package/dist/config/ui-state.js +14 -4
  12. package/dist/diagnostics/cli-commands.d.ts +10 -0
  13. package/dist/diagnostics/cli-commands.js +61 -0
  14. package/dist/diagnostics/doctor.d.ts +29 -0
  15. package/dist/diagnostics/doctor.js +207 -0
  16. package/dist/diagnostics/info.d.ts +30 -0
  17. package/dist/diagnostics/info.js +99 -0
  18. package/dist/exec/cli-commands.d.ts +31 -0
  19. package/dist/exec/cli-commands.js +63 -0
  20. package/dist/exec/emitter.d.ts +80 -0
  21. package/dist/exec/emitter.js +127 -0
  22. package/dist/exec/events.d.ts +226 -0
  23. package/dist/exec/events.js +43 -0
  24. package/dist/exec/index.d.ts +56 -0
  25. package/dist/exec/index.js +439 -0
  26. package/dist/exec/options.d.ts +76 -0
  27. package/dist/exec/options.js +221 -0
  28. package/dist/exec/permission.d.ts +66 -0
  29. package/dist/exec/permission.js +96 -0
  30. package/dist/exec/runner.d.ts +204 -0
  31. package/dist/exec/runner.js +559 -0
  32. package/dist/exec/stdin-stream.d.ts +46 -0
  33. package/dist/exec/stdin-stream.js +144 -0
  34. package/dist/logging/install.d.ts +22 -0
  35. package/dist/logging/install.js +22 -0
  36. package/dist/session/cli-commands.d.ts +30 -0
  37. package/dist/session/cli-commands.js +184 -0
  38. package/dist/session/persist.d.ts +46 -0
  39. package/dist/session/persist.js +8 -0
  40. package/dist/session/store.d.ts +147 -0
  41. package/dist/session/store.js +442 -0
  42. package/dist/team/runtime.d.ts +12 -0
  43. package/dist/team/runtime.js +6 -0
  44. package/dist/team/subagent.d.ts +12 -0
  45. package/dist/team/subagent.js +4 -0
  46. package/dist/tools/index.d.ts +20 -0
  47. package/dist/tools/index.js +26 -0
  48. package/dist/ui/ActivityLine.d.ts +16 -0
  49. package/dist/ui/ActivityLine.js +23 -1
  50. package/dist/ui/App.d.ts +14 -1
  51. package/dist/ui/App.js +61 -7
  52. package/dist/ui/BottomStatusRow.d.ts +33 -1
  53. package/dist/ui/BottomStatusRow.js +20 -6
  54. package/dist/ui/Composer.d.ts +14 -1
  55. package/dist/ui/Composer.js +5 -5
  56. package/dist/ui/overlays/HelpOverlay.js +12 -1
  57. package/dist/ui/use-startup-notices.d.ts +98 -4
  58. package/dist/ui/use-startup-notices.js +108 -2
  59. package/dist/ui/win-vt-force.d.ts +136 -0
  60. package/dist/ui/win-vt-force.js +237 -0
  61. package/dist/ui/win-vt-input.d.ts +57 -0
  62. package/dist/ui/win-vt-input.js +62 -0
  63. package/package.json +7 -3
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Stream translation, budgets and interrupts for `aragon exec`
3
+ * (cli-integration-surface section 3.3 / 3.5 / 3.6).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * IT NEVER TOUCHES STDOUT. Everything it wants to say goes through the
8
+ * `ExecEmitter` it was handed, which is the only object in this feature that
9
+ * holds a stdout stream (R-2). The one exception is `stderr`, used for the
10
+ * caller-facing notices `-p` already writes there.
11
+ *
12
+ * IT DOES NOT EMIT `result` EITHER, and that is deliberate (D-2). `run()`
13
+ * RETURNS the parameters; `exec/index.ts` persists the session first and emits
14
+ * second, because a caller that reads `result` and immediately respawns with the
15
+ * same `--session-id` must not race the writer of the file it is about to read.
16
+ */
17
+ import { formatStreamError } from '../agent/reducer.js';
18
+ import { setSignalTerminator } from '../logging/install.js';
19
+ import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
20
+ export function singlePrompt(text) {
21
+ let served = false;
22
+ return {
23
+ next: async () => {
24
+ if (served)
25
+ return null;
26
+ served = true;
27
+ return text;
28
+ },
29
+ };
30
+ }
31
+ const DEFAULT_SIGNAL_PORT = {
32
+ setTerminator: setSignalTerminator,
33
+ exit: (code) => process.exit(code),
34
+ };
35
+ /** A second signal inside this window stops waiting for a graceful settle. */
36
+ const SECOND_SIGNAL_WINDOW_MS = 2000;
37
+ export class ExecRunner {
38
+ emitter;
39
+ sessionId;
40
+ opts;
41
+ signals;
42
+ now;
43
+ controller = null;
44
+ unsubscribers = [];
45
+ turns = 0;
46
+ usage = { inputTokens: 0, outputTokens: 0 };
47
+ lastAssistantText = '';
48
+ errored = false;
49
+ sawTurnEnd = false;
50
+ stopReason = 'end_turn';
51
+ errorCode = null;
52
+ errorMessage = null;
53
+ signalNumber = null;
54
+ thinkingBuffer = '';
55
+ budgetTimer = null;
56
+ timerStarted = false;
57
+ detached = false;
58
+ lastSignalAt = 0;
59
+ constructor(options) {
60
+ this.emitter = options.emitter;
61
+ this.sessionId = options.sessionId;
62
+ this.opts = options;
63
+ this.signals = options.signals ?? DEFAULT_SIGNAL_PORT;
64
+ this.now = options.now ?? Date.now;
65
+ }
66
+ // -----------------------------------------------------------------------
67
+ // Attach / detach
68
+ // -----------------------------------------------------------------------
69
+ /**
70
+ * Subscribe to all four streams.
71
+ *
72
+ * TEXT MODE CALLS THIS TOO, WITH A `TextEmitter` THAT WRITES NOTHING (section
73
+ * 3.2). The listener is what counts `turn_end` and accumulates usage for
74
+ * `SessionMeta`; byte-equality with `runHeadless` is a property of what is
75
+ * WRITTEN, not of how many listeners exist, and `runHeadless` already tolerates
76
+ * multiple subscribers.
77
+ */
78
+ attach(controller) {
79
+ this.controller = controller;
80
+ this.unsubscribers.push(controller.subscribe((event) => this.onAgentEvent(event)));
81
+ const team = controller.subscribeTeam?.((event) => {
82
+ if (event.type === 'usage') {
83
+ this.usage.inputTokens += event.usage.inputTokens;
84
+ this.usage.outputTokens += event.usage.outputTokens;
85
+ return;
86
+ }
87
+ this.emitTeam(event);
88
+ });
89
+ if (team)
90
+ this.unsubscribers.push(team);
91
+ const todos = controller.subscribeTodos?.((event) => {
92
+ if (event.type !== 'updated')
93
+ return;
94
+ const { total, doneCount, activeIndex, items } = event.snapshot;
95
+ this.emit({
96
+ type: 'todo',
97
+ sessionId: this.sessionId,
98
+ total,
99
+ done: doneCount,
100
+ activeIndex,
101
+ items: items.map((item) => ({ content: item.content, status: item.status })),
102
+ });
103
+ });
104
+ if (todos)
105
+ this.unsubscribers.push(todos);
106
+ const fast = controller.subscribeFast?.((event) => {
107
+ if (event.type === 'usage') {
108
+ this.usage.inputTokens += event.usage.inputTokens;
109
+ this.usage.outputTokens += event.usage.outputTokens;
110
+ return;
111
+ }
112
+ if (event.type !== 'review_end' || !event.review.text)
113
+ return;
114
+ this.emit({
115
+ type: 'fast_review',
116
+ sessionId: this.sessionId,
117
+ turn: event.review.turn,
118
+ model: event.review.model,
119
+ text: event.review.text,
120
+ });
121
+ });
122
+ if (fast)
123
+ this.unsubscribers.push(fast);
124
+ this.installTerminator();
125
+ }
126
+ /**
127
+ * Drop the subscriptions and the timer.
128
+ *
129
+ * IDEMPOTENT, and it clears the wall-clock timer even though that timer is
130
+ * `unref`d: `unref` only stops it holding the event loop open, it does not stop
131
+ * it firing and calling `abort()` on a controller the caller has since
132
+ * disposed (R-10).
133
+ */
134
+ detach() {
135
+ if (this.detached)
136
+ return;
137
+ this.detached = true;
138
+ for (const off of this.unsubscribers.splice(0)) {
139
+ try {
140
+ off();
141
+ }
142
+ catch {
143
+ // A subscription that was already torn down is not a reason to leave the
144
+ // rest attached.
145
+ }
146
+ }
147
+ if (this.budgetTimer) {
148
+ clearTimeout(this.budgetTimer);
149
+ this.budgetTimer = null;
150
+ }
151
+ }
152
+ stats() {
153
+ return {
154
+ turns: this.turns,
155
+ usage: { ...this.usage },
156
+ lastAssistantText: this.lastAssistantText,
157
+ errored: this.errored,
158
+ stopReason: this.stopReason,
159
+ errorCode: this.errorCode,
160
+ errorMessage: this.errorMessage,
161
+ signalNumber: this.signalNumber,
162
+ };
163
+ }
164
+ /**
165
+ * `0` success | `1` agent error | `3` budget | `128 + signo` for a signal.
166
+ *
167
+ * `3` IS ITS OWN CODE RATHER THAN `0` OR `1` (D-6). Both alternatives lose
168
+ * information a shell script needs and cannot recover without parsing JSON:
169
+ * `0` claims a complete answer that does not exist, `1` claims a malfunction
170
+ * that did not happen.
171
+ */
172
+ exitCode() {
173
+ if (this.stopReason === 'interrupted')
174
+ return 128 + (this.signalNumber ?? 2);
175
+ if (this.stopReason === 'max_turns' || this.stopReason === 'timeout')
176
+ return 3;
177
+ return this.errored ? 1 : 0;
178
+ }
179
+ // -----------------------------------------------------------------------
180
+ // The run loop
181
+ // -----------------------------------------------------------------------
182
+ /**
183
+ * Drive the conversation until the source is exhausted or a budget fires.
184
+ *
185
+ * The inner loop is the todo follow-through continuation of `runHeadless`,
186
+ * with the IDENTICAL policy - both call `decideFollowThrough`, which is why
187
+ * the two faces cannot drift about what "unfinished" means.
188
+ */
189
+ async run(source) {
190
+ const controller = this.controller;
191
+ if (!controller)
192
+ throw new Error('ExecRunner.run() before attach()');
193
+ const followThrough = this.opts.followThrough ?? 'notify';
194
+ for (;;) {
195
+ const next = await source.next();
196
+ // THE SOURCE IS POLLED BEFORE THE EXHAUSTION CHECK, SO A RUN THAT ENDS ON
197
+ // ITS OWN TERMS IS NEVER MISREPORTED (IF-3). `next === null` first is what makes
198
+ // that true: a conversation of exactly `--max-turns` turns whose caller has
199
+ // nothing more to say exits 0 with `end_turn`, not 3 with `max_turns`.
200
+ if (next === null)
201
+ break;
202
+ if (this.turnBudgetExhausted()) {
203
+ this.stopReason = 'max_turns';
204
+ break;
205
+ }
206
+ const finished = await this.runFollowThroughChain(controller, next, followThrough);
207
+ if (finished)
208
+ break;
209
+ }
210
+ }
211
+ /** One caller message plus every auto-continuation it produces. */
212
+ async runFollowThroughChain(controller, initial, followThrough) {
213
+ let budget = emptyBudget();
214
+ let wasAutoContinuation = false;
215
+ let message = initial;
216
+ let source = 'caller';
217
+ for (;;) {
218
+ // PER-ITERATION RESET, exactly as `runHeadless` does it: `sawTurnEnd` is
219
+ // the silent-failure detector, and left sticky a swallowed throw on
220
+ // iteration 2 is invisible because iteration 1 set the flag.
221
+ this.sawTurnEnd = false;
222
+ this.emit({
223
+ type: 'user',
224
+ sessionId: this.sessionId,
225
+ turn: this.turns + 1,
226
+ text: message,
227
+ source,
228
+ });
229
+ this.startBudgetTimer();
230
+ await controller.prompt(message);
231
+ if (this.stopReason !== 'end_turn')
232
+ return true;
233
+ const snapshot = controller.getTodoSnapshot?.() ?? null;
234
+ budget = advanceBudget(budget, snapshot, wasAutoContinuation);
235
+ wasAutoContinuation = false;
236
+ const decision = decideFollowThrough({
237
+ mode: followThrough,
238
+ snapshot,
239
+ // `aborted` is false here for the same structural reason `-p` gives: an
240
+ // interrupt sets `stopReason` and returns above, so this branch is only
241
+ // reached by a run that ended on its own terms.
242
+ runEnd: { aborted: false, errored: this.errored },
243
+ budget,
244
+ interactive: false,
245
+ });
246
+ if (decision.kind === 'none')
247
+ return false;
248
+ if (decision.kind === 'notify') {
249
+ this.note(`[todo] ${decision.text}`);
250
+ return false;
251
+ }
252
+ // A CLI-generated continuation is still a turn the caller is paying for,
253
+ // so the ceiling binds here exactly as it does on a caller message.
254
+ if (this.turnBudgetExhausted()) {
255
+ this.stopReason = 'max_turns';
256
+ return true;
257
+ }
258
+ // CHARGED BEFORE THE RE-PROMPT. Headless has no grace timer, so without
259
+ // this line `maxAutoContinuesPerList` would never bind in the one
260
+ // environment with no human to press Esc.
261
+ budget = { ...budget, used: budget.used + 1 };
262
+ wasAutoContinuation = true;
263
+ this.note(`[todo] ${decision.notice}`);
264
+ message = decision.message;
265
+ source = 'todo_continue';
266
+ }
267
+ }
268
+ // -----------------------------------------------------------------------
269
+ // Budgets (section 3.5)
270
+ // -----------------------------------------------------------------------
271
+ /**
272
+ * The wall clock starts at the FIRST `prompt()`, not at process start: the
273
+ * caller is bounding the run, and config resolution plus a skills scan are not
274
+ * part of what they asked to bound.
275
+ */
276
+ startBudgetTimer() {
277
+ if (this.timerStarted || this.opts.maxDurationMs === undefined)
278
+ return;
279
+ this.timerStarted = true;
280
+ this.budgetTimer = setTimeout(() => {
281
+ this.stopReason = 'timeout';
282
+ this.controller?.abort();
283
+ }, this.opts.maxDurationMs);
284
+ // `unref` so it can never hold the event loop open, and it is cleared in
285
+ // `detach()` so it can never fire against a disposed controller (R-10).
286
+ this.budgetTimer.unref?.();
287
+ }
288
+ turnBudgetExhausted() {
289
+ return this.opts.maxTurns !== undefined && this.turns >= this.opts.maxTurns;
290
+ }
291
+ /**
292
+ * Enforced at `turn_start`, NOT at `turn_end` (implementation finding IF-3).
293
+ *
294
+ * The design said "count `turn_end`; on reaching `n`, abort", and the counting
295
+ * half is right - `turns` is exactly that count. Acting on it at `turn_end` is
296
+ * not: a run that COMPLETES in exactly `n` turns would trip the budget it
297
+ * never exceeded and report `stopReason: "max_turns"` with exit 3, so a CI
298
+ * script running `aragon exec --max-turns 20 ... || fail` would fail on a
299
+ * successful twenty-turn run. That is the single most likely way to make a
300
+ * safety ceiling unusable as a safety ceiling.
301
+ *
302
+ * A turn that is STARTING when the count already sits at `n` is, by contrast,
303
+ * unambiguous: the model asked for turn `n + 1` and the caller said no. AC-16
304
+ * is satisfied either way - "a model that would take five turns" with
305
+ * `--max-turns 2` still exits 3 with the partial answer in `result.result`.
306
+ */
307
+ checkTurnBudget() {
308
+ if (!this.turnBudgetExhausted())
309
+ return;
310
+ if (this.stopReason !== 'end_turn')
311
+ return;
312
+ this.stopReason = 'max_turns';
313
+ // THE EXISTING ABORT PATH, the same one `Esc` uses, so subagents are torn
314
+ // down and the engine settles normally rather than being abandoned mid-turn.
315
+ this.controller?.abort();
316
+ }
317
+ // -----------------------------------------------------------------------
318
+ // Interrupts (section 3.6 / P0-1 / D-15)
319
+ // -----------------------------------------------------------------------
320
+ /**
321
+ * EXEC REPLACES THE TERMINATOR; IT DOES NOT ADD A LISTENER.
322
+ *
323
+ * `logging/install.ts` registers the `SIGINT` / `SIGTERM` / `SIGHUP` listeners
324
+ * once, for EVERY invocation, and its default terminator exits. A listener
325
+ * added here would be later in EventEmitter order and would never run - the
326
+ * process is already gone - so the run would vanish with no `result` line,
327
+ * which is precisely the "the wrapper has to time out to learn the run ended"
328
+ * outcome this whole section exists to prevent (P0-1 / R-15).
329
+ *
330
+ * FIRST SIGNAL: abort, record the reason, and RETURN WITHOUT EXITING. The run
331
+ * settles through the normal path - session persisted, `result` emitted,
332
+ * `process.exitCode = 128 + signo`.
333
+ *
334
+ * SECOND SIGNAL WITHIN 2s: exit immediately. That escape is what makes
335
+ * returning safe: a wedged settle is always one more Ctrl-C away from ending.
336
+ *
337
+ * AFTER `detach()`: exit immediately as well, because from that moment there
338
+ * is nothing left to settle and the caller's Ctrl-C must not become "nothing
339
+ * happens".
340
+ *
341
+ * `detach()` DOES NOT RESTORE THE PREVIOUS TERMINATOR, AND THAT IS THE POINT
342
+ * OF THE BRANCH ABOVE (implementation finding IF-7). The design asked for a
343
+ * restore in `finally`; `setSignalTerminator` returns `void` and there is no
344
+ * getter, so a literal restore would mean changing `logging/install.ts`, which
345
+ * the change plan holds to a doc-comment-only edit. The `detached` branch is
346
+ * what makes that safe rather than merely convenient: once detached this
347
+ * terminator does `exit(128 + signo)`, which is byte for byte what the default
348
+ * terminator does, so a left-behind one is INDISTINGUISHABLE from a restored
349
+ * one. Anything that ever gives this process a terminator worth returning to -
350
+ * the full-screen one that restores the terminal, or a second `runExec` in one
351
+ * process under the programmatic API section 2.2 hedges for - must revisit
352
+ * this and add the getter.
353
+ */
354
+ installTerminator() {
355
+ this.signals.setTerminator((signo) => {
356
+ const at = this.now();
357
+ const secondSignal = at - this.lastSignalAt <= SECOND_SIGNAL_WINDOW_MS;
358
+ if (this.detached || (this.lastSignalAt > 0 && secondSignal)) {
359
+ this.signals.exit(128 + signo);
360
+ return;
361
+ }
362
+ this.lastSignalAt = at;
363
+ this.signalNumber = signo;
364
+ this.stopReason = 'interrupted';
365
+ this.controller?.abort();
366
+ });
367
+ }
368
+ // -----------------------------------------------------------------------
369
+ // Event translation
370
+ // -----------------------------------------------------------------------
371
+ emit(event) {
372
+ this.emitter.emit(event);
373
+ }
374
+ /** Caller-facing prose. STDERR ALWAYS - stdout carries nothing but the schema. */
375
+ note(text) {
376
+ if (this.opts.quiet)
377
+ return;
378
+ (this.opts.stderr ?? process.stderr).write(`${text}\n`);
379
+ }
380
+ onAgentEvent(event) {
381
+ switch (event.type) {
382
+ case 'message_update':
383
+ this.onStreamEvent(event.streamEvent);
384
+ break;
385
+ case 'turn_start':
386
+ // The ONE budget check that catches the ordinary case: a single
387
+ // `prompt()` drives many turns, so a ceiling enforced only between
388
+ // caller messages would never bind at all.
389
+ this.checkTurnBudget();
390
+ break;
391
+ case 'tool_execution_start':
392
+ this.emit({
393
+ type: 'tool_call',
394
+ sessionId: this.sessionId,
395
+ turn: this.turns + 1,
396
+ id: event.toolCallId,
397
+ name: event.toolName,
398
+ input: (event.args ?? {}),
399
+ });
400
+ break;
401
+ case 'tool_execution_end':
402
+ this.emit({
403
+ type: 'tool_result',
404
+ sessionId: this.sessionId,
405
+ turn: this.turns + 1,
406
+ id: event.toolCallId,
407
+ name: event.toolName,
408
+ isError: event.isError,
409
+ durationMs: event.duration,
410
+ output: toolResultText(event.result),
411
+ });
412
+ break;
413
+ case 'turn_end':
414
+ this.onTurnEnd(event.message, event.usage);
415
+ break;
416
+ case 'agent_end':
417
+ // A swallowed throw (missing key / empty stream) produces no `turn_end`.
418
+ if (!this.sawTurnEnd && !this.errored && this.stopReason === 'end_turn') {
419
+ this.errored = true;
420
+ this.recordError('agent_error', 'The run produced no assistant turn.');
421
+ }
422
+ break;
423
+ default:
424
+ break;
425
+ }
426
+ }
427
+ onStreamEvent(se) {
428
+ if (se.type === 'text_delta') {
429
+ this.flushThinking();
430
+ if (this.opts.partialMessages) {
431
+ this.emit({
432
+ type: 'text_delta',
433
+ sessionId: this.sessionId,
434
+ turn: this.turns + 1,
435
+ delta: se.delta,
436
+ });
437
+ }
438
+ return;
439
+ }
440
+ if (se.type === 'thinking_delta') {
441
+ if (this.opts.includeThinking)
442
+ this.thinkingBuffer += se.delta;
443
+ return;
444
+ }
445
+ if (se.type === 'error') {
446
+ this.errored = true;
447
+ this.recordError('stream_error', formatStreamError(se.error));
448
+ this.emit({
449
+ type: 'error',
450
+ sessionId: this.sessionId,
451
+ fatal: true,
452
+ code: 'stream_error',
453
+ message: formatStreamError(se.error),
454
+ });
455
+ return;
456
+ }
457
+ if (se.type === 'retry_scheduled') {
458
+ this.emit({
459
+ type: 'retry',
460
+ sessionId: this.sessionId,
461
+ attempt: se.attempt,
462
+ maxRetries: se.maxRetries,
463
+ errorType: String(se.errorType),
464
+ delayMs: se.delayMs,
465
+ });
466
+ }
467
+ }
468
+ onTurnEnd(message, usage) {
469
+ this.sawTurnEnd = true;
470
+ this.turns += 1;
471
+ this.usage.inputTokens += usage.inputTokens;
472
+ this.usage.outputTokens += usage.outputTokens;
473
+ this.flushThinking();
474
+ const text = assistantText(message);
475
+ if (text.length > 0) {
476
+ this.lastAssistantText = text;
477
+ this.emit({ type: 'assistant', sessionId: this.sessionId, turn: this.turns, text });
478
+ }
479
+ }
480
+ flushThinking() {
481
+ if (this.thinkingBuffer.length === 0)
482
+ return;
483
+ const text = this.thinkingBuffer;
484
+ this.thinkingBuffer = '';
485
+ this.emit({ type: 'thinking', sessionId: this.sessionId, turn: this.turns + 1, text });
486
+ }
487
+ recordError(code, message) {
488
+ if (this.errorCode)
489
+ return;
490
+ this.errorCode = code;
491
+ this.errorMessage = message;
492
+ if (this.stopReason === 'end_turn')
493
+ this.stopReason = 'error';
494
+ }
495
+ emitTeam(event) {
496
+ if (event.type === 'dispatch_start') {
497
+ this.emit({
498
+ type: 'team',
499
+ sessionId: this.sessionId,
500
+ subtype: 'dispatch_start',
501
+ total: event.specs.length,
502
+ });
503
+ return;
504
+ }
505
+ if (event.type === 'agent_update') {
506
+ const { run } = event;
507
+ this.emit({
508
+ type: 'team',
509
+ sessionId: this.sessionId,
510
+ subtype: 'agent_update',
511
+ label: run.label,
512
+ phase: run.phase,
513
+ description: run.description,
514
+ durationMs: Math.max(0, (run.endedAt ?? 0) - (run.startedAt ?? 0)),
515
+ toolCalls: run.toolCalls,
516
+ });
517
+ return;
518
+ }
519
+ if (event.type === 'dispatch_end') {
520
+ const runs = event.outcome.runs;
521
+ this.emit({
522
+ type: 'team',
523
+ sessionId: this.sessionId,
524
+ subtype: 'dispatch_end',
525
+ ok: runs.filter((r) => r.phase === 'done' && !r.error).length,
526
+ total: runs.length,
527
+ aborted: event.outcome.aborted,
528
+ });
529
+ }
530
+ }
531
+ }
532
+ /** The final assistant text of a turn, or `''` when the turn was tool calls only. */
533
+ function assistantText(message) {
534
+ if (!message || !Array.isArray(message.content))
535
+ return '';
536
+ const parts = [];
537
+ for (const block of message.content) {
538
+ if (block.type === 'text')
539
+ parts.push(block.text);
540
+ }
541
+ return parts.join('').trim();
542
+ }
543
+ /**
544
+ * Flatten a `ToolResult` to the text the MODEL saw.
545
+ *
546
+ * NO SECOND TRUNCATION (AC-26). The core executor already clips at roughly
547
+ * 100 KB; clipping again here would make the event a summary of a summary, and
548
+ * the one faithful choice is to carry what was actually in the conversation.
549
+ */
550
+ function toolResultText(result) {
551
+ if (!result || !Array.isArray(result.content))
552
+ return '';
553
+ const parts = [];
554
+ for (const block of result.content) {
555
+ if (block.type === 'text')
556
+ parts.push(block.text);
557
+ }
558
+ return parts.join('');
559
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `--input-format stream-json`: an NDJSON reader on stdin
3
+ * (cli-integration-surface section 4.1).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * TOLERANT BY CONTRACT (AC-20). A malformed line yields ONE non-fatal `error`
8
+ * event and is skipped; an unknown `type` is ignored entirely. The forward
9
+ * compatibility the schema promises consumers has to run in both directions, or
10
+ * a caller written against a newer version could not talk to an older CLI at
11
+ * all - and a run that dies on one bad line is the worst possible failure for a
12
+ * long-lived worker feeding turns down a pipe.
13
+ *
14
+ * Three message types are understood:
15
+ *
16
+ * { "type": "user", "text": "..." } the next turn
17
+ * { "type": "interrupt" } abort the turn in flight
18
+ * { "type": "end" } no more turns; settle and exit
19
+ *
20
+ * Reaching end-of-stream means the same as `end`, so a caller that simply closes
21
+ * the pipe is not punished for it.
22
+ */
23
+ import type { ExecPromptSource } from './runner.js';
24
+ export interface StdinStreamOptions {
25
+ input: NodeJS.ReadableStream;
26
+ /** A non-fatal parse failure. The caller turns this into an `error` event. */
27
+ onBadLine: (message: string) => void;
28
+ /** `{"type":"interrupt"}`. The caller aborts the run in flight. */
29
+ onInterrupt: () => void;
30
+ }
31
+ /**
32
+ * A prompt source backed by NDJSON on stdin.
33
+ *
34
+ * IT BUFFERS RATHER THAN PAUSING THE STREAM. A caller may legitimately push
35
+ * three turns in one write before the first has finished, and a reader that only
36
+ * accepted a line when someone was waiting for one would deadlock the second and
37
+ * third behind a turn that is minutes long.
38
+ */
39
+ export declare function createStdinPromptSource(opts: StdinStreamOptions): ExecPromptSource;
40
+ /**
41
+ * Serve a fixed list of prompts, then delegate.
42
+ *
43
+ * Used for `aragon exec --input-format stream-json "first turn"`, where the
44
+ * positional argument is the opening message and stdin supplies the rest.
45
+ */
46
+ export declare function prefixSource(prefix: string[], rest: ExecPromptSource): ExecPromptSource;