@aragon-agent/cli 0.6.42 → 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/dist/agent/controller.js +9 -12
- package/dist/agent/headless.js +6 -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/limits.d.ts +1 -1
- package/dist/team/limits.js +1 -1
- package/dist/team/model.d.ts +17 -1
- package/dist/team/model.js +14 -0
- 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 +24 -10
- package/package.json +2 -2
package/dist/agent/controller.js
CHANGED
|
@@ -900,18 +900,15 @@ export class AgentController {
|
|
|
900
900
|
// executor's timeout is cooperative, so an override is inert for any
|
|
901
901
|
// tool that does not listen (I-2).
|
|
902
902
|
//
|
|
903
|
-
// `
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
...(this.teamRegistered
|
|
913
|
-
? { task: config.team.overseer ? 0 : config.team.dispatchTimeoutMs }
|
|
914
|
-
: {}),
|
|
903
|
+
// ALWAYS `0` = NO EXECUTOR TIMER, and the override itself must stay:
|
|
904
|
+
// deleting it would drop `task` back onto the executor's default tool
|
|
905
|
+
// timeout and kill every long fan-out at that default. The dispatch
|
|
906
|
+
// ceiling lives in ONE place, `TeamRuntime.dispatch()`, which re-reads
|
|
907
|
+
// `team.overseer` / `team.dispatchTimeoutMs` on every dispatch. The
|
|
908
|
+
// executor's table is frozen at construction, so a ceiling armed here
|
|
909
|
+
// outlived a mid-session `Settings > Team` switch to the supervisor
|
|
910
|
+
// and killed the dispatch the supervisor was meant to look after.
|
|
911
|
+
...(this.teamRegistered ? { task: 0 } : {}),
|
|
915
912
|
},
|
|
916
913
|
},
|
|
917
914
|
});
|
package/dist/agent/headless.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
|
|
8
8
|
import { formatStreamError } from './reducer.js';
|
|
9
|
+
import { runDurationMs } from '../team/report.js';
|
|
9
10
|
import { derivePlanPhase } from '../todo/acceptance.js';
|
|
10
11
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
11
12
|
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
@@ -99,19 +100,22 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
99
100
|
if (quiet)
|
|
100
101
|
return;
|
|
101
102
|
if (event.type === 'dispatch_start') {
|
|
103
|
+
// Labels default to a1, a2... on every dispatch, so "once only" is per dispatch.
|
|
104
|
+
started.clear();
|
|
105
|
+
finished.clear();
|
|
102
106
|
err.write(`\n[team] dispatch ${event.specs.length} subagents\n`);
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
105
109
|
if (event.type === 'agent_update') {
|
|
106
110
|
const { run } = event;
|
|
107
|
-
if (run.
|
|
111
|
+
if (run.startedAt !== undefined && !started.has(run.label)) {
|
|
108
112
|
started.add(run.label);
|
|
109
113
|
err.write(`[team] ${run.label} start "${run.description}"\n`);
|
|
110
114
|
}
|
|
111
115
|
const terminal = run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
|
|
112
116
|
if (terminal && !finished.has(run.label)) {
|
|
113
117
|
finished.add(run.label);
|
|
114
|
-
const ms =
|
|
118
|
+
const ms = runDurationMs(run);
|
|
115
119
|
const verdict = run.phase === 'done' && !run.error ? 'ok' : run.phase;
|
|
116
120
|
err.write(`[team] ${run.label} ${verdict} ${formatDuration(ms)} ${run.toolCalls} tools\n`);
|
|
117
121
|
}
|
package/dist/exec/runner.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* same `--session-id` must not race the writer of the file it is about to read.
|
|
16
16
|
*/
|
|
17
17
|
import { formatStreamError } from '../agent/reducer.js';
|
|
18
|
+
import { runDurationMs } from '../team/report.js';
|
|
18
19
|
import { setSignalTerminator } from '../logging/install.js';
|
|
19
20
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
20
21
|
import { EXEC_SCHEMA_VERSION, buildRequestEndEvent } from './events.js';
|
|
@@ -1622,7 +1623,7 @@ export class ExecRunner {
|
|
|
1622
1623
|
label: run.label,
|
|
1623
1624
|
phase: run.phase,
|
|
1624
1625
|
description: run.description,
|
|
1625
|
-
durationMs:
|
|
1626
|
+
durationMs: runDurationMs(run),
|
|
1626
1627
|
toolCalls: run.toolCalls,
|
|
1627
1628
|
});
|
|
1628
1629
|
return;
|
package/dist/team/bus.d.ts
CHANGED
package/dist/team/bus.js
CHANGED
|
@@ -103,10 +103,20 @@ export class TeamBus {
|
|
|
103
103
|
send(from, to, subjectRaw, bodyRaw) {
|
|
104
104
|
const target = to.trim().toLowerCase();
|
|
105
105
|
const cost = target === ALL_KEY ? TEAM_LIMITS.broadcastCost : 1;
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
106
|
+
const addressed = this.resolveRecipients(from, target);
|
|
107
|
+
if (addressed.length === 0) {
|
|
108
108
|
return { ok: false, reason: 'unknown_recipient', recipients: this.participants() };
|
|
109
109
|
}
|
|
110
|
+
// A finished peer has no loop left to drain its mailbox, so "Delivered" would be
|
|
111
|
+
// a false receipt and the quota spent on it a pure loss. Same fact `checkWaitable`
|
|
112
|
+
// already honours. The lead is always addressable: its mail is collected for the
|
|
113
|
+
// report, not read by a loop. Refused like every other refusal: nothing is spent
|
|
114
|
+
// and `lastSentAt` does not move. A peer that is about to finish but has not yet is
|
|
115
|
+
// still delivered to; that window cannot be closed without a lock.
|
|
116
|
+
const recipients = addressed.filter((r) => r === LEAD_KEY || this.canSend(r));
|
|
117
|
+
if (recipients.length === 0) {
|
|
118
|
+
return { ok: false, reason: 'recipient_finished', who: addressed };
|
|
119
|
+
}
|
|
110
120
|
const used = this.sentCount(from);
|
|
111
121
|
if (used + cost > TEAM_LIMITS.messagesPerAgent) {
|
|
112
122
|
return { ok: false, reason: 'agent_quota', used, max: TEAM_LIMITS.messagesPerAgent };
|
package/dist/team/comm-tools.js
CHANGED
|
@@ -64,6 +64,9 @@ export function makeTeamSend(bus, selfKey) {
|
|
|
64
64
|
case 'unknown_recipient':
|
|
65
65
|
return textResult(`Not sent: no subagent labelled "${to.trim()}". Teammates: ` +
|
|
66
66
|
`${verdict.recipients.filter((r) => r !== selfKey).join(', ')}.`);
|
|
67
|
+
case 'recipient_finished':
|
|
68
|
+
return textResult(`Not sent: ${verdict.who.join(', ')} already finished and will not read it. ` +
|
|
69
|
+
'Put what matters in your own final summary instead.');
|
|
67
70
|
case 'agent_quota':
|
|
68
71
|
return textResult(`Not sent: message quota spent (${verdict.used}/${verdict.max}). Finish your work ` +
|
|
69
72
|
'and put it in your final summary.');
|
package/dist/team/limits.d.ts
CHANGED
|
@@ -265,7 +265,7 @@ 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
269
|
/**
|
|
270
270
|
* The version string on `<forced_team_mode>`'s first line.
|
|
271
271
|
*
|
package/dist/team/limits.js
CHANGED
|
@@ -279,7 +279,7 @@ 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
283
|
// ---------------------------------------------------------------------------
|
|
284
284
|
// Forced team mode (`/team <brief>`, team-command-forced-dispatch §6.5).
|
|
285
285
|
//
|
package/dist/team/model.d.ts
CHANGED
|
@@ -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
|
@@ -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.
|
package/dist/ui/App.js
CHANGED
|
@@ -55,6 +55,7 @@ import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow
|
|
|
55
55
|
// no policy. Imported beside the todo decision it shares one timer with.
|
|
56
56
|
import { decideForcedDispatch, FORCED_NUDGE_NOTICE } from '../team/dispatch-gate.js';
|
|
57
57
|
import { FORCED_NUDGE_GRACE_MS } from '../team/limits.js';
|
|
58
|
+
import { priceTeamUsage } from '../team/model.js';
|
|
58
59
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
59
60
|
import { useHeightStore } from './use-height-store.js';
|
|
60
61
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
@@ -929,21 +930,34 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
929
930
|
// handler above records): this effect's dependency list never changes,
|
|
930
931
|
// so it closes over the `cfg` object of the render it mounted in, and
|
|
931
932
|
// `setModel` builds a NEW one.
|
|
932
|
-
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
:
|
|
933
|
+
//
|
|
934
|
+
// PRICED AT THE MODEL THE CHILDREN RAN (`team.model`), through the same
|
|
935
|
+
// resolution the dispatch report prices with: under `fast` / `profile` the
|
|
936
|
+
// lead's table would over-report by up to an order of magnitude, and an
|
|
937
|
+
// unpriced child model must read as unknown (ZERO here), not as the lead's.
|
|
938
|
+
const costDelta = priceTeamUsage(controller.subagentModelResolution(), event.usage, {
|
|
939
|
+
mainCost: controller.getModelInfo().cost,
|
|
940
|
+
isPriced: (ref) => controller.isPricedModel(ref),
|
|
941
|
+
costFor: (ref) => controller.getModelInfoFor(ref).cost,
|
|
942
|
+
});
|
|
941
943
|
dispatch({ type: 'teamUsage', usage: event.usage, costDelta });
|
|
942
944
|
break;
|
|
943
945
|
}
|
|
944
|
-
case 'dispatch_end':
|
|
946
|
+
case 'dispatch_end': {
|
|
947
|
+
// The supervisor's own calls never emit a usage event (they go through the fast
|
|
948
|
+
// registry directly), so their spend reaches the readout HERE, once per
|
|
949
|
+
// dispatch, at the fast tier's price - the same channel review spend uses.
|
|
950
|
+
const spend = event.outcome.overseerUsage;
|
|
951
|
+
if (spend !== undefined && (spend.inputTokens > 0 || spend.outputTokens > 0)) {
|
|
952
|
+
const tier = controller.getFastStatus().tier;
|
|
953
|
+
const costDelta = tier.ok && controller.isPricedModel(tier.ref)
|
|
954
|
+
? computeCost(spend, controller.getModelInfoFor(tier.ref).cost)
|
|
955
|
+
: 0;
|
|
956
|
+
dispatch({ type: 'fastUsage', usage: spend, costDelta });
|
|
957
|
+
}
|
|
945
958
|
dispatch({ type: 'teamEnd', outcome: event.outcome });
|
|
946
959
|
break;
|
|
960
|
+
}
|
|
947
961
|
}
|
|
948
962
|
});
|
|
949
963
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.43",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aragon-agent/core": "^0.2.
|
|
59
|
+
"@aragon-agent/core": "^0.2.55",
|
|
60
60
|
"chalk": "^5.6.2",
|
|
61
61
|
"cli-highlight": "^2.0.0",
|
|
62
62
|
"commander": "^12.1.0",
|