@aragon-agent/cli 0.5.2 → 0.5.3
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 +90 -0
- package/README.md +128 -1
- package/dist/agent/controller.d.ts +51 -1
- package/dist/agent/controller.js +177 -2
- package/dist/agent/headless.d.ts +12 -0
- package/dist/agent/headless.js +41 -1
- package/dist/agent/reducer.d.ts +48 -0
- package/dist/agent/reducer.js +91 -2
- package/dist/agent/system-prompt.d.ts +17 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/cli.js +66 -2
- package/dist/commands/builtins.js +82 -0
- package/dist/config/app-paths.d.ts +17 -0
- package/dist/config/app-paths.js +21 -0
- package/dist/config/cli-commands.d.ts +26 -2
- package/dist/config/cli-commands.js +63 -4
- package/dist/config/env.d.ts +2 -2
- package/dist/config/env.js +28 -2
- package/dist/config/history-commands.d.ts +20 -0
- package/dist/config/history-commands.js +71 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.js +65 -5
- package/dist/config/migrate-state-out-of-config.d.ts +32 -0
- package/dist/config/migrate-state-out-of-config.js +85 -0
- package/dist/config/prompt-history.d.ts +86 -0
- package/dist/config/prompt-history.js +332 -0
- package/dist/config/schema.d.ts +112 -16
- package/dist/config/schema.js +89 -6
- package/dist/config/store.js +13 -4
- package/dist/config/ui-state.d.ts +52 -0
- package/dist/config/ui-state.js +149 -0
- package/dist/logging/install.d.ts +85 -0
- package/dist/logging/install.js +94 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.d.ts +16 -0
- package/dist/session/persist.js +19 -1
- package/dist/team/activity.d.ts +67 -0
- package/dist/team/activity.js +130 -0
- package/dist/team/bus.d.ts +190 -0
- package/dist/team/bus.js +366 -0
- package/dist/team/comm-tools.d.ts +61 -0
- package/dist/team/comm-tools.js +221 -0
- package/dist/team/human-queue.d.ts +55 -0
- package/dist/team/human-queue.js +72 -0
- package/dist/team/limits.d.ts +123 -0
- package/dist/team/limits.js +128 -0
- package/dist/team/normalize.d.ts +39 -0
- package/dist/team/normalize.js +106 -0
- package/dist/team/panel-rows.d.ts +48 -0
- package/dist/team/panel-rows.js +75 -0
- package/dist/team/prompt.d.ts +49 -0
- package/dist/team/prompt.js +87 -0
- package/dist/team/report.d.ts +58 -0
- package/dist/team/report.js +205 -0
- package/dist/team/retry.d.ts +48 -0
- package/dist/team/retry.js +56 -0
- package/dist/team/runtime.d.ts +122 -0
- package/dist/team/runtime.js +406 -0
- package/dist/team/subagent.d.ts +90 -0
- package/dist/team/subagent.js +294 -0
- package/dist/team/task-tool.d.ts +47 -0
- package/dist/team/task-tool.js +118 -0
- package/dist/team/types.d.ts +172 -0
- package/dist/team/types.js +13 -0
- package/dist/tools/index.d.ts +48 -2
- package/dist/tools/index.js +26 -1
- package/dist/ui/App.js +97 -13
- package/dist/ui/StatusBar.d.ts +21 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TeamPanel.d.ts +57 -0
- package/dist/ui/TeamPanel.js +134 -0
- package/dist/ui/Transcript.js +10 -0
- package/dist/ui/entries/TeamCard.d.ts +31 -0
- package/dist/ui/entries/TeamCard.js +39 -0
- package/dist/ui/glyphs.d.ts +10 -0
- package/dist/ui/glyphs.js +6 -0
- package/dist/ui/layout/AppShell.d.ts +11 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/overlays/HelpOverlay.js +12 -0
- package/dist/ui/transcript-text.js +20 -0
- package/dist/ui/use-startup-notices.d.ts +1 -1
- package/dist/ui/use-startup-notices.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeSubagentSpecs` — pure repair of whatever the model put in the `task`
|
|
3
|
+
* call (team-subagents §3.3.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* MODELLED DIRECTLY ON `human-input.ts::normalizeQuestions`, INCLUDING THE
|
|
8
|
+
* REASON IT EXISTS. The JSON Schema declares SHAPE, not POLICY: `type` and
|
|
9
|
+
* `required` stay in the schema because a `subagents` value that is not an array
|
|
10
|
+
* has nothing for a normalizer to repair. Every BOUND lives in the tool's
|
|
11
|
+
* `description` (which is what the model reads) and is enforced here (which is
|
|
12
|
+
* deterministic and unit-tested). Putting `maxItems: 10` in the schema instead
|
|
13
|
+
* would behave differently depending on whether `ajv` — an OPTIONAL dependency
|
|
14
|
+
* of `@aragon-agent/core` — happened to install: present, the executor rejects an
|
|
15
|
+
* eleven-spec call outright and these rules are dead code; absent, the same call
|
|
16
|
+
* sails straight through to here. One design, two behaviours, selected by
|
|
17
|
+
* whether an optional install step succeeded.
|
|
18
|
+
*
|
|
19
|
+
* NEVER THROWS. Zero survivors is the caller's single hard failure.
|
|
20
|
+
*/
|
|
21
|
+
import type { SubagentSpec } from './types.js';
|
|
22
|
+
export interface NormalizedSpecs {
|
|
23
|
+
specs: SubagentSpec[];
|
|
24
|
+
/** How many entries the model asked for, before dropping and capping. */
|
|
25
|
+
requested: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Repair and bound a raw `subagents` array.
|
|
29
|
+
*
|
|
30
|
+
* DROP FIRST, CAP SECOND. Capping first would let a blank entry consume a slot
|
|
31
|
+
* the model meant for real work — the fan-out would silently be one narrower
|
|
32
|
+
* than the model believed, and nothing would say so.
|
|
33
|
+
*
|
|
34
|
+
* @param raw Whatever arrived in `params.subagents`.
|
|
35
|
+
* @param max The resolved `team.maxSubagents`; further clamped to
|
|
36
|
+
* `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
|
|
37
|
+
* cannot raise the requirement's ceiling (R-g).
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeSubagentSpecs(raw: unknown, max: number): NormalizedSpecs;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `normalizeSubagentSpecs` — pure repair of whatever the model put in the `task`
|
|
3
|
+
* call (team-subagents §3.3.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* MODELLED DIRECTLY ON `human-input.ts::normalizeQuestions`, INCLUDING THE
|
|
8
|
+
* REASON IT EXISTS. The JSON Schema declares SHAPE, not POLICY: `type` and
|
|
9
|
+
* `required` stay in the schema because a `subagents` value that is not an array
|
|
10
|
+
* has nothing for a normalizer to repair. Every BOUND lives in the tool's
|
|
11
|
+
* `description` (which is what the model reads) and is enforced here (which is
|
|
12
|
+
* deterministic and unit-tested). Putting `maxItems: 10` in the schema instead
|
|
13
|
+
* would behave differently depending on whether `ajv` — an OPTIONAL dependency
|
|
14
|
+
* of `@aragon-agent/core` — happened to install: present, the executor rejects an
|
|
15
|
+
* eleven-spec call outright and these rules are dead code; absent, the same call
|
|
16
|
+
* sails straight through to here. One design, two behaviours, selected by
|
|
17
|
+
* whether an optional install step succeeded.
|
|
18
|
+
*
|
|
19
|
+
* NEVER THROWS. Zero survivors is the caller's single hard failure.
|
|
20
|
+
*/
|
|
21
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
22
|
+
function asString(value) {
|
|
23
|
+
return typeof value === 'string' ? value : '';
|
|
24
|
+
}
|
|
25
|
+
function clamp(text, maxChars) {
|
|
26
|
+
const trimmed = text.trim();
|
|
27
|
+
return trimmed.length > maxChars ? trimmed.slice(0, maxChars) : trimmed;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Slugify a label to `[a-z0-9-]{1,12}`. An empty result falls back to the
|
|
31
|
+
* positional default so a child is never nameless in the panel.
|
|
32
|
+
*/
|
|
33
|
+
function slugLabel(raw, index) {
|
|
34
|
+
const slug = asString(raw)
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
38
|
+
.replace(/^-+|-+$/g, '')
|
|
39
|
+
.slice(0, TEAM_LIMITS.labelChars);
|
|
40
|
+
return slug.length > 0 ? slug : `a${index + 1}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Make `label` unique within the dispatch, case-insensitively, by appending
|
|
44
|
+
* `-2`, `-3`, ...
|
|
45
|
+
*
|
|
46
|
+
* Two agents the user cannot tell apart in the panel is worse than an ugly
|
|
47
|
+
* label, and the bus addresses children BY label — a duplicate would make
|
|
48
|
+
* `team_send` ambiguous, which is a correctness problem rather than a cosmetic
|
|
49
|
+
* one.
|
|
50
|
+
*/
|
|
51
|
+
function dedupeLabel(label, taken) {
|
|
52
|
+
const base = label.toLowerCase();
|
|
53
|
+
if (!taken.has(base)) {
|
|
54
|
+
taken.add(base);
|
|
55
|
+
return label;
|
|
56
|
+
}
|
|
57
|
+
for (let n = 2; n < 100; n += 1) {
|
|
58
|
+
const suffix = `-${n}`;
|
|
59
|
+
const candidate = `${label.slice(0, TEAM_LIMITS.labelChars - suffix.length)}${suffix}`;
|
|
60
|
+
if (!taken.has(candidate.toLowerCase())) {
|
|
61
|
+
taken.add(candidate.toLowerCase());
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
taken.add(base);
|
|
66
|
+
return label;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Repair and bound a raw `subagents` array.
|
|
70
|
+
*
|
|
71
|
+
* DROP FIRST, CAP SECOND. Capping first would let a blank entry consume a slot
|
|
72
|
+
* the model meant for real work — the fan-out would silently be one narrower
|
|
73
|
+
* than the model believed, and nothing would say so.
|
|
74
|
+
*
|
|
75
|
+
* @param raw Whatever arrived in `params.subagents`.
|
|
76
|
+
* @param max The resolved `team.maxSubagents`; further clamped to
|
|
77
|
+
* `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
|
|
78
|
+
* cannot raise the requirement's ceiling (R-g).
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeSubagentSpecs(raw, max) {
|
|
81
|
+
if (!Array.isArray(raw))
|
|
82
|
+
return { specs: [], requested: 0 };
|
|
83
|
+
const requested = raw.length;
|
|
84
|
+
const kept = [];
|
|
85
|
+
const taken = new Set();
|
|
86
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
87
|
+
const item = raw[i];
|
|
88
|
+
if (!item || typeof item !== 'object')
|
|
89
|
+
continue;
|
|
90
|
+
const src = item;
|
|
91
|
+
const description = clamp(asString(src.description), TEAM_LIMITS.descriptionChars);
|
|
92
|
+
if (description.length === 0)
|
|
93
|
+
continue;
|
|
94
|
+
const prompt = clamp(asString(src.prompt), TEAM_LIMITS.promptChars);
|
|
95
|
+
if (prompt.length === 0)
|
|
96
|
+
continue;
|
|
97
|
+
kept.push({
|
|
98
|
+
label: dedupeLabel(slugLabel(src.label, kept.length), taken),
|
|
99
|
+
description,
|
|
100
|
+
prompt,
|
|
101
|
+
readOnly: src.readOnly === true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const ceiling = Math.max(1, Math.min(Math.floor(max) || 1, TEAM_LIMITS.hardMaxSubagents));
|
|
105
|
+
return { specs: kept.slice(0, ceiling), requested };
|
|
106
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `selectPanelRows` - which children the live roster shows (team-live-activity
|
|
3
|
+
* F-2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE DEFECT THIS REPLACES. `TeamPanel` took the first `panelMaxRows` runs by
|
|
8
|
+
* ARRAY INDEX, and the slot pool hands out indices from a shared cursor in
|
|
9
|
+
* order - so at `maxSubagents: 8, maxConcurrent: 3` the first five rows are the
|
|
10
|
+
* five that finished first and everything still working sits behind `+3 more`.
|
|
11
|
+
* The one thing the requirement asks the panel for - show that subagents are
|
|
12
|
+
* running - is the one thing it stopped doing at the fan-out widths the
|
|
13
|
+
* requirement explicitly permits.
|
|
14
|
+
*
|
|
15
|
+
* Pure: a function of a roster snapshot and a row cap, with no clock, no
|
|
16
|
+
* terminal and no `Agent` anywhere near it.
|
|
17
|
+
*/
|
|
18
|
+
import type { SubagentRun } from './types.js';
|
|
19
|
+
export interface PanelRows {
|
|
20
|
+
visible: SubagentRun[];
|
|
21
|
+
hiddenTotal: number;
|
|
22
|
+
hiddenRunning: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Rank the roster so that what is VISIBLE is what is HAPPENING.
|
|
26
|
+
*
|
|
27
|
+
* Ordering inside each group is chosen for ROW STABILITY, which is the real
|
|
28
|
+
* usability constraint here: the panel re-renders every 200 ms and a row that
|
|
29
|
+
* changes position on each tick is unreadable.
|
|
30
|
+
* - running: by `startedAt` ascending. `startedAt` changes exactly once in a
|
|
31
|
+
* run's life outside of its first assignment - on a cold-start retry (F-4),
|
|
32
|
+
* which re-stamps it (P2-4). A retried child therefore moves to the end of
|
|
33
|
+
* the running group, once, at the moment it visibly restarts. That is the
|
|
34
|
+
* correct reading of the ordering rule rather than an exception to it: the
|
|
35
|
+
* group is ordered by "how long has this been going", and a retry resets
|
|
36
|
+
* exactly that.
|
|
37
|
+
* - queued: original order; that IS the order the pool will start them in.
|
|
38
|
+
* - settled: most recently ended first, so the newest result is the one that
|
|
39
|
+
* stays on screen as others complete.
|
|
40
|
+
* A run changes position at most twice in its life - queued -> running ->
|
|
41
|
+
* settled - and each move carries real information.
|
|
42
|
+
*
|
|
43
|
+
* The final tiebreak is the original index. `Array.prototype.sort` is stable in
|
|
44
|
+
* every Node this package supports, so the tiebreak is not load-bearing; it is
|
|
45
|
+
* here so the ordering is total and the test can assert it without depending on
|
|
46
|
+
* that guarantee.
|
|
47
|
+
*/
|
|
48
|
+
export declare function selectPanelRows(runs: SubagentRun[], max: number): PanelRows;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `selectPanelRows` - which children the live roster shows (team-live-activity
|
|
3
|
+
* F-2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE DEFECT THIS REPLACES. `TeamPanel` took the first `panelMaxRows` runs by
|
|
8
|
+
* ARRAY INDEX, and the slot pool hands out indices from a shared cursor in
|
|
9
|
+
* order - so at `maxSubagents: 8, maxConcurrent: 3` the first five rows are the
|
|
10
|
+
* five that finished first and everything still working sits behind `+3 more`.
|
|
11
|
+
* The one thing the requirement asks the panel for - show that subagents are
|
|
12
|
+
* running - is the one thing it stopped doing at the fan-out widths the
|
|
13
|
+
* requirement explicitly permits.
|
|
14
|
+
*
|
|
15
|
+
* Pure: a function of a roster snapshot and a row cap, with no clock, no
|
|
16
|
+
* terminal and no `Agent` anywhere near it.
|
|
17
|
+
*/
|
|
18
|
+
const RUNNING = new Set(['starting', 'thinking', 'tool', 'waiting']);
|
|
19
|
+
const SETTLED = new Set(['done', 'failed', 'aborted']);
|
|
20
|
+
/** 0 running, 1 queued, 2 settled. */
|
|
21
|
+
function group(run) {
|
|
22
|
+
if (RUNNING.has(run.phase))
|
|
23
|
+
return 0;
|
|
24
|
+
if (SETTLED.has(run.phase))
|
|
25
|
+
return 2;
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rank the roster so that what is VISIBLE is what is HAPPENING.
|
|
30
|
+
*
|
|
31
|
+
* Ordering inside each group is chosen for ROW STABILITY, which is the real
|
|
32
|
+
* usability constraint here: the panel re-renders every 200 ms and a row that
|
|
33
|
+
* changes position on each tick is unreadable.
|
|
34
|
+
* - running: by `startedAt` ascending. `startedAt` changes exactly once in a
|
|
35
|
+
* run's life outside of its first assignment - on a cold-start retry (F-4),
|
|
36
|
+
* which re-stamps it (P2-4). A retried child therefore moves to the end of
|
|
37
|
+
* the running group, once, at the moment it visibly restarts. That is the
|
|
38
|
+
* correct reading of the ordering rule rather than an exception to it: the
|
|
39
|
+
* group is ordered by "how long has this been going", and a retry resets
|
|
40
|
+
* exactly that.
|
|
41
|
+
* - queued: original order; that IS the order the pool will start them in.
|
|
42
|
+
* - settled: most recently ended first, so the newest result is the one that
|
|
43
|
+
* stays on screen as others complete.
|
|
44
|
+
* A run changes position at most twice in its life - queued -> running ->
|
|
45
|
+
* settled - and each move carries real information.
|
|
46
|
+
*
|
|
47
|
+
* The final tiebreak is the original index. `Array.prototype.sort` is stable in
|
|
48
|
+
* every Node this package supports, so the tiebreak is not load-bearing; it is
|
|
49
|
+
* here so the ordering is total and the test can assert it without depending on
|
|
50
|
+
* that guarantee.
|
|
51
|
+
*/
|
|
52
|
+
export function selectPanelRows(runs, max) {
|
|
53
|
+
const ranked = runs
|
|
54
|
+
.map((run, index) => ({ run, index }))
|
|
55
|
+
.sort((a, b) => {
|
|
56
|
+
const ga = group(a.run);
|
|
57
|
+
const gb = group(b.run);
|
|
58
|
+
if (ga !== gb)
|
|
59
|
+
return ga - gb;
|
|
60
|
+
if (ga === 0)
|
|
61
|
+
return (a.run.startedAt ?? 0) - (b.run.startedAt ?? 0) || a.index - b.index;
|
|
62
|
+
if (ga === 2)
|
|
63
|
+
return (b.run.endedAt ?? 0) - (a.run.endedAt ?? 0) || a.index - b.index;
|
|
64
|
+
return a.index - b.index;
|
|
65
|
+
})
|
|
66
|
+
.map((e) => e.run);
|
|
67
|
+
const limit = Math.max(0, max);
|
|
68
|
+
const visible = ranked.slice(0, limit);
|
|
69
|
+
const hidden = ranked.slice(limit);
|
|
70
|
+
return {
|
|
71
|
+
visible,
|
|
72
|
+
hiddenTotal: hidden.length,
|
|
73
|
+
hiddenRunning: hidden.filter((r) => RUNNING.has(r.phase)).length,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two system-prompt blocks team mode adds (team-subagents §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope, and these
|
|
5
|
+
* strings also reach a model that may be running in a `cmd.exe` terminal.
|
|
6
|
+
*
|
|
7
|
+
* BOTH BLOCKS ARE SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
|
|
8
|
+
* preserves invariant I-8: with team mode off the prompt is BYTE-IDENTICAL to
|
|
9
|
+
* the pre-team output for a fixed tool array. An unconditional guidance line
|
|
10
|
+
* anywhere here would quietly break `--no-team`, `--no-skills` and the plan-mode
|
|
11
|
+
* snapshot all at once.
|
|
12
|
+
*
|
|
13
|
+
* `TEAM_BLOCK_VERSION` exists so a change to this wording is greppable from a
|
|
14
|
+
* behaviour report.
|
|
15
|
+
*/
|
|
16
|
+
import { TEAM_BLOCK_VERSION } from './limits.js';
|
|
17
|
+
export { TEAM_BLOCK_VERSION };
|
|
18
|
+
export interface TeamBlockParams {
|
|
19
|
+
maxSubagents: number;
|
|
20
|
+
maxConcurrent: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The lead's `<team_mode>` block. Under 1200 characters on purpose: it is paid
|
|
24
|
+
* for on EVERY turn of every team-enabled session, so anything that does not
|
|
25
|
+
* change a delegation decision does not belong in it.
|
|
26
|
+
*
|
|
27
|
+
* "Two to four is the usual size" is where the requirement's "shu liang zi shi
|
|
28
|
+
* ying" (adaptive count) actually lives. The ceiling is mechanical and enforced
|
|
29
|
+
* elsewhere; the CHOICE is the model's, and this paragraph is the only thing
|
|
30
|
+
* that informs it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildTeamBlock(params: TeamBlockParams): string;
|
|
33
|
+
export interface SubagentBlockParams {
|
|
34
|
+
label: string;
|
|
35
|
+
description: string;
|
|
36
|
+
/** Sibling labels, excluding this child's own. */
|
|
37
|
+
peers: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The child's `<subagent_role>` block.
|
|
41
|
+
*
|
|
42
|
+
* "YOUR FINAL MESSAGE IS THE ONLY THING YOUR LEAD SEES" is load-bearing rather
|
|
43
|
+
* than decorative: `SubagentRun.summary` is literally the last assistant text of
|
|
44
|
+
* the child's history, so a child that signs off with "Let me know if you want
|
|
45
|
+
* more detail!" produces a useless report entry and there is no mechanism that
|
|
46
|
+
* can repair it after the fact. Wording is the only enforcement available, so it
|
|
47
|
+
* is stated in imperative terms (R-12).
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildSubagentBlock(params: SubagentBlockParams): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two system-prompt blocks team mode adds (team-subagents §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope, and these
|
|
5
|
+
* strings also reach a model that may be running in a `cmd.exe` terminal.
|
|
6
|
+
*
|
|
7
|
+
* BOTH BLOCKS ARE SPLICED CONDITIONALLY by `buildSystemPrompt`, which is what
|
|
8
|
+
* preserves invariant I-8: with team mode off the prompt is BYTE-IDENTICAL to
|
|
9
|
+
* the pre-team output for a fixed tool array. An unconditional guidance line
|
|
10
|
+
* anywhere here would quietly break `--no-team`, `--no-skills` and the plan-mode
|
|
11
|
+
* snapshot all at once.
|
|
12
|
+
*
|
|
13
|
+
* `TEAM_BLOCK_VERSION` exists so a change to this wording is greppable from a
|
|
14
|
+
* behaviour report.
|
|
15
|
+
*/
|
|
16
|
+
import { TEAM_BLOCK_VERSION, TEAM_LIMITS } from './limits.js';
|
|
17
|
+
export { TEAM_BLOCK_VERSION };
|
|
18
|
+
/**
|
|
19
|
+
* The lead's `<team_mode>` block. Under 1200 characters on purpose: it is paid
|
|
20
|
+
* for on EVERY turn of every team-enabled session, so anything that does not
|
|
21
|
+
* change a delegation decision does not belong in it.
|
|
22
|
+
*
|
|
23
|
+
* "Two to four is the usual size" is where the requirement's "shu liang zi shi
|
|
24
|
+
* ying" (adaptive count) actually lives. The ceiling is mechanical and enforced
|
|
25
|
+
* elsewhere; the CHOICE is the model's, and this paragraph is the only thing
|
|
26
|
+
* that informs it.
|
|
27
|
+
*/
|
|
28
|
+
export function buildTeamBlock(params) {
|
|
29
|
+
const max = Math.max(1, Math.floor(params.maxSubagents));
|
|
30
|
+
const concurrent = Math.max(1, Math.min(Math.floor(params.maxConcurrent), max));
|
|
31
|
+
return [
|
|
32
|
+
'<team_mode>',
|
|
33
|
+
`You can delegate to parallel subagents with the task tool (up to ${max} per`,
|
|
34
|
+
`call, ${concurrent} running at once).`,
|
|
35
|
+
'',
|
|
36
|
+
'Delegate when: the work splits into 2+ parts that do not need each other\'s',
|
|
37
|
+
'output; several areas must be read or searched at once; a long build or test',
|
|
38
|
+
'run can proceed while another part is investigated.',
|
|
39
|
+
'',
|
|
40
|
+
'Do not delegate when: one part needs another part\'s result (do those in order',
|
|
41
|
+
'yourself); two parts would edit the same file; the whole job is a few edits.',
|
|
42
|
+
'One well-scoped subagent is better than four vague ones. Two to four is the',
|
|
43
|
+
'usual size; more than that is rarely faster.',
|
|
44
|
+
'',
|
|
45
|
+
'Each subagent starts with no memory of this conversation, so its prompt must',
|
|
46
|
+
'carry everything it needs: the goal, the files or areas it owns, and what to',
|
|
47
|
+
'report back. Give each one a disjoint set of files to write. Subagents cannot',
|
|
48
|
+
'dispatch further subagents and cannot ask the user anything.',
|
|
49
|
+
'',
|
|
50
|
+
'You get one combined report when they all finish. Read it before deciding what',
|
|
51
|
+
'to do next, and tell the user what the team found in your own words.',
|
|
52
|
+
'</team_mode>',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The child's `<subagent_role>` block.
|
|
57
|
+
*
|
|
58
|
+
* "YOUR FINAL MESSAGE IS THE ONLY THING YOUR LEAD SEES" is load-bearing rather
|
|
59
|
+
* than decorative: `SubagentRun.summary` is literally the last assistant text of
|
|
60
|
+
* the child's history, so a child that signs off with "Let me know if you want
|
|
61
|
+
* more detail!" produces a useless report entry and there is no mechanism that
|
|
62
|
+
* can repair it after the fact. Wording is the only enforcement available, so it
|
|
63
|
+
* is stated in imperative terms (R-12).
|
|
64
|
+
*/
|
|
65
|
+
export function buildSubagentBlock(params) {
|
|
66
|
+
const peers = params.peers.length > 0
|
|
67
|
+
? `Teammates: ${params.peers.join(', ')}. You cannot see their conversations and they cannot see yours.`
|
|
68
|
+
: 'You are the only subagent on this task.';
|
|
69
|
+
return [
|
|
70
|
+
'<subagent_role>',
|
|
71
|
+
`You are subagent "${params.label}" on a team. Your job: ${params.description}.`,
|
|
72
|
+
peers,
|
|
73
|
+
'',
|
|
74
|
+
'Work only on your own task. Do not edit files another subagent owns. You cannot',
|
|
75
|
+
'delegate further and there is no user to ask - if something is ambiguous, choose',
|
|
76
|
+
'the most reasonable reading, proceed, and say what you assumed.',
|
|
77
|
+
'',
|
|
78
|
+
`To reach a teammate use team_send (at most ${TEAM_LIMITS.messagesPerAgent} messages, one every`,
|
|
79
|
+
`${Math.round(TEAM_LIMITS.minSendIntervalMs / 1000)} seconds). Their replies arrive attached to your next tool result.`,
|
|
80
|
+
'Use team_wait only when you genuinely cannot continue without an answer.',
|
|
81
|
+
'',
|
|
82
|
+
'END WITH YOUR REPORT. Your final message is the only thing your lead sees. It',
|
|
83
|
+
'must state: what you did, what you found, every file you changed, and anything',
|
|
84
|
+
'still open. Be concrete and brief - no preamble, no restating this brief.',
|
|
85
|
+
'</subagent_role>',
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildDispatchReport` — the single text the lead receives back from `task`
|
|
3
|
+
* (team-subagents §3.7).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* BUDGETS ARE IN BYTES, NOT CHARACTERS (D-8 / I-9). `ToolExecutor` truncates
|
|
8
|
+
* combined tool text at 100 000 BYTES and appends `... [truncated]`; a 24 000
|
|
9
|
+
* character CJK report is 72 000 bytes, so a character-denominated cap would let
|
|
10
|
+
* the executor chop the report mid-section — and because the header is FIRST,
|
|
11
|
+
* what it would chop off is the tail, but a report assembled the other way round
|
|
12
|
+
* would lose its own failure count. Hence the second rule below.
|
|
13
|
+
*
|
|
14
|
+
* HEADER FIRST, ALWAYS. When the budget is exhausted the SUMMARIES are trimmed
|
|
15
|
+
* (longest first, each to a floor, with an explicit `[trimmed]` marker); the
|
|
16
|
+
* header, the per-agent status lines and the conflict warning are never dropped.
|
|
17
|
+
* A report that loses its own failure count is worse than one that loses prose.
|
|
18
|
+
*
|
|
19
|
+
* A FAILED CHILD NEVER FAILS THE DISPATCH (D-14). Partial results are the normal
|
|
20
|
+
* outcome of a fan-out; an all-or-nothing result would throw away four successes
|
|
21
|
+
* because of one HTTP 429.
|
|
22
|
+
*/
|
|
23
|
+
import type { ModelCost } from '@aragon-agent/core';
|
|
24
|
+
import type { DispatchOutcome, SubagentRun } from './types.js';
|
|
25
|
+
export interface ReportOptions {
|
|
26
|
+
/** The lead's model cost table, so the header can state real spend (R-5). */
|
|
27
|
+
cost?: ModelCost;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
|
|
31
|
+
*
|
|
32
|
+
* Slicing the buffer blindly at a byte offset produces a replacement character
|
|
33
|
+
* in the middle of a CJK report — the exact failure D-8 exists to prevent, just
|
|
34
|
+
* one layer lower down.
|
|
35
|
+
*/
|
|
36
|
+
export declare function truncateBytes(text: string, maxBytes: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Files written by more than one child.
|
|
39
|
+
*
|
|
40
|
+
* PHRASED AS A WARNING, NEVER AS A FACT. `filesTouched` only sees `write_file` /
|
|
41
|
+
* `edit_file`, so a child that writes through `bash` is invisible to it (R-2).
|
|
42
|
+
* "review before trusting either" is honest about that; "these two conflict"
|
|
43
|
+
* would not be.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findFileConflicts(runs: SubagentRun[]): Array<{
|
|
46
|
+
path: string;
|
|
47
|
+
labels: string[];
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Assemble the report.
|
|
51
|
+
*
|
|
52
|
+
* Trimming order: every summary starts clamped to `summaryMaxBytes`; if the
|
|
53
|
+
* whole document is still over `reportMaxBytes`, the LONGEST summary is halved
|
|
54
|
+
* repeatedly (never below `summaryFloorBytes`) until it fits or every summary is
|
|
55
|
+
* at the floor. Only then, as a last resort, are summaries dropped entirely —
|
|
56
|
+
* and even then the status lines survive, because they carry the failure count.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildDispatchReport(outcome: DispatchOutcome, options?: ReportOptions): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildDispatchReport` — the single text the lead receives back from `task`
|
|
3
|
+
* (team-subagents §3.7).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* BUDGETS ARE IN BYTES, NOT CHARACTERS (D-8 / I-9). `ToolExecutor` truncates
|
|
8
|
+
* combined tool text at 100 000 BYTES and appends `... [truncated]`; a 24 000
|
|
9
|
+
* character CJK report is 72 000 bytes, so a character-denominated cap would let
|
|
10
|
+
* the executor chop the report mid-section — and because the header is FIRST,
|
|
11
|
+
* what it would chop off is the tail, but a report assembled the other way round
|
|
12
|
+
* would lose its own failure count. Hence the second rule below.
|
|
13
|
+
*
|
|
14
|
+
* HEADER FIRST, ALWAYS. When the budget is exhausted the SUMMARIES are trimmed
|
|
15
|
+
* (longest first, each to a floor, with an explicit `[trimmed]` marker); the
|
|
16
|
+
* header, the per-agent status lines and the conflict warning are never dropped.
|
|
17
|
+
* A report that loses its own failure count is worse than one that loses prose.
|
|
18
|
+
*
|
|
19
|
+
* A FAILED CHILD NEVER FAILS THE DISPATCH (D-14). Partial results are the normal
|
|
20
|
+
* outcome of a fan-out; an all-or-nothing result would throw away four successes
|
|
21
|
+
* because of one HTTP 429.
|
|
22
|
+
*/
|
|
23
|
+
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
24
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
25
|
+
function byteLength(text) {
|
|
26
|
+
return Buffer.byteLength(text, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
|
|
30
|
+
*
|
|
31
|
+
* Slicing the buffer blindly at a byte offset produces a replacement character
|
|
32
|
+
* in the middle of a CJK report — the exact failure D-8 exists to prevent, just
|
|
33
|
+
* one layer lower down.
|
|
34
|
+
*/
|
|
35
|
+
export function truncateBytes(text, maxBytes) {
|
|
36
|
+
if (maxBytes <= 0)
|
|
37
|
+
return '';
|
|
38
|
+
if (byteLength(text) <= maxBytes)
|
|
39
|
+
return text;
|
|
40
|
+
const buf = Buffer.from(text, 'utf8');
|
|
41
|
+
let end = Math.min(maxBytes, buf.length);
|
|
42
|
+
// A continuation byte is 0b10xxxxxx; back off until `end` starts a sequence.
|
|
43
|
+
while (end > 0 && (buf[end] !== undefined) && (buf[end] & 0xc0) === 0x80)
|
|
44
|
+
end -= 1;
|
|
45
|
+
return buf.subarray(0, end).toString('utf8');
|
|
46
|
+
}
|
|
47
|
+
/** `ok` / `aborted` / `failed: <reason>` — the bracketed badge on a section head. */
|
|
48
|
+
function statusBadge(run) {
|
|
49
|
+
if (run.phase === 'aborted')
|
|
50
|
+
return '[aborted]';
|
|
51
|
+
if (run.phase === 'failed' || run.error) {
|
|
52
|
+
return `[failed: ${run.error ?? 'run produced no output'}]`;
|
|
53
|
+
}
|
|
54
|
+
if (run.truncated)
|
|
55
|
+
return '[stopped: turn cap reached]';
|
|
56
|
+
return '[ok]';
|
|
57
|
+
}
|
|
58
|
+
function elapsed(run) {
|
|
59
|
+
if (run.startedAt === undefined)
|
|
60
|
+
return '0.0s';
|
|
61
|
+
return formatDuration(Math.max(0, (run.endedAt ?? run.startedAt) - run.startedAt));
|
|
62
|
+
}
|
|
63
|
+
function isOk(run) {
|
|
64
|
+
return run.phase === 'done' && !run.error;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Files written by more than one child.
|
|
68
|
+
*
|
|
69
|
+
* PHRASED AS A WARNING, NEVER AS A FACT. `filesTouched` only sees `write_file` /
|
|
70
|
+
* `edit_file`, so a child that writes through `bash` is invisible to it (R-2).
|
|
71
|
+
* "review before trusting either" is honest about that; "these two conflict"
|
|
72
|
+
* would not be.
|
|
73
|
+
*/
|
|
74
|
+
export function findFileConflicts(runs) {
|
|
75
|
+
const owners = new Map();
|
|
76
|
+
for (const run of runs) {
|
|
77
|
+
for (const path of run.filesTouched) {
|
|
78
|
+
const list = owners.get(path) ?? [];
|
|
79
|
+
if (!list.includes(run.label))
|
|
80
|
+
list.push(run.label);
|
|
81
|
+
owners.set(path, list);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const [path, labels] of owners) {
|
|
86
|
+
if (labels.length > 1)
|
|
87
|
+
out.push({ path, labels });
|
|
88
|
+
}
|
|
89
|
+
return out.sort((a, b) => a.path.localeCompare(b.path, 'en'));
|
|
90
|
+
}
|
|
91
|
+
function buildHeader(outcome, options) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
const durationMs = Math.max(0, outcome.endedAt - outcome.startedAt);
|
|
94
|
+
if (outcome.aborted) {
|
|
95
|
+
// FIRST LINE on an abort, so the model cannot present partial work as
|
|
96
|
+
// complete without having read past it.
|
|
97
|
+
lines.push(`Team dispatch ABORTED after ${formatDuration(durationMs)}; partial results below.`);
|
|
98
|
+
}
|
|
99
|
+
const ok = outcome.runs.filter(isOk).length;
|
|
100
|
+
const failed = outcome.runs.length - ok;
|
|
101
|
+
const counts = `${ok} ok, ${failed} failed`;
|
|
102
|
+
lines.push(outcome.requested > outcome.runs.length
|
|
103
|
+
? `Team dispatch: ran ${outcome.runs.length} of ${outcome.requested} requested subagents ` +
|
|
104
|
+
`(${counts}) in ${formatDuration(durationMs)}.`
|
|
105
|
+
: `Team dispatch: ${outcome.runs.length} of ${outcome.runs.length} subagents finished ` +
|
|
106
|
+
`(${counts}) in ${formatDuration(durationMs)}.`);
|
|
107
|
+
lines.push(`Tokens: in ${formatTokens(outcome.usage.inputTokens)}, out ` +
|
|
108
|
+
`${formatTokens(outcome.usage.outputTokens)}. Cost: ${formatCost(computeCost(outcome.usage, options.cost))}.`);
|
|
109
|
+
for (const conflict of findFileConflicts(outcome.runs)) {
|
|
110
|
+
const who = conflict.labels.map((l) => `"${l}"`).join(' and ');
|
|
111
|
+
lines.push(`WARNING: agents ${who} both wrote ${conflict.path} - review before trusting either.`);
|
|
112
|
+
}
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
115
|
+
function buildSection(run) {
|
|
116
|
+
// `retried Nx` on the head line, which is never trimmed. 11 bytes, and it is
|
|
117
|
+
// the only place the lead learns that a child's first attempt died before it
|
|
118
|
+
// did anything (F-4).
|
|
119
|
+
const retried = run.retries ? `, retried ${run.retries}x` : '';
|
|
120
|
+
const head = [
|
|
121
|
+
`### ${run.label} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
|
|
122
|
+
`${run.turns} turns, ${run.toolCalls} tools${retried}`,
|
|
123
|
+
];
|
|
124
|
+
if (run.filesTouched.length > 0)
|
|
125
|
+
head.push(`files: ${run.filesTouched.join(', ')}`);
|
|
126
|
+
// A child that tried to reach a teammate and could not is a child whose brief
|
|
127
|
+
// had a dependency the lead should not have split (F-3). One line, in a
|
|
128
|
+
// section that is trimmed last, and only when it actually happened.
|
|
129
|
+
if (run.blockedWaits)
|
|
130
|
+
head.push(`blocked waits: ${run.blockedWaits} (no teammate could answer)`);
|
|
131
|
+
const summary = (run.summary ?? '').trim();
|
|
132
|
+
return { head, summary: summary.length > 0 ? summary : '(no summary)' };
|
|
133
|
+
}
|
|
134
|
+
function buildLeadMail(outcome) {
|
|
135
|
+
if (outcome.leadMail.length === 0)
|
|
136
|
+
return [];
|
|
137
|
+
const lines = ['### Messages to you'];
|
|
138
|
+
for (const m of outcome.leadMail) {
|
|
139
|
+
const at = Math.max(0, Math.round((m.at - outcome.startedAt) / 1000));
|
|
140
|
+
lines.push(`[${m.from} -> lead] "${m.subject}" (${at}s in)`);
|
|
141
|
+
if (m.body.length > 0)
|
|
142
|
+
lines.push(m.body);
|
|
143
|
+
}
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Assemble the report.
|
|
148
|
+
*
|
|
149
|
+
* Trimming order: every summary starts clamped to `summaryMaxBytes`; if the
|
|
150
|
+
* whole document is still over `reportMaxBytes`, the LONGEST summary is halved
|
|
151
|
+
* repeatedly (never below `summaryFloorBytes`) until it fits or every summary is
|
|
152
|
+
* at the floor. Only then, as a last resort, are summaries dropped entirely —
|
|
153
|
+
* and even then the status lines survive, because they carry the failure count.
|
|
154
|
+
*/
|
|
155
|
+
export function buildDispatchReport(outcome, options = {}) {
|
|
156
|
+
const header = buildHeader(outcome, options);
|
|
157
|
+
const sections = outcome.runs.map(buildSection);
|
|
158
|
+
const mail = buildLeadMail(outcome);
|
|
159
|
+
const summaries = sections.map((s) => truncateBytes(s.summary, TEAM_LIMITS.summaryMaxBytes));
|
|
160
|
+
const trimmed = summaries.map((s, i) => s.length < sections[i].summary.length);
|
|
161
|
+
const render = (bodies) => {
|
|
162
|
+
const blocks = [header.join('\n')];
|
|
163
|
+
sections.forEach((section, i) => {
|
|
164
|
+
const body = bodies[i];
|
|
165
|
+
const lines = [...section.head];
|
|
166
|
+
if (body !== null && body !== undefined) {
|
|
167
|
+
lines.push(trimmed[i] ? `${body}\n[trimmed]` : body);
|
|
168
|
+
}
|
|
169
|
+
blocks.push(lines.join('\n'));
|
|
170
|
+
});
|
|
171
|
+
if (mail.length > 0)
|
|
172
|
+
blocks.push(mail.join('\n'));
|
|
173
|
+
return blocks.join('\n\n');
|
|
174
|
+
};
|
|
175
|
+
let bodies = [...summaries];
|
|
176
|
+
let out = render(bodies);
|
|
177
|
+
let guard = 0;
|
|
178
|
+
while (byteLength(out) > TEAM_LIMITS.reportMaxBytes && guard < 200) {
|
|
179
|
+
guard += 1;
|
|
180
|
+
let longest = -1;
|
|
181
|
+
let longestBytes = TEAM_LIMITS.summaryFloorBytes;
|
|
182
|
+
bodies.forEach((body, i) => {
|
|
183
|
+
if (body === null)
|
|
184
|
+
return;
|
|
185
|
+
const size = byteLength(body);
|
|
186
|
+
if (size > longestBytes) {
|
|
187
|
+
longest = i;
|
|
188
|
+
longestBytes = size;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (longest < 0)
|
|
192
|
+
break; // Every summary is at the floor.
|
|
193
|
+
const next = Math.max(TEAM_LIMITS.summaryFloorBytes, Math.floor(longestBytes / 2));
|
|
194
|
+
bodies[longest] = truncateBytes(bodies[longest], next);
|
|
195
|
+
trimmed[longest] = true;
|
|
196
|
+
out = render(bodies);
|
|
197
|
+
}
|
|
198
|
+
if (byteLength(out) > TEAM_LIMITS.reportMaxBytes) {
|
|
199
|
+
// Last resort. The header, the status lines and the conflict warning are
|
|
200
|
+
// still here, which is the whole point of assembling them separately.
|
|
201
|
+
bodies = bodies.map(() => null);
|
|
202
|
+
out = render(bodies);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|