@aragon-agent/cli 0.6.35 → 0.6.36
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.
- package/CHANGELOG.md +21 -0
- package/README.md +3173 -3071
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +64 -4
- package/dist/agent/headless.js +31 -0
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +131 -4
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +310 -46
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +2 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +21 -1
- package/dist/ui/ScrollbackShell.d.ts +19 -19
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/TeamPanel.d.ts +26 -44
- package/dist/ui/TeamPanel.js +46 -135
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +13 -3
- package/dist/ui/layout/header-hints.js +48 -14
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +23 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +17 -2
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/team-status.d.ts +25 -0
- package/dist/ui/team-status.js +31 -3
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +9 -1
- package/package.json +2 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two stripping lists, and the two functions that apply them
|
|
3
|
+
* (tui-subagent-panel §4.5.4).
|
|
4
|
+
*
|
|
5
|
+
* WHY TWO LISTS AND NOT ONE. The fields fall into three groups, not two:
|
|
6
|
+
*
|
|
7
|
+
* - `activity` / `activityArgs` / `retry` / `timeline` have NO reader on a settled run
|
|
8
|
+
* (the card and the report read `summary` / `error` / the counters), so they are
|
|
9
|
+
* removed at the dispatch exit as well;
|
|
10
|
+
* - `lastIntervention` MUST survive onto the settled run -
|
|
11
|
+
* `overseer-runtime-supervision.test.ts` asserts it and the supervision report reads
|
|
12
|
+
* it - but `team/types.ts` marks it TRANSIENT / never persisted, and it is not part
|
|
13
|
+
* of `session/validate-session.ts`'s persisted shape either. So it is stripped at
|
|
14
|
+
* the SAVE boundary only;
|
|
15
|
+
* - `retryable`, `truncated`, `compactions`, `lastTool`, `summary`, `error` are facts
|
|
16
|
+
* that still mean something after the dispatch, and are in NEITHER list. Deleting
|
|
17
|
+
* `retryable` in passing would be the kind of "cleanup" that loses a contract.
|
|
18
|
+
*
|
|
19
|
+
* WHY TWO PLACES AND NOT ONE (§4.5.4 / RV-P0-1). `dispatch()`'s exit only cleans the
|
|
20
|
+
* FINAL outcome. Everything written to disk goes through `saveSession`, and during a
|
|
21
|
+
* dispatch the live `team` entry holds the LIVE runs - `teamUpdate` writes them
|
|
22
|
+
* straight into `view.entries`. So a `/save`, a checkpoint or the exit flush issued
|
|
23
|
+
* mid-dispatch would carry the timeline into `<id>.json` even with the dispatch exit
|
|
24
|
+
* perfectly clean. The save boundary is therefore the second, independent gate, and it
|
|
25
|
+
* is the one that protects all three write paths (`R-12`).
|
|
26
|
+
*
|
|
27
|
+
* WHY THIS IS NOT IN `runtime.ts` (RV-P1-4): that file is already past the 1,000-line
|
|
28
|
+
* ceiling `CLAUDE.md` sets and has no dated exception in `.claude-index/config.md`, so
|
|
29
|
+
* new logic goes here and `runtime.ts` keeps one call.
|
|
30
|
+
*
|
|
31
|
+
* ASCII ONLY: `src/team/**` is inside `glyphs.test.ts`'s static scan.
|
|
32
|
+
*/
|
|
33
|
+
/** Fields with no reader on a settled run. Stripped at the dispatch exit. */
|
|
34
|
+
export const SETTLE_STRIP_FIELDS = ['activity', 'activityArgs', 'retry', 'timeline'];
|
|
35
|
+
/**
|
|
36
|
+
* Fields that may never reach a file. `lastIntervention` joins the four above because
|
|
37
|
+
* it must live on the settled run while staying out of every persisted one.
|
|
38
|
+
*/
|
|
39
|
+
export const PERSIST_STRIP_FIELDS = [...SETTLE_STRIP_FIELDS, 'lastIntervention'];
|
|
40
|
+
/**
|
|
41
|
+
* Drop `fields` from one run, returning a NEW object - and the SAME object when there
|
|
42
|
+
* is nothing to drop, so a run's identity is preserved for reference equality where
|
|
43
|
+
* nothing changed (the `publish()` path compares nothing, but the reducer's
|
|
44
|
+
* `mapEntry` does dedupe on identity in some frames).
|
|
45
|
+
*/
|
|
46
|
+
function stripRun(run, fields) {
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const field of fields) {
|
|
49
|
+
if (run[field] !== undefined) {
|
|
50
|
+
changed = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!changed)
|
|
55
|
+
return run;
|
|
56
|
+
const copy = { ...run };
|
|
57
|
+
for (const field of fields)
|
|
58
|
+
delete copy[field];
|
|
59
|
+
return copy;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The dispatch exit: one run, without `SETTLE_STRIP_FIELDS`.
|
|
63
|
+
*
|
|
64
|
+
* `lastIntervention` is deliberately KEPT (see this file's header) - the settled card's
|
|
65
|
+
* badge and the supervision report both read it.
|
|
66
|
+
*/
|
|
67
|
+
export function settledRun(run) {
|
|
68
|
+
return stripRun({ ...run, filesTouched: [...run.filesTouched] }, SETTLE_STRIP_FIELDS);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The save boundary: every `kind: 'team'` entry's runs, without
|
|
72
|
+
* `PERSIST_STRIP_FIELDS`.
|
|
73
|
+
*
|
|
74
|
+
* Entries that carry no team runs - or whose runs carry none of the fields - are
|
|
75
|
+
* returned UNTOUCHED, which is what keeps an ordinary session file byte-identical to
|
|
76
|
+
* the one this build produced before the feature.
|
|
77
|
+
*/
|
|
78
|
+
export function stripEphemeralRuns(entries) {
|
|
79
|
+
return entries.map((entry) => {
|
|
80
|
+
if (entry.kind !== 'team')
|
|
81
|
+
return entry;
|
|
82
|
+
const runs = entry.runs.map((run) => stripRun(run, PERSIST_STRIP_FIELDS));
|
|
83
|
+
return runs.some((run, index) => run !== entry.runs[index])
|
|
84
|
+
? { ...entry, runs }
|
|
85
|
+
: entry;
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/team/subagent.js
CHANGED
|
@@ -15,8 +15,9 @@ import { formatStreamError } from '../agent/reducer.js';
|
|
|
15
15
|
import { buildSystemPrompt } from '../agent/system-prompt.js';
|
|
16
16
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
17
17
|
import { createBuiltinTools } from '../tools/index.js';
|
|
18
|
-
import { pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
18
|
+
import { describeToolActivity, pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
19
19
|
import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
|
|
20
|
+
import { appendTimeline } from './timeline.js';
|
|
20
21
|
import { isRetryableStreamError } from './retry.js';
|
|
21
22
|
import { resolveSubagentKey, resolveSubagentModel } from './model.js';
|
|
22
23
|
import { makeTeamSend, makeTeamWait, withMailboxTail } from './comm-tools.js';
|
|
@@ -306,6 +307,18 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
306
307
|
// append: a long turn would otherwise grow this without limit, and it is the
|
|
307
308
|
// only per-token allocation this feature adds.
|
|
308
309
|
let textTail = '';
|
|
310
|
+
/**
|
|
311
|
+
* The tool call in flight, for the timeline's duration (tui-subagent-panel §4.5.2).
|
|
312
|
+
*
|
|
313
|
+
* ONE field, not a stack: a child executes one tool at a time, and the ring records
|
|
314
|
+
* only the pair's difference. The `start` event deliberately writes NOTHING to the
|
|
315
|
+
* timeline - a line that appears and is then rewritten would push a real event out of
|
|
316
|
+
* the 24-slot ring for every call.
|
|
317
|
+
*/
|
|
318
|
+
let pendingTool = null;
|
|
319
|
+
const noteTimeline = (entry) => {
|
|
320
|
+
run.timeline = appendTimeline(run.timeline, entry);
|
|
321
|
+
};
|
|
309
322
|
const emit = () => {
|
|
310
323
|
const now = Date.now();
|
|
311
324
|
const retryKey = run.retry ? `${run.retry.attempt}/${run.retry.maxRetries}` : '';
|
|
@@ -399,6 +412,15 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
399
412
|
break;
|
|
400
413
|
}
|
|
401
414
|
case 'compaction_end':
|
|
415
|
+
// The timeline records the APPLIED compactions only (§4.5.2): a refused one
|
|
416
|
+
// changed nothing, and the board already shows the phase.
|
|
417
|
+
if (event.applied) {
|
|
418
|
+
noteTimeline({
|
|
419
|
+
kind: 'compact',
|
|
420
|
+
at: Date.now(),
|
|
421
|
+
text: `compacted ${event.estimatedTokensBefore} -> ${event.estimatedTokensAfter} tokens`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
402
424
|
contextManager?.onCompactionEnd({
|
|
403
425
|
applied: event.applied,
|
|
404
426
|
...(event.reason !== undefined ? { reason: event.reason } : {}),
|
|
@@ -407,6 +429,9 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
407
429
|
});
|
|
408
430
|
break;
|
|
409
431
|
case 'tool_execution_start':
|
|
432
|
+
// Recorded for the DURATION only; the entry itself is appended on the end
|
|
433
|
+
// event, when `ok` and `ms` are both known (§4.5.2).
|
|
434
|
+
pendingTool = { name: event.toolName, at: Date.now() };
|
|
410
435
|
run.phase = event.toolName === 'team_wait' ? 'waiting' : 'tool';
|
|
411
436
|
run.lastTool = event.toolName;
|
|
412
437
|
run.toolCalls += 1;
|
|
@@ -415,7 +440,20 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
415
440
|
run.activityArgs = pickActivityArgs(event.args); // PICKED, not raw (P1-4)
|
|
416
441
|
noteFile(event.toolName, event.args);
|
|
417
442
|
break;
|
|
418
|
-
case 'tool_execution_end':
|
|
443
|
+
case 'tool_execution_end': {
|
|
444
|
+
const endedAt = Date.now();
|
|
445
|
+
const startedAt = pendingTool?.at;
|
|
446
|
+
pendingTool = null;
|
|
447
|
+
// THE ARGUMENTS ARE GONE BY NOW, on purpose (P1-5): `activityArgs` is cleared a
|
|
448
|
+
// few lines below, and the timeline's text comes from `describeToolActivity` on
|
|
449
|
+
// the PICKED subset, so a `write_file` body can never reach the board.
|
|
450
|
+
noteTimeline({
|
|
451
|
+
kind: 'tool',
|
|
452
|
+
at: endedAt,
|
|
453
|
+
ok: event.isError !== true,
|
|
454
|
+
...(startedAt !== undefined ? { ms: Math.max(0, endedAt - startedAt) } : {}),
|
|
455
|
+
text: describeToolActivity(event.toolName, run.activityArgs, true),
|
|
456
|
+
});
|
|
419
457
|
run.phase = 'thinking';
|
|
420
458
|
textTail = '';
|
|
421
459
|
run.activity = undefined;
|
|
@@ -434,6 +472,7 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
434
472
|
run.blockedWaits = deps.bus.blockedWaitCount(spec.label);
|
|
435
473
|
}
|
|
436
474
|
break;
|
|
475
|
+
}
|
|
437
476
|
case 'message_update':
|
|
438
477
|
if (event.streamEvent.type === 'error') {
|
|
439
478
|
run.error = formatStreamError(event.streamEvent.error);
|
|
@@ -444,6 +483,13 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
444
483
|
run.retry = undefined;
|
|
445
484
|
}
|
|
446
485
|
else if (event.streamEvent.type === 'retry_scheduled') {
|
|
486
|
+
// The timeline keeps the retry's ARRIVAL only (§4.5.2): the countdown itself is
|
|
487
|
+
// a live readout on the row, and a tick per attempt would eat the ring.
|
|
488
|
+
noteTimeline({
|
|
489
|
+
kind: 'retry',
|
|
490
|
+
at: Date.now(),
|
|
491
|
+
text: `retry ${event.streamEvent.attempt}/${event.streamEvent.maxRetries}`,
|
|
492
|
+
});
|
|
447
493
|
// SET, and the phase is deliberately LEFT ALONE: the child is still
|
|
448
494
|
// `thinking` as far as the scheduler is concerned, and rewriting the
|
|
449
495
|
// phase would put a retry into `SubagentPhase` — a union the report, the
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-child event ring behind the Agents board's `timeline` section
|
|
3
|
+
* (tui-subagent-panel §4.5).
|
|
4
|
+
*
|
|
5
|
+
* WHY A RING ON THE RUN rather than a second store: a `SubagentRun` already IS "one
|
|
6
|
+
* child's projection", and every UI surface reads it through `publish()`. A separate
|
|
7
|
+
* store would be a second authority for the same subject and would have to be
|
|
8
|
+
* subscribed to separately - and the four collection points below are all single
|
|
9
|
+
* funnels that already exist (§4.5.2).
|
|
10
|
+
*
|
|
11
|
+
* FIVE KINDS, NOT EVERY TURN (D-12). A turn marker per turn would fill twenty-four
|
|
12
|
+
* slots in a long task and push out the tool calls, which are the part a reader cannot
|
|
13
|
+
* reconstruct from the report. Phase changes are visible in the board's `state` field
|
|
14
|
+
* in real time, so they do not need history.
|
|
15
|
+
*
|
|
16
|
+
* BOUNDED AND PURE. `appendTimeline` returns a NEW array (never mutates the old one),
|
|
17
|
+
* drops the oldest entry past `TEAM_LIMITS.timelineEntries`, and clamps every line to
|
|
18
|
+
* `TEAM_LIMITS.timelineTextChars`. Purity is what lets `publish()`'s shallow copy hold
|
|
19
|
+
* a stable snapshot without knowing this module exists (AC-25).
|
|
20
|
+
*
|
|
21
|
+
* ASCII ONLY: `src/team/**` is inside `glyphs.test.ts`'s static scan.
|
|
22
|
+
*
|
|
23
|
+
* THE TEXT IS ALWAYS BUILT FROM `describeToolActivity` / `sanitizeActivity`, never
|
|
24
|
+
* from raw tool arguments (D-11 / P1-4). That is the boundary that keeps a
|
|
25
|
+
* `write_file` body, a `bash` transcript or a child's prompt out of a surface that is
|
|
26
|
+
* one `JSON.stringify` from a log.
|
|
27
|
+
*/
|
|
28
|
+
import type { SubagentPhase, SubagentRun } from './types.js';
|
|
29
|
+
export type TimelineKind = 'tool' | 'retry' | 'compact' | 'overseer' | 'settle';
|
|
30
|
+
export interface TimelineEntry {
|
|
31
|
+
kind: TimelineKind;
|
|
32
|
+
/** Absolute epoch ms, the same units as `startedAt` / `endedAt` / `lastIntervention.at`. */
|
|
33
|
+
at: number;
|
|
34
|
+
/** Already sanitized + clamped: at most `TEAM_LIMITS.timelineTextChars` characters. */
|
|
35
|
+
text: string;
|
|
36
|
+
/** Whether the tool call succeeded (`tool` only). */
|
|
37
|
+
ok?: boolean;
|
|
38
|
+
/** How long the tool call took in ms (`tool` only). */
|
|
39
|
+
ms?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function isTerminalPhase(phase: SubagentPhase): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Append one event, returning a NEW array. The oldest entry is dropped once the ring
|
|
44
|
+
* is full, so the array's length is bounded by `max` for every possible input.
|
|
45
|
+
*/
|
|
46
|
+
export declare function appendTimeline(prev: readonly TimelineEntry[] | undefined, entry: TimelineEntry, max?: number): TimelineEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* The terminal event for one ATTEMPT, or `null` when there is nothing to record yet.
|
|
49
|
+
*
|
|
50
|
+
* WHY THIS IS CALLED FROM `publish()` AND NOT FROM `settlePhase` (§4.5.2 / RV-P1-3):
|
|
51
|
+
* `settlePhase` covers the retry loop's exit only, and two paths set a terminal phase
|
|
52
|
+
* directly - the worker's "never started" abort and the cold-start backoff abort. Those
|
|
53
|
+
* are exactly the children whose explanation a reader needs most, and hanging the
|
|
54
|
+
* settle entry on `settlePhase` alone would leave them with no last event.
|
|
55
|
+
*
|
|
56
|
+
* IDEMPOTENCE IS KEYED ON THE ATTEMPT, not on "the last entry is a settle" (I-14): a
|
|
57
|
+
* retry re-stamps `startedAt`, so a new attempt legitimately earns a second terminal
|
|
58
|
+
* entry, while a repeated `publish()` of the same attempt adds nothing.
|
|
59
|
+
*/
|
|
60
|
+
export declare function settleTimelineEntry(run: SubagentRun, at: number): TimelineEntry | null;
|
|
61
|
+
/**
|
|
62
|
+
* The run as a reader should see it: unchanged, or with its attempt's terminal event
|
|
63
|
+
* appended once. Called from `publish()`, which is every state change's single exit
|
|
64
|
+
* (§4.5.2) and therefore covers the two paths that set a terminal phase directly.
|
|
65
|
+
*
|
|
66
|
+
* ADDED TO THE EMITTED SNAPSHOT, NEVER TO THE LIVE RUN (this is the part `publish()`
|
|
67
|
+
* makes subtle). `subagent.ts` calls `onUpdate` with a COPY of the run, so writing the
|
|
68
|
+
* entry onto the object `publish()` receives would be lost - and because
|
|
69
|
+
* `settleTimelineEntry` uses the live timeline as its idempotence key, a lost write
|
|
70
|
+
* means every later publish of the same settled child appends another `settle` row to
|
|
71
|
+
* the ring. Keeping the live run untouched makes the entry a property of the VIEW:
|
|
72
|
+
* exactly one terminal event per attempt, however many frames are published.
|
|
73
|
+
*/
|
|
74
|
+
export declare function withSettleEntry(run: SubagentRun, at: number): SubagentRun;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-child event ring behind the Agents board's `timeline` section
|
|
3
|
+
* (tui-subagent-panel §4.5).
|
|
4
|
+
*
|
|
5
|
+
* WHY A RING ON THE RUN rather than a second store: a `SubagentRun` already IS "one
|
|
6
|
+
* child's projection", and every UI surface reads it through `publish()`. A separate
|
|
7
|
+
* store would be a second authority for the same subject and would have to be
|
|
8
|
+
* subscribed to separately - and the four collection points below are all single
|
|
9
|
+
* funnels that already exist (§4.5.2).
|
|
10
|
+
*
|
|
11
|
+
* FIVE KINDS, NOT EVERY TURN (D-12). A turn marker per turn would fill twenty-four
|
|
12
|
+
* slots in a long task and push out the tool calls, which are the part a reader cannot
|
|
13
|
+
* reconstruct from the report. Phase changes are visible in the board's `state` field
|
|
14
|
+
* in real time, so they do not need history.
|
|
15
|
+
*
|
|
16
|
+
* BOUNDED AND PURE. `appendTimeline` returns a NEW array (never mutates the old one),
|
|
17
|
+
* drops the oldest entry past `TEAM_LIMITS.timelineEntries`, and clamps every line to
|
|
18
|
+
* `TEAM_LIMITS.timelineTextChars`. Purity is what lets `publish()`'s shallow copy hold
|
|
19
|
+
* a stable snapshot without knowing this module exists (AC-25).
|
|
20
|
+
*
|
|
21
|
+
* ASCII ONLY: `src/team/**` is inside `glyphs.test.ts`'s static scan.
|
|
22
|
+
*
|
|
23
|
+
* THE TEXT IS ALWAYS BUILT FROM `describeToolActivity` / `sanitizeActivity`, never
|
|
24
|
+
* from raw tool arguments (D-11 / P1-4). That is the boundary that keeps a
|
|
25
|
+
* `write_file` body, a `bash` transcript or a child's prompt out of a surface that is
|
|
26
|
+
* one `JSON.stringify` from a log.
|
|
27
|
+
*/
|
|
28
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
29
|
+
/** The phases that end an attempt. Used by the settle-idempotence rule below. */
|
|
30
|
+
const TERMINAL_PHASES = new Set(['done', 'failed', 'aborted']);
|
|
31
|
+
export function isTerminalPhase(phase) {
|
|
32
|
+
return TERMINAL_PHASES.has(phase);
|
|
33
|
+
}
|
|
34
|
+
/** Clamp one line to the display budget. Collapses whitespace for the same reason. */
|
|
35
|
+
function clampText(text) {
|
|
36
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
37
|
+
if (flat.length <= TEAM_LIMITS.timelineTextChars)
|
|
38
|
+
return flat;
|
|
39
|
+
return `${flat.slice(0, TEAM_LIMITS.timelineTextChars - 3)}...`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Append one event, returning a NEW array. The oldest entry is dropped once the ring
|
|
43
|
+
* is full, so the array's length is bounded by `max` for every possible input.
|
|
44
|
+
*/
|
|
45
|
+
export function appendTimeline(prev, entry, max = TEAM_LIMITS.timelineEntries) {
|
|
46
|
+
const limit = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : TEAM_LIMITS.timelineEntries;
|
|
47
|
+
const next = [...(prev ?? []), { ...entry, text: clampText(entry.text) }];
|
|
48
|
+
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
49
|
+
}
|
|
50
|
+
/** The first line of a multi-line answer, clamped - a timeline entry is one line. */
|
|
51
|
+
function head(text) {
|
|
52
|
+
if (text === undefined || text.length === 0)
|
|
53
|
+
return '';
|
|
54
|
+
return clampText(text);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The terminal event for one ATTEMPT, or `null` when there is nothing to record yet.
|
|
58
|
+
*
|
|
59
|
+
* WHY THIS IS CALLED FROM `publish()` AND NOT FROM `settlePhase` (§4.5.2 / RV-P1-3):
|
|
60
|
+
* `settlePhase` covers the retry loop's exit only, and two paths set a terminal phase
|
|
61
|
+
* directly - the worker's "never started" abort and the cold-start backoff abort. Those
|
|
62
|
+
* are exactly the children whose explanation a reader needs most, and hanging the
|
|
63
|
+
* settle entry on `settlePhase` alone would leave them with no last event.
|
|
64
|
+
*
|
|
65
|
+
* IDEMPOTENCE IS KEYED ON THE ATTEMPT, not on "the last entry is a settle" (I-14): a
|
|
66
|
+
* retry re-stamps `startedAt`, so a new attempt legitimately earns a second terminal
|
|
67
|
+
* entry, while a repeated `publish()` of the same attempt adds nothing.
|
|
68
|
+
*/
|
|
69
|
+
export function settleTimelineEntry(run, at) {
|
|
70
|
+
if (!isTerminalPhase(run.phase))
|
|
71
|
+
return null;
|
|
72
|
+
const attemptStart = run.startedAt ?? 0;
|
|
73
|
+
const last = run.timeline?.[run.timeline.length - 1];
|
|
74
|
+
if (last?.kind === 'settle' && last.at >= attemptStart)
|
|
75
|
+
return null;
|
|
76
|
+
const text = run.phase === 'done'
|
|
77
|
+
? `done: ${head(run.summary)}`.trim()
|
|
78
|
+
: run.phase === 'failed'
|
|
79
|
+
? `failed: ${head(run.error)}`.trim()
|
|
80
|
+
: 'aborted';
|
|
81
|
+
return { kind: 'settle', at, text };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The run as a reader should see it: unchanged, or with its attempt's terminal event
|
|
85
|
+
* appended once. Called from `publish()`, which is every state change's single exit
|
|
86
|
+
* (§4.5.2) and therefore covers the two paths that set a terminal phase directly.
|
|
87
|
+
*
|
|
88
|
+
* ADDED TO THE EMITTED SNAPSHOT, NEVER TO THE LIVE RUN (this is the part `publish()`
|
|
89
|
+
* makes subtle). `subagent.ts` calls `onUpdate` with a COPY of the run, so writing the
|
|
90
|
+
* entry onto the object `publish()` receives would be lost - and because
|
|
91
|
+
* `settleTimelineEntry` uses the live timeline as its idempotence key, a lost write
|
|
92
|
+
* means every later publish of the same settled child appends another `settle` row to
|
|
93
|
+
* the ring. Keeping the live run untouched makes the entry a property of the VIEW:
|
|
94
|
+
* exactly one terminal event per attempt, however many frames are published.
|
|
95
|
+
*/
|
|
96
|
+
export function withSettleEntry(run, at) {
|
|
97
|
+
const settle = settleTimelineEntry(run, at);
|
|
98
|
+
if (settle === null)
|
|
99
|
+
return run;
|
|
100
|
+
return { ...run, timeline: appendTimeline(run.timeline, settle) };
|
|
101
|
+
}
|
package/dist/team/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { TokenUsage } from '@aragon-agent/core';
|
|
14
14
|
import type { ActivityArgs } from './activity.js';
|
|
15
|
+
import type { TimelineEntry } from './timeline.js';
|
|
15
16
|
/** One normalized delegation request. Produced only by `normalizeSubagentSpecs`. */
|
|
16
17
|
export interface SubagentSpec {
|
|
17
18
|
/** Slug, unique within the dispatch. Shown in the panel, the report and logs. */
|
|
@@ -163,7 +164,10 @@ export interface SubagentRun {
|
|
|
163
164
|
* The most recent supervisor decision applied to this child
|
|
164
165
|
* (subagent-overseer-v2 section 6). TRANSIENT, never persisted - the
|
|
165
166
|
* same rule as `activity` / `activityArgs`; a resumed session has no
|
|
166
|
-
* live dispatch.
|
|
167
|
+
* live dispatch. The strip that enforces it is `PERSIST_STRIP_FIELDS` in
|
|
168
|
+
* `team/settled.ts` (tui-subagent-panel §4.5.4): it stays on the settled run,
|
|
169
|
+
* because the card's badge and the supervision report read it there, and the
|
|
170
|
+
* SAVE boundary is where it is removed.
|
|
167
171
|
*
|
|
168
172
|
* `at` is ABSOLUTE epoch ms, the same units as `startedAt` / `endedAt`
|
|
169
173
|
* (R-P2-3), so the panel renders `nudged 2m` with one subtraction and
|
|
@@ -175,6 +179,25 @@ export interface SubagentRun {
|
|
|
175
179
|
at: number;
|
|
176
180
|
reasonHead: string;
|
|
177
181
|
};
|
|
182
|
+
/**
|
|
183
|
+
* The last `TEAM_LIMITS.timelineEntries` (24) events of this child
|
|
184
|
+
* (tui-subagent-panel §4.5 / §7).
|
|
185
|
+
*
|
|
186
|
+
* EPHEMERAL, on exactly the same terms as `activity` / `activityArgs` / `retry`, and
|
|
187
|
+
* guaranteed by TWO independent strips (`team/settled.ts`): `settledRun()` at the
|
|
188
|
+
* dispatch exit and `stripEphemeralRuns()` in `saveSession`'s chain. One gate is not
|
|
189
|
+
* enough - the live entry's runs are written by `teamUpdate` during a dispatch, so a
|
|
190
|
+
* checkpoint or an exit flush would otherwise carry the ring to disk (§4.5.4).
|
|
191
|
+
*
|
|
192
|
+
* NEVER persisted, never in `aragon exec`'s stream either: `exec/runner.ts`'s
|
|
193
|
+
* `emitTeam` builds its events from an explicit field list, so a new run field cannot
|
|
194
|
+
* leak into `stream-json` (AC-22).
|
|
195
|
+
*
|
|
196
|
+
* `at` is ABSOLUTE epoch ms (the units of `startedAt` / `lastIntervention.at`), and
|
|
197
|
+
* each `text` is already sanitized and clamped, so appending is the only write and
|
|
198
|
+
* `appendTimeline` is the only appender.
|
|
199
|
+
*/
|
|
200
|
+
timeline?: TimelineEntry[];
|
|
178
201
|
}
|
|
179
202
|
/** What the supervisor may do with one inspected child. */
|
|
180
203
|
export type OverseerAction = 'wait' | 'nudge' | 'replace' | 'abandon';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The acceptance-criteria model (todo-acceptance-criteria §6.2 / §6.3 / §6.6.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/todo/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* PURE: no I/O, no timers, no `Date.now()`. The clock is injected where a
|
|
7
|
+
* timestamp is needed, exactly as `TodoStore`'s constructor does (P2-4).
|
|
8
|
+
*
|
|
9
|
+
* ONE PRINCIPLE RUNS THROUGH THE WHOLE FILE, and it is the reason this module
|
|
10
|
+
* exists: **a verdict is evidence, a plan is a belief**. A verdict is never
|
|
11
|
+
* destroyed by omission - not by re-sending the criterion as `pending`, and not
|
|
12
|
+
* by leaving it out of the list altogether - because a full replacement means the
|
|
13
|
+
* model re-sends the whole table on every turn and one lapse would silently erase
|
|
14
|
+
* a conclusion the user was shown. An unchecked criterion, by contrast, IS a
|
|
15
|
+
* plan, so a re-plan is free to drop it.
|
|
16
|
+
*
|
|
17
|
+
* NEVER THROWS, NEVER REJECTS, ONLY REPAIRS (I-9): the same discipline
|
|
18
|
+
* `normalize.ts` records for the todo list itself.
|
|
19
|
+
*/
|
|
20
|
+
import type { AcceptanceItem, AcceptanceSnapshot, PlanPhase, TodoItem, TodoSnapshot } from './types.js';
|
|
21
|
+
export interface NormalizeAcceptanceResult {
|
|
22
|
+
items: AcceptanceItem[];
|
|
23
|
+
/** How many entries the payload contained BEFORE dropping and capping. */
|
|
24
|
+
requested: number;
|
|
25
|
+
/** Human-readable repair notes, already bounded. Empty when nothing was wrong. */
|
|
26
|
+
repairs: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface NormalizeAcceptanceOptions {
|
|
29
|
+
/** `todo.acceptance`, read LIVE (D-11). False short-circuits everything below. */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Per-lineage dedupe of the restoration note (§6.2.1 step 9 / P2-4).
|
|
33
|
+
*
|
|
34
|
+
* OWNED BY `TodoStore`, not by this module: the note would otherwise be
|
|
35
|
+
* re-printed on every turn, because the model has no way to know the server
|
|
36
|
+
* restored a verdict and would keep sending the same `pending` row. Passing the
|
|
37
|
+
* set in keeps this function pure and keeps the dedupe state in the one object
|
|
38
|
+
* that already owns the plan's lifetime.
|
|
39
|
+
*/
|
|
40
|
+
restoredNotes?: Set<string>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Repair whatever arrived in the `acceptance` field of a `todo_write` call.
|
|
44
|
+
*
|
|
45
|
+
* ORDER IS SEMANTICS, and one step of that order is load-bearing enough to be a
|
|
46
|
+
* review condition (C-2): `enabled` short-circuits FIRST, then `undefined`, then
|
|
47
|
+
* `Array.isArray`. `undefined` (the key is absent) and `[]` (the model sent an
|
|
48
|
+
* empty array) are NOT the same thing: absent means "I am only updating steps,
|
|
49
|
+
* keep the table", empty means "this plan needs no criteria". This is the one
|
|
50
|
+
* place in the feature where the two differ (§9).
|
|
51
|
+
*/
|
|
52
|
+
export declare function normalizeAcceptance(raw: unknown, previous: readonly AcceptanceItem[], options: NormalizeAcceptanceOptions): NormalizeAcceptanceResult;
|
|
53
|
+
/**
|
|
54
|
+
* The immutable projection of one committed table (§6.2.2).
|
|
55
|
+
*
|
|
56
|
+
* CALLED FROM EXACTLY ONE PLACE PER COMMIT - `TodoStore`'s `write` / `restore` /
|
|
57
|
+
* `clear` / `reopenAll` - and stored, never recomputed on read. `snapshot()` runs
|
|
58
|
+
* on every event; recomputing here would stamp a new `now()` and a new object
|
|
59
|
+
* each frame, invalidating `TodoCard`'s `React.memo` and the transcript's height
|
|
60
|
+
* cache (P1-3).
|
|
61
|
+
*/
|
|
62
|
+
export declare function acceptanceSummary(items: readonly AcceptanceItem[], now: () => number): AcceptanceSnapshot;
|
|
63
|
+
/**
|
|
64
|
+
* The ONE judge of "which phase is this plan in" (I-3 / D-5).
|
|
65
|
+
*
|
|
66
|
+
* Derived rather than stored, so the model, the rail, the transcript card and the
|
|
67
|
+
* machine event stream cannot disagree: a stored field would be a second writer
|
|
68
|
+
* of a fact that two lists already determine. Callers must not invoke this for a
|
|
69
|
+
* `null` plan.
|
|
70
|
+
*/
|
|
71
|
+
export declare function derivePlanPhase(items: readonly TodoItem[], acceptance: readonly AcceptanceItem[] | null): PlanPhase;
|
|
72
|
+
/**
|
|
73
|
+
* "Is there nothing left to do in this plan" - THE single judge (G-4 / I-3).
|
|
74
|
+
*
|
|
75
|
+
* Referenced by follow-through, by `App`'s grace-window re-check and by
|
|
76
|
+
* `TodoStore.beginUserTurn`, because "every step is completed" stopped being the
|
|
77
|
+
* same claim as "the work is done" the moment acceptance criteria existed. Three
|
|
78
|
+
* copies of the old equation is exactly how a plan gets cleared while its last
|
|
79
|
+
* criterion is still unchecked.
|
|
80
|
+
*/
|
|
81
|
+
export declare function planIsResolved(items: readonly TodoItem[], acceptance: readonly AcceptanceItem[] | null): boolean;
|
|
82
|
+
export declare function countUnfinishedSteps(items: readonly TodoItem[]): number;
|
|
83
|
+
export declare function countUncheckedCriteria(acceptance: readonly AcceptanceItem[] | null): number;
|
|
84
|
+
/** What is left in a plan, as the two numbers the notices are written from. */
|
|
85
|
+
export interface Outstanding {
|
|
86
|
+
steps: number;
|
|
87
|
+
checks: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The six follow-through exits all need "how much is left", and after this
|
|
91
|
+
* feature that quantity has two halves (§6.6.1.1). One function decides it, so no
|
|
92
|
+
* exit can print `0 steps left` on a plan whose steps are all done but whose
|
|
93
|
+
* criteria are not - which is what `total - doneCount` would say on every verify
|
|
94
|
+
* turn.
|
|
95
|
+
*/
|
|
96
|
+
export declare function outstandingOf(snapshot: TodoSnapshot): Outstanding;
|
|
97
|
+
/**
|
|
98
|
+
* Singular and plural decided in ONE place (§6.6.1.1): six call sites must not
|
|
99
|
+
* each spell their own.
|
|
100
|
+
*/
|
|
101
|
+
export declare function criteriaPhrase(count: number): string;
|
|
102
|
+
/**
|
|
103
|
+
* A bounded enumeration of what a plan still owes, for the tool result text
|
|
104
|
+
* (§6.3). Ordered `todoFirst` because the tool result is read top-down and the
|
|
105
|
+
* step being worked on is the more urgent of the two when both exist.
|
|
106
|
+
*
|
|
107
|
+
* BOUNDED BY CONSTRUCTION at `VERIFY_MESSAGE_LIMITS.uncheckedItems` rows: this
|
|
108
|
+
* string is paid for on every call (I-11).
|
|
109
|
+
*/
|
|
110
|
+
export declare function planOutstanding(items: readonly TodoItem[], acceptance: readonly AcceptanceItem[] | null, todoFirst: boolean): string[];
|
|
111
|
+
/**
|
|
112
|
+
* The continuation text for the verification phase (§6.6.2).
|
|
113
|
+
*
|
|
114
|
+
* CONSTRUCTIVELY BOUNDED, which is what AC-22 asserts: the unchecked list stops
|
|
115
|
+
* at 5 rows, the failed list at 2, `check` is clipped to 100 characters and
|
|
116
|
+
* `evidence` to 60. Both truncations are deliberate - the full text is already in
|
|
117
|
+
* the model's own previous payload, so re-sending it is paying twice for the same
|
|
118
|
+
* bytes; what the model needs is a reminder of WHICH rows are open.
|
|
119
|
+
*/
|
|
120
|
+
export declare function buildVerifyMessage(snapshot: TodoSnapshot): string;
|