@aragon-agent/cli 0.6.40 → 0.6.43
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 +2632 -2589
- package/README.md +3720 -3706
- package/dist/agent/controller.d.ts +36 -5
- package/dist/agent/controller.js +112 -33
- package/dist/agent/headless.js +6 -2
- package/dist/agent/system-prompt.d.ts +19 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/commands/builtins.js +16 -182
- package/dist/commands/subagent-command.d.ts +21 -16
- package/dist/commands/subagent-command.js +22 -225
- package/dist/commands/team-command.d.ts +49 -0
- package/dist/commands/team-command.js +259 -0
- package/dist/config/model-kits.d.ts +2 -1
- package/dist/config/store.js +3 -2
- package/dist/exec/runner.js +2 -1
- package/dist/team/bus.d.ts +4 -0
- package/dist/team/bus.js +12 -2
- package/dist/team/comm-tools.js +3 -0
- package/dist/team/dispatch-gate.d.ts +86 -0
- package/dist/team/dispatch-gate.js +75 -0
- package/dist/team/forced-mode.d.ts +116 -0
- package/dist/team/forced-mode.js +156 -0
- package/dist/team/forced-prompt.d.ts +48 -0
- package/dist/team/forced-prompt.js +84 -0
- package/dist/team/limits.d.ts +41 -1
- package/dist/team/limits.js +49 -1
- package/dist/team/model.d.ts +22 -6
- package/dist/team/model.js +19 -5
- package/dist/team/overseer.d.ts +4 -0
- package/dist/team/overseer.js +38 -6
- package/dist/team/prompt.js +3 -3
- package/dist/team/report.d.ts +21 -0
- package/dist/team/report.js +40 -9
- package/dist/team/runtime.d.ts +3 -0
- package/dist/team/runtime.js +61 -5
- package/dist/team/subagent.js +25 -7
- package/dist/ui/App.js +140 -20
- package/dist/ui/StatusBar.d.ts +14 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/layout/status-layout.d.ts +17 -0
- package/dist/ui/layout/status-layout.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +8 -4
- package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
- package/dist/ui/overlays/SettingsScreen.js +17 -7
- package/dist/ui/overlays/TeamFanoutPage.d.ts +17 -1
- package/dist/ui/overlays/TeamFanoutPage.js +23 -11
- package/dist/ui/overlays/kit-settings-state.js +3 -2
- package/dist/ui/overlays/model-profile-state.d.ts +1 -1
- package/dist/ui/overlays/team-fanout-state.d.ts +46 -13
- package/dist/ui/overlays/team-fanout-state.js +78 -20
- package/package.json +81 -81
package/dist/team/limits.d.ts
CHANGED
|
@@ -265,4 +265,44 @@ export declare const TEAM_SUBAGENT_TOOL_NAMES: ReadonlySet<string>;
|
|
|
265
265
|
* Bumped whenever the wording of either prompt block changes, so a behaviour
|
|
266
266
|
* report can be tied to a block revision with one grep.
|
|
267
267
|
*/
|
|
268
|
-
export declare const TEAM_BLOCK_VERSION = "
|
|
268
|
+
export declare const TEAM_BLOCK_VERSION = "v3-2026-10";
|
|
269
|
+
/**
|
|
270
|
+
* The version string on `<forced_team_mode>`'s first line.
|
|
271
|
+
*
|
|
272
|
+
* The block's words are a CONTRACT with the model, so a behaviour report ("it
|
|
273
|
+
* still did the work itself") has to be tieable to one revision. Kept beside
|
|
274
|
+
* `TEAM_BLOCK_VERSION` rather than reusing it: the two blocks answer different
|
|
275
|
+
* questions ("can dispatch" versus "must dispatch") and a shared number would
|
|
276
|
+
* name neither revision.
|
|
277
|
+
*/
|
|
278
|
+
export declare const FORCED_TEAM_BLOCK_VERSION = "v1-2026-10";
|
|
279
|
+
/**
|
|
280
|
+
* Length ceiling of the `<forced_team_mode>` text.
|
|
281
|
+
*
|
|
282
|
+
* THE FIRST BLOCK LENGTH IN THIS SUBSYSTEM THAT A TEST ASSERTS: the `<team_mode>`
|
|
283
|
+
* block's 1200 is a comment in `team/prompt.ts`, and unifying the two is a
|
|
284
|
+
* separate small refactor (deliberately out of scope here). Looser than 1200
|
|
285
|
+
* because this block is only ever paid for while the session is ARMED, and it
|
|
286
|
+
* carries three layers of instruction rather than one.
|
|
287
|
+
*/
|
|
288
|
+
export declare const FORCED_TEAM_BLOCK_MAX_CHARS = 1400;
|
|
289
|
+
/**
|
|
290
|
+
* Fallback nudges per user turn. ONE, and not configurable.
|
|
291
|
+
*
|
|
292
|
+
* The gate exists because the first two layers (the directive and the prompt
|
|
293
|
+
* block) both depend on the model cooperating. A second nudge would double the
|
|
294
|
+
* worst case of a model that is simply not going to dispatch - at N children per
|
|
295
|
+
* attempt - and D-8's whole argument is that the mechanical layer must be
|
|
296
|
+
* bounded by something other than the model's willingness.
|
|
297
|
+
*/
|
|
298
|
+
export declare const FORCED_NUDGE_LIMIT = 1;
|
|
299
|
+
/**
|
|
300
|
+
* Grace window before the fallback nudge fires, in milliseconds.
|
|
301
|
+
*
|
|
302
|
+
* SAME ORDER AS THE TODO FOLLOW-THROUGH GRACE, for a structural reason rather
|
|
303
|
+
* than a stylistic one: both continuations share ONE `followTimer` in `App`
|
|
304
|
+
* (I-9), so "at most one armed continuation" is only explicable while the two
|
|
305
|
+
* numbers stay in the same order of magnitude. **Changing todo's grace means
|
|
306
|
+
* re-reading this constant.**
|
|
307
|
+
*/
|
|
308
|
+
export declare const FORCED_NUDGE_GRACE_MS = 3000;
|
package/dist/team/limits.js
CHANGED
|
@@ -279,4 +279,52 @@ export const TEAM_SUBAGENT_TOOL_NAMES = new Set(['team_send', 'team_wait']);
|
|
|
279
279
|
* Bumped whenever the wording of either prompt block changes, so a behaviour
|
|
280
280
|
* report can be tied to a block revision with one grep.
|
|
281
281
|
*/
|
|
282
|
-
export const TEAM_BLOCK_VERSION = '
|
|
282
|
+
export const TEAM_BLOCK_VERSION = 'v3-2026-10';
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Forced team mode (`/team <brief>`, team-command-forced-dispatch §6.5).
|
|
285
|
+
//
|
|
286
|
+
// FOUR STRUCTURAL NUMBERS, here rather than in the modules that read them for
|
|
287
|
+
// the reason this file's header records: none of them is a user preference, and
|
|
288
|
+
// a `config.json` key for any of them would make "the model ignored the
|
|
289
|
+
// instruction" tunable by the person the instruction exists to protect.
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
/**
|
|
292
|
+
* The version string on `<forced_team_mode>`'s first line.
|
|
293
|
+
*
|
|
294
|
+
* The block's words are a CONTRACT with the model, so a behaviour report ("it
|
|
295
|
+
* still did the work itself") has to be tieable to one revision. Kept beside
|
|
296
|
+
* `TEAM_BLOCK_VERSION` rather than reusing it: the two blocks answer different
|
|
297
|
+
* questions ("can dispatch" versus "must dispatch") and a shared number would
|
|
298
|
+
* name neither revision.
|
|
299
|
+
*/
|
|
300
|
+
export const FORCED_TEAM_BLOCK_VERSION = 'v1-2026-10';
|
|
301
|
+
/**
|
|
302
|
+
* Length ceiling of the `<forced_team_mode>` text.
|
|
303
|
+
*
|
|
304
|
+
* THE FIRST BLOCK LENGTH IN THIS SUBSYSTEM THAT A TEST ASSERTS: the `<team_mode>`
|
|
305
|
+
* block's 1200 is a comment in `team/prompt.ts`, and unifying the two is a
|
|
306
|
+
* separate small refactor (deliberately out of scope here). Looser than 1200
|
|
307
|
+
* because this block is only ever paid for while the session is ARMED, and it
|
|
308
|
+
* carries three layers of instruction rather than one.
|
|
309
|
+
*/
|
|
310
|
+
export const FORCED_TEAM_BLOCK_MAX_CHARS = 1400;
|
|
311
|
+
/**
|
|
312
|
+
* Fallback nudges per user turn. ONE, and not configurable.
|
|
313
|
+
*
|
|
314
|
+
* The gate exists because the first two layers (the directive and the prompt
|
|
315
|
+
* block) both depend on the model cooperating. A second nudge would double the
|
|
316
|
+
* worst case of a model that is simply not going to dispatch - at N children per
|
|
317
|
+
* attempt - and D-8's whole argument is that the mechanical layer must be
|
|
318
|
+
* bounded by something other than the model's willingness.
|
|
319
|
+
*/
|
|
320
|
+
export const FORCED_NUDGE_LIMIT = 1;
|
|
321
|
+
/**
|
|
322
|
+
* Grace window before the fallback nudge fires, in milliseconds.
|
|
323
|
+
*
|
|
324
|
+
* SAME ORDER AS THE TODO FOLLOW-THROUGH GRACE, for a structural reason rather
|
|
325
|
+
* than a stylistic one: both continuations share ONE `followTimer` in `App`
|
|
326
|
+
* (I-9), so "at most one armed continuation" is only explicable while the two
|
|
327
|
+
* numbers stay in the same order of magnitude. **Changing todo's grace means
|
|
328
|
+
* re-reading this constant.**
|
|
329
|
+
*/
|
|
330
|
+
export const FORCED_NUDGE_GRACE_MS = 3000;
|
package/dist/team/model.d.ts
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
6
|
*
|
|
7
7
|
* EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
|
|
8
|
-
* ELSE — `createSubagent`, the `task` report header
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* ELSE — `createSubagent`, the `task` report header and the settings root row —
|
|
9
|
+
* following the `resolveFastTier` precedent one directory over: three call
|
|
10
|
+
* sites each recomputing "which model do children run on?" from raw
|
|
11
11
|
* config keys is exactly how the report ends up naming a model the children
|
|
12
12
|
* silently fell back from.
|
|
13
13
|
*
|
|
14
14
|
* IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
|
|
15
15
|
* the fast tier, inherited here): `team.model`, the fast tier and the profile
|
|
16
|
-
* library are all LIVE keys a settings save
|
|
17
|
-
* rewrite mid-session. A resolution captured at construction would be a
|
|
16
|
+
* library are all LIVE keys a settings save (the `Subagent model` row), `/fast
|
|
17
|
+
* off` or a kit can rewrite mid-session. A resolution captured at construction would be a
|
|
18
18
|
* resolution describing a session that no longer exists.
|
|
19
19
|
*
|
|
20
20
|
* A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* went stale. The reason is disclosed, never swallowed: the report header and
|
|
24
24
|
* the settings summary line both render it.
|
|
25
25
|
*/
|
|
26
|
-
import type { ModelRef, ThinkingLevel } from '@aragon-agent/core';
|
|
26
|
+
import type { ModelCost, ModelRef, ThinkingLevel, TokenUsage } from '@aragon-agent/core';
|
|
27
27
|
import type { ModelProfile, ModelRole } from '../config/model-profiles.js';
|
|
28
28
|
import type { CliConfig } from '../config/schema.js';
|
|
29
29
|
/** Which model children run on. `main` is the default and the historical behavior. */
|
|
@@ -53,6 +53,22 @@ export interface SubagentModelResolution {
|
|
|
53
53
|
/** Present only when `effective !== mode`. */
|
|
54
54
|
fallbackReason?: SubagentFallbackReason;
|
|
55
55
|
}
|
|
56
|
+
/** What `priceTeamUsage` needs from the controller: the lead's table and the lookup. */
|
|
57
|
+
export interface TeamPriceBook {
|
|
58
|
+
/** The lead's own price table (`main` mode prices at exactly this). */
|
|
59
|
+
mainCost: ModelCost | undefined;
|
|
60
|
+
isPriced: (ref: ModelRef) => boolean;
|
|
61
|
+
costFor: (ref: ModelRef) => ModelCost | undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* USD for one child `usage` event, at the model the children ACTUALLY ran.
|
|
65
|
+
*
|
|
66
|
+
* The status bar and the dispatch report must agree, so both price through the same
|
|
67
|
+
* resolution: under the default `main` mode this is the lead's table (byte parity),
|
|
68
|
+
* otherwise the fast / profile model's. A model no table prices contributes ZERO rather
|
|
69
|
+
* than a fabricated figure (C-11 / RV-4), where the report says `pricing unknown`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function priceTeamUsage(resolution: SubagentModelResolution, usage: TokenUsage, book: TeamPriceBook): number;
|
|
56
72
|
/** The `AgentController.hasApiKey` shape, injected so there is one key resolver. */
|
|
57
73
|
export type HasApiKey = (providerId: string, role?: ModelRole) => boolean;
|
|
58
74
|
/** The lead-layer key resolver `resolveSubagentKey` falls back to. */
|
package/dist/team/model.js
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
6
|
*
|
|
7
7
|
* EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
|
|
8
|
-
* ELSE — `createSubagent`, the `task` report header
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* ELSE — `createSubagent`, the `task` report header and the settings root row —
|
|
9
|
+
* following the `resolveFastTier` precedent one directory over: three call
|
|
10
|
+
* sites each recomputing "which model do children run on?" from raw
|
|
11
11
|
* config keys is exactly how the report ends up naming a model the children
|
|
12
12
|
* silently fell back from.
|
|
13
13
|
*
|
|
14
14
|
* IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
|
|
15
15
|
* the fast tier, inherited here): `team.model`, the fast tier and the profile
|
|
16
|
-
* library are all LIVE keys a settings save
|
|
17
|
-
* rewrite mid-session. A resolution captured at construction would be a
|
|
16
|
+
* library are all LIVE keys a settings save (the `Subagent model` row), `/fast
|
|
17
|
+
* off` or a kit can rewrite mid-session. A resolution captured at construction would be a
|
|
18
18
|
* resolution describing a session that no longer exists.
|
|
19
19
|
*
|
|
20
20
|
* A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
|
|
@@ -23,7 +23,21 @@
|
|
|
23
23
|
* went stale. The reason is disclosed, never swallowed: the report header and
|
|
24
24
|
* the settings summary line both render it.
|
|
25
25
|
*/
|
|
26
|
+
import { computeCost } from '../agent/usage.js';
|
|
26
27
|
import { resolveFastTier } from '../fast/resolve.js';
|
|
28
|
+
/**
|
|
29
|
+
* USD for one child `usage` event, at the model the children ACTUALLY ran.
|
|
30
|
+
*
|
|
31
|
+
* The status bar and the dispatch report must agree, so both price through the same
|
|
32
|
+
* resolution: under the default `main` mode this is the lead's table (byte parity),
|
|
33
|
+
* otherwise the fast / profile model's. A model no table prices contributes ZERO rather
|
|
34
|
+
* than a fabricated figure (C-11 / RV-4), where the report says `pricing unknown`.
|
|
35
|
+
*/
|
|
36
|
+
export function priceTeamUsage(resolution, usage, book) {
|
|
37
|
+
if (resolution.effective === 'main')
|
|
38
|
+
return computeCost(usage, book.mainCost);
|
|
39
|
+
return book.isPriced(resolution.ref) ? computeCost(usage, book.costFor(resolution.ref)) : 0;
|
|
40
|
+
}
|
|
27
41
|
function mainRef(config) {
|
|
28
42
|
return {
|
|
29
43
|
providerId: config.provider,
|
package/dist/team/overseer.d.ts
CHANGED
|
@@ -293,5 +293,9 @@ export declare class TeamOverseer {
|
|
|
293
293
|
* THE NOTE TELLS THE FRESH CHILD WHAT THE DEAD ONE DID (`filesTouched` is
|
|
294
294
|
* best-effort, the same caveat the report's conflict warning carries): a
|
|
295
295
|
* replacement that rewrites a half-written file blind is not a recovery.
|
|
296
|
+
*
|
|
297
|
+
* ONLY THE VARIABLE MIDDLE IS EVER CLAMPED. The closing sentence and the blank line
|
|
298
|
+
* that separates the note from the brief are what make the note a note; clamping the
|
|
299
|
+
* whole string from the tail glued an ellipsis onto the first word of the brief.
|
|
296
300
|
*/
|
|
297
301
|
export declare function buildReplacementPrompt(spec: SubagentSpec, decision: OverseerDecision, previousRun: SubagentRun): string;
|
package/dist/team/overseer.js
CHANGED
|
@@ -629,8 +629,33 @@ export class TeamOverseer {
|
|
|
629
629
|
// ---------------------------------------------------------------------------
|
|
630
630
|
// Replacement brief
|
|
631
631
|
// ---------------------------------------------------------------------------
|
|
632
|
-
/**
|
|
633
|
-
|
|
632
|
+
/**
|
|
633
|
+
* Headroom the amended brief keeps over the original prompt's clamp. Sized for the
|
|
634
|
+
* LARGEST legal note: the head, a reason (`overseerReasonChars`), the file list
|
|
635
|
+
* (`REPLACEMENT_FILES_CHARS`), a guidance (`overseerGuidanceChars`) and the closing
|
|
636
|
+
* sentence. A cap below that sum silently chops a supervisor's own words.
|
|
637
|
+
*/
|
|
638
|
+
const REPLACEMENT_NOTE_CHARS = 1_900;
|
|
639
|
+
/** The file list gets a small slice of its own: 30 long paths must not crowd out guidance. */
|
|
640
|
+
const REPLACEMENT_FILES_CHARS = 240;
|
|
641
|
+
/** The last thing a replacement reads before its brief; never clamped away. */
|
|
642
|
+
const REPLACEMENT_NOTE_TAIL = ' Verify rather than assume prior work is complete.\n\n';
|
|
643
|
+
/** `a, b, +3 more`: whole paths only, never a path cut in half. */
|
|
644
|
+
function listFiles(files, max) {
|
|
645
|
+
const kept = [];
|
|
646
|
+
let used = 0;
|
|
647
|
+
for (const file of files) {
|
|
648
|
+
const next = used + file.length + (kept.length > 0 ? 2 : 0);
|
|
649
|
+
if (next > max)
|
|
650
|
+
break;
|
|
651
|
+
kept.push(file);
|
|
652
|
+
used = next;
|
|
653
|
+
}
|
|
654
|
+
const hidden = files.length - kept.length;
|
|
655
|
+
if (kept.length === 0)
|
|
656
|
+
return clampChars(files[0] ?? '', max);
|
|
657
|
+
return hidden > 0 ? `${kept.join(', ')}, +${hidden} more` : kept.join(', ');
|
|
658
|
+
}
|
|
634
659
|
/**
|
|
635
660
|
* Prepend the supervisor's note to a spec's prompt, bounding the TOTAL to
|
|
636
661
|
* `TEAM_LIMITS.promptChars` so the replacement fits exactly what a
|
|
@@ -639,17 +664,24 @@ const REPLACEMENT_NOTE_CHARS = 800;
|
|
|
639
664
|
* THE NOTE TELLS THE FRESH CHILD WHAT THE DEAD ONE DID (`filesTouched` is
|
|
640
665
|
* best-effort, the same caveat the report's conflict warning carries): a
|
|
641
666
|
* replacement that rewrites a half-written file blind is not a recovery.
|
|
667
|
+
*
|
|
668
|
+
* ONLY THE VARIABLE MIDDLE IS EVER CLAMPED. The closing sentence and the blank line
|
|
669
|
+
* that separates the note from the brief are what make the note a note; clamping the
|
|
670
|
+
* whole string from the tail glued an ellipsis onto the first word of the brief.
|
|
642
671
|
*/
|
|
643
672
|
export function buildReplacementPrompt(spec, decision, previousRun) {
|
|
673
|
+
const head = `[supervisor note] A previous attempt at this task ran ${previousRun.turns} turns ` +
|
|
674
|
+
'and was replaced. Reason: ';
|
|
644
675
|
const files = previousRun.filesTouched.length > 0
|
|
645
|
-
? ` It already wrote to: ${previousRun.filesTouched
|
|
676
|
+
? ` It already wrote to: ${listFiles(previousRun.filesTouched, REPLACEMENT_FILES_CHARS)}.`
|
|
646
677
|
: '';
|
|
647
678
|
const guidance = decision.action === 'replace' && decision.guidance !== undefined
|
|
648
679
|
? ` Guidance: ${decision.guidance}`
|
|
649
680
|
: '';
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
681
|
+
const fixedChars = head.length + REPLACEMENT_NOTE_TAIL.length;
|
|
682
|
+
const middleBudget = Math.max(0, REPLACEMENT_NOTE_CHARS - fixedChars);
|
|
683
|
+
const middle = clampChars(`${decision.reason}.${files}${guidance}`, middleBudget);
|
|
684
|
+
const note = `${head}${middle}${REPLACEMENT_NOTE_TAIL}`;
|
|
653
685
|
const originalBudget = Math.max(0, TEAM_LIMITS.promptChars - note.length);
|
|
654
686
|
return `${note}${spec.prompt.slice(0, originalBudget)}`;
|
|
655
687
|
}
|
package/dist/team/prompt.js
CHANGED
|
@@ -48,9 +48,9 @@ export function buildTeamBlock(params) {
|
|
|
48
48
|
'',
|
|
49
49
|
'Each subagent starts with no memory of this conversation, so its prompt must',
|
|
50
50
|
'carry everything it needs: the goal, the files or areas it owns, and what to',
|
|
51
|
-
'report back. Give each one a disjoint set of files to write.
|
|
52
|
-
'
|
|
53
|
-
'cannot ask the user anything.',
|
|
51
|
+
'report back. Give each one a disjoint set of files to write. Each subagent runs',
|
|
52
|
+
'on the model the user configured for subagents (usually your own). Subagents',
|
|
53
|
+
'cannot dispatch further subagents and cannot ask the user anything.',
|
|
54
54
|
'',
|
|
55
55
|
'You get one combined report when they all finish. Read it before deciding what',
|
|
56
56
|
'to do next, and tell the user what the team found in your own words.',
|
package/dist/team/report.d.ts
CHANGED
|
@@ -57,6 +57,27 @@ export interface ReportOptions {
|
|
|
57
57
|
* one layer lower down.
|
|
58
58
|
*/
|
|
59
59
|
export declare function truncateBytes(text: string, maxBytes: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* Wall time of a child's run for the machine-readable feeds (`-p` stderr, `exec` JSON).
|
|
62
|
+
*
|
|
63
|
+
* A child that was aborted while still queued carries `endedAt` but NO `startedAt`
|
|
64
|
+
* (`startedAt` means "an attempt began", and `selectPanelRows` / `teamClockText` rely on
|
|
65
|
+
* it), so `endedAt - 0` would be an epoch-sized number. A running child (`endedAt`
|
|
66
|
+
* unset) keeps reading `0`, exactly as the feeds always have.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runDurationMs(run: SubagentRun): number;
|
|
69
|
+
/**
|
|
70
|
+
* The form a written path is stored in: relative to the child's cwd with `/`
|
|
71
|
+
* separators when it lives under it, otherwise the resolved absolute path. `src/a.ts`,
|
|
72
|
+
* `./src/a.ts` and `<cwd>/src/a.ts` are one file and have to read as one file.
|
|
73
|
+
*/
|
|
74
|
+
export declare function normalizeLedgerPath(cwd: string, path: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The identity two spellings are compared by. Case is folded on Windows only: ext4 and
|
|
77
|
+
* APFS disagree about case, and a missed warning beats an invented one for a message
|
|
78
|
+
* that is phrased as a warning (see `findFileConflicts`).
|
|
79
|
+
*/
|
|
80
|
+
export declare function ledgerPathKey(path: string): string;
|
|
60
81
|
/**
|
|
61
82
|
* Files written by more than one child.
|
|
62
83
|
*
|
package/dist/team/report.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* outcome of a fan-out; an all-or-nothing result would throw away four successes
|
|
21
21
|
* because of one HTTP 429.
|
|
22
22
|
*/
|
|
23
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
23
24
|
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
24
25
|
import { TEAM_AGGREGATE_LABEL, TEAM_LIMITS } from './limits.js';
|
|
25
26
|
/** The pseudo-label aggregate notices use; reserved at normalization. */
|
|
@@ -57,6 +58,19 @@ function statusBadge(run) {
|
|
|
57
58
|
return '[stopped: turn cap reached]';
|
|
58
59
|
return '[ok]';
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Wall time of a child's run for the machine-readable feeds (`-p` stderr, `exec` JSON).
|
|
63
|
+
*
|
|
64
|
+
* A child that was aborted while still queued carries `endedAt` but NO `startedAt`
|
|
65
|
+
* (`startedAt` means "an attempt began", and `selectPanelRows` / `teamClockText` rely on
|
|
66
|
+
* it), so `endedAt - 0` would be an epoch-sized number. A running child (`endedAt`
|
|
67
|
+
* unset) keeps reading `0`, exactly as the feeds always have.
|
|
68
|
+
*/
|
|
69
|
+
export function runDurationMs(run) {
|
|
70
|
+
if (run.startedAt === undefined)
|
|
71
|
+
return 0;
|
|
72
|
+
return Math.max(0, (run.endedAt ?? 0) - run.startedAt);
|
|
73
|
+
}
|
|
60
74
|
function elapsed(run) {
|
|
61
75
|
if (run.startedAt === undefined)
|
|
62
76
|
return '0.0s';
|
|
@@ -65,6 +79,26 @@ function elapsed(run) {
|
|
|
65
79
|
function isOk(run) {
|
|
66
80
|
return run.phase === 'done' && !run.error;
|
|
67
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The form a written path is stored in: relative to the child's cwd with `/`
|
|
84
|
+
* separators when it lives under it, otherwise the resolved absolute path. `src/a.ts`,
|
|
85
|
+
* `./src/a.ts` and `<cwd>/src/a.ts` are one file and have to read as one file.
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeLedgerPath(cwd, path) {
|
|
88
|
+
const absolute = resolve(cwd, path);
|
|
89
|
+
const rel = relative(cwd, absolute);
|
|
90
|
+
const inside = rel.length > 0 && rel.split(/[/\\]/)[0] !== '..' && !isAbsolute(rel);
|
|
91
|
+
return (inside ? rel : absolute).replace(/\\/g, '/');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The identity two spellings are compared by. Case is folded on Windows only: ext4 and
|
|
95
|
+
* APFS disagree about case, and a missed warning beats an invented one for a message
|
|
96
|
+
* that is phrased as a warning (see `findFileConflicts`).
|
|
97
|
+
*/
|
|
98
|
+
export function ledgerPathKey(path) {
|
|
99
|
+
const key = resolve(path).replace(/\\/g, '/');
|
|
100
|
+
return process.platform === 'win32' ? key.toLowerCase() : key;
|
|
101
|
+
}
|
|
68
102
|
/**
|
|
69
103
|
* Files written by more than one child.
|
|
70
104
|
*
|
|
@@ -77,17 +111,14 @@ export function findFileConflicts(runs) {
|
|
|
77
111
|
const owners = new Map();
|
|
78
112
|
for (const run of runs) {
|
|
79
113
|
for (const path of run.filesTouched) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
const key = ledgerPathKey(path);
|
|
115
|
+
const entry = owners.get(key) ?? { path, labels: [] };
|
|
116
|
+
if (!entry.labels.includes(run.label))
|
|
117
|
+
entry.labels.push(run.label);
|
|
118
|
+
owners.set(key, entry);
|
|
84
119
|
}
|
|
85
120
|
}
|
|
86
|
-
const out = [];
|
|
87
|
-
for (const [path, labels] of owners) {
|
|
88
|
-
if (labels.length > 1)
|
|
89
|
-
out.push({ path, labels });
|
|
90
|
-
}
|
|
121
|
+
const out = [...owners.values()].filter((entry) => entry.labels.length > 1);
|
|
91
122
|
return out.sort((a, b) => a.path.localeCompare(b.path, 'en'));
|
|
92
123
|
}
|
|
93
124
|
function buildHeader(outcome, options) {
|
package/dist/team/runtime.d.ts
CHANGED
|
@@ -157,6 +157,9 @@ export declare class TeamRuntime {
|
|
|
157
157
|
/** The live roster, for `/team` status. `null` when nothing is dispatching. */
|
|
158
158
|
snapshot(): TeamSnapshot | null;
|
|
159
159
|
dispatch(specs: SubagentSpec[], requested: number, signal?: AbortSignal): Promise<DispatchOutcome>;
|
|
160
|
+
/** Idempotent: puts every per-dispatch field back where `dispatch()` found it. */
|
|
161
|
+
private resetDispatchState;
|
|
162
|
+
private runDispatch;
|
|
160
163
|
/** One slot of the pool: take the next index until the list is exhausted. */
|
|
161
164
|
private worker;
|
|
162
165
|
/**
|
package/dist/team/runtime.js
CHANGED
|
@@ -361,6 +361,36 @@ export class TeamRuntime {
|
|
|
361
361
|
// Dispatch
|
|
362
362
|
// -------------------------------------------------------------------------
|
|
363
363
|
async dispatch(specs, requested, signal) {
|
|
364
|
+
try {
|
|
365
|
+
return await this.runDispatch(specs, requested, signal);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
// A throw out of the dispatch body (a child that cannot be constructed) must not
|
|
369
|
+
// leave `busy` latched: every later `task` call, `/team` and busy-guarded command
|
|
370
|
+
// of the session would answer "a dispatch is already running". The success path
|
|
371
|
+
// tears down inline and never reaches this.
|
|
372
|
+
this.resetDispatchState();
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Idempotent: puts every per-dispatch field back where `dispatch()` found it. */
|
|
377
|
+
resetDispatchState() {
|
|
378
|
+
if (this.dispatchTimer)
|
|
379
|
+
clearTimeout(this.dispatchTimer);
|
|
380
|
+
this.dispatchTimer = null;
|
|
381
|
+
for (const handle of this.live)
|
|
382
|
+
handle.unsubscribe();
|
|
383
|
+
this.monitor?.dispose();
|
|
384
|
+
this.monitor = null;
|
|
385
|
+
this.supervision?.clearAll();
|
|
386
|
+
this.supervision = null;
|
|
387
|
+
this.overseer = null;
|
|
388
|
+
this.live = [];
|
|
389
|
+
this.bus = null;
|
|
390
|
+
this.snapshotState = null;
|
|
391
|
+
this.busy = false;
|
|
392
|
+
}
|
|
393
|
+
async runDispatch(specs, requested, signal) {
|
|
364
394
|
this.counter += 1;
|
|
365
395
|
const dispatchId = `d${this.counter}`;
|
|
366
396
|
const config = this.deps.getConfig();
|
|
@@ -703,7 +733,9 @@ export class TeamRuntime {
|
|
|
703
733
|
// where the replacement is actually built and driven. `attempt` resets so
|
|
704
734
|
// the fresh body gets its own cold-start budget.
|
|
705
735
|
if (verdict !== null && verdict.action === 'replace' && !this.aborted) {
|
|
706
|
-
this.replaceChild(dispatchId, handles, index, makeHandle, handle, verdict)
|
|
736
|
+
if (this.replaceChild(dispatchId, handles, index, makeHandle, handle, verdict) === null) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
707
739
|
attempt = 0;
|
|
708
740
|
continue;
|
|
709
741
|
}
|
|
@@ -734,7 +766,8 @@ export class TeamRuntime {
|
|
|
734
766
|
this.monitor?.clear(run.label);
|
|
735
767
|
return;
|
|
736
768
|
}
|
|
737
|
-
this.replaceChild(dispatchId, handles, index, makeHandle, handle, null)
|
|
769
|
+
if (this.replaceChild(dispatchId, handles, index, makeHandle, handle, null) === null)
|
|
770
|
+
return;
|
|
738
771
|
await this.backoff(TEAM_LIMITS.retryBackoffMs);
|
|
739
772
|
if (this.aborted) {
|
|
740
773
|
const fresh = handles[index];
|
|
@@ -778,12 +811,13 @@ export class TeamRuntime {
|
|
|
778
811
|
trigger: 'postmortem',
|
|
779
812
|
});
|
|
780
813
|
if (decision.action === 'replace' && overseer.replacementsLeft(run.label)) {
|
|
781
|
-
this.replaceChild(ctx.dispatchId, ctx.handles, index, ctx.makeHandle, handle, {
|
|
814
|
+
const fresh = this.replaceChild(ctx.dispatchId, ctx.handles, index, ctx.makeHandle, handle, {
|
|
782
815
|
action: 'replace',
|
|
783
816
|
reason: decision.reason,
|
|
784
817
|
...(decision.guidance !== undefined ? { guidance: decision.guidance } : {}),
|
|
785
818
|
});
|
|
786
|
-
|
|
819
|
+
if (fresh !== null)
|
|
820
|
+
await this.runOne(ctx, index);
|
|
787
821
|
}
|
|
788
822
|
}
|
|
789
823
|
/**
|
|
@@ -822,13 +856,35 @@ export class TeamRuntime {
|
|
|
822
856
|
}, handle.run),
|
|
823
857
|
}
|
|
824
858
|
: handle.spec;
|
|
825
|
-
|
|
859
|
+
let fresh;
|
|
860
|
+
try {
|
|
861
|
+
fresh = makeHandle(spec);
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
// The old body is already unsubscribed and aborted, so nothing will ever update
|
|
865
|
+
// its run again: left alone the panel shows it running forever. Settle IT.
|
|
866
|
+
const run = handle.run;
|
|
867
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
868
|
+
run.phase = 'failed';
|
|
869
|
+
run.error = `${run.error ? `${run.error}; ` : ''}could not build replacement: ${reason}`;
|
|
870
|
+
run.endedAt = run.endedAt ?? Date.now();
|
|
871
|
+
this.supervision?.clear(run.label);
|
|
872
|
+
this.monitor?.clear(run.label);
|
|
873
|
+
this.publish(dispatchId, run);
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
826
876
|
// Provably {0,0} under the cold-start predicate above (no turn completed,
|
|
827
877
|
// so no `turn_end` and no usage event). Carried anyway so that a future
|
|
828
878
|
// relaxation of the predicate cannot silently under-report spend - the most
|
|
829
879
|
// misleading failure this feature can produce.
|
|
830
880
|
fresh.run.usage = carried;
|
|
831
881
|
fresh.run.retries = retries;
|
|
882
|
+
// The files the discarded body already wrote are on disk whatever happens next,
|
|
883
|
+
// so the report and the conflict warning must keep seeing them (and a chained
|
|
884
|
+
// replacement must keep telling the next body about them). ONLY the ledger moves:
|
|
885
|
+
// `turns` / `toolCalls` are live controls (turn cap, cold-start retry predicate,
|
|
886
|
+
// supervisor digest) that describe the CURRENT body. Empty on a cold-start retry.
|
|
887
|
+
fresh.run.filesTouched = handle.run.filesTouched.slice(0, TEAM_LIMITS.filesTouchedMax);
|
|
832
888
|
if (replacements > 0)
|
|
833
889
|
fresh.run.replacements = replacements;
|
|
834
890
|
if (interventions !== undefined)
|
package/dist/team/subagent.js
CHANGED
|
@@ -15,6 +15,7 @@ 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, gatedToolTimeouts, } from '../tools/index.js';
|
|
18
|
+
import { ledgerPathKey, normalizeLedgerPath } from './report.js';
|
|
18
19
|
import { describeToolActivity, pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
19
20
|
import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
|
|
20
21
|
import { appendTimeline } from './timeline.js';
|
|
@@ -386,17 +387,26 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
386
387
|
textTail.length > 0 ? sanitizeActivity(textTail, TEAM_LIMITS.activityChars) : undefined;
|
|
387
388
|
hooks.onUpdate({ ...run, filesTouched: [...run.filesTouched] });
|
|
388
389
|
};
|
|
389
|
-
|
|
390
|
+
/**
|
|
391
|
+
* The path a write tool is ABOUT to touch, or `undefined` for every other tool.
|
|
392
|
+
* Staged on the start event and committed on the end event, because only the end
|
|
393
|
+
* event knows whether the write happened (`isError`). The string is the path alone,
|
|
394
|
+
* never the file body (P1-4 / P1-5).
|
|
395
|
+
*/
|
|
396
|
+
const stagedWritePath = (toolName, args) => {
|
|
390
397
|
if (toolName !== 'write_file' && toolName !== 'edit_file')
|
|
391
|
-
return;
|
|
398
|
+
return undefined;
|
|
392
399
|
const path = args?.path;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
return typeof path === 'string' && path.length > 0 ? path : undefined;
|
|
401
|
+
};
|
|
402
|
+
const noteFile = (path) => {
|
|
403
|
+
const normalized = normalizeLedgerPath(deps.getCwd(), path);
|
|
404
|
+
const key = ledgerPathKey(normalized);
|
|
405
|
+
if (run.filesTouched.some((known) => ledgerPathKey(known) === key))
|
|
396
406
|
return;
|
|
397
407
|
if (run.filesTouched.length >= TEAM_LIMITS.filesTouchedMax)
|
|
398
408
|
return;
|
|
399
|
-
run.filesTouched.push(
|
|
409
|
+
run.filesTouched.push(normalized);
|
|
400
410
|
};
|
|
401
411
|
const unsubscribe = agent.subscribe((event) => {
|
|
402
412
|
switch (event.type) {
|
|
@@ -476,18 +486,26 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
476
486
|
// Recorded for the DURATION only; the entry itself is appended on the end
|
|
477
487
|
// event, when `ok` and `ms` are both known (§4.5.2).
|
|
478
488
|
pendingTool = { name: event.toolName, at: Date.now() };
|
|
489
|
+
const stagedPath = stagedWritePath(event.toolName, event.args);
|
|
490
|
+
if (stagedPath !== undefined)
|
|
491
|
+
pendingTool.path = stagedPath;
|
|
479
492
|
run.phase = event.toolName === 'team_wait' ? 'waiting' : 'tool';
|
|
480
493
|
run.lastTool = event.toolName;
|
|
481
494
|
run.toolCalls += 1;
|
|
482
495
|
textTail = '';
|
|
483
496
|
run.activity = undefined; // prose is stale now
|
|
484
497
|
run.activityArgs = pickActivityArgs(event.args); // PICKED, not raw (P1-4)
|
|
485
|
-
noteFile(event.toolName, event.args);
|
|
486
498
|
break;
|
|
487
499
|
case 'tool_execution_end': {
|
|
488
500
|
const endedAt = Date.now();
|
|
489
501
|
const startedAt = pendingTool?.at;
|
|
502
|
+
const writtenPath = pendingTool?.path;
|
|
490
503
|
pendingTool = null;
|
|
504
|
+
// The ledger records a write that HAPPENED: a refused (plan gate, --confirm,
|
|
505
|
+
// sandbox) or failed write is not a touched file, and the timeline below
|
|
506
|
+
// already reads `isError` for the same reason.
|
|
507
|
+
if (writtenPath !== undefined && event.isError !== true)
|
|
508
|
+
noteFile(writtenPath);
|
|
491
509
|
// THE ARGUMENTS ARE GONE BY NOW, on purpose (P1-5): `activityArgs` is cleared a
|
|
492
510
|
// few lines below, and the timeline's text comes from `describeToolActivity` on
|
|
493
511
|
// the PICKED subset, so a `write_file` body can never reach the board.
|