@aragon-agent/cli 0.6.42 → 0.6.44
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/proc/kill-tree.d.ts +19 -1
- package/dist/proc/kill-tree.js +32 -5
- package/dist/proc/limits.d.ts +19 -0
- package/dist/proc/limits.js +19 -0
- package/dist/proc/supervisor.js +8 -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/tools/bounded-spawn.d.ts +56 -0
- package/dist/tools/bounded-spawn.js +76 -0
- package/dist/tools/search-tools.js +52 -44
- package/dist/ui/App.js +24 -10
- package/dist/ui/ScrollbackShell.d.ts +35 -7
- package/dist/ui/ScrollbackShell.js +1 -1
- package/dist/ui/scrollback/LiveTail.d.ts +3 -1
- package/dist/ui/scrollback/budget.d.ts +12 -6
- package/dist/ui/scrollback/budget.js +11 -4
- package/dist/ui/scrollback/tail-anchor.d.ts +5 -2
- package/dist/ui/scrollback/tail-anchor.js +3 -1
- 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/proc/kill-tree.d.ts
CHANGED
|
@@ -38,5 +38,23 @@ export declare function killTree(pid: number | undefined, signal?: NodeJS.Signal
|
|
|
38
38
|
* The only kill primitive a signal handler, `process.on('exit')` or
|
|
39
39
|
* `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
|
|
40
40
|
* that can block or throw is worse than one that misses a child.
|
|
41
|
+
*
|
|
42
|
+
* IT RETURNS WHETHER THE KILL WAS CONFIRMED, AND THAT IS NOT BOOKKEEPING. The
|
|
43
|
+
* Windows branch is a `taskkill` PROCESS, and `spawnSync`'s own `timeout` kills
|
|
44
|
+
* that process when the budget runs out. Measured on 2026-10-23: one
|
|
45
|
+
* `taskkill /t /f` of a shell plus its node child takes 1027-1058 ms on an IDLE
|
|
46
|
+
* machine (five samples), and a `taskkill` cut off at a 150 ms deadline leaves the
|
|
47
|
+
* tree ALIVE 1.5 s later (`ETIMEDOUT`, `status = null`, `signal = SIGTERM`). This
|
|
48
|
+
* function used to `catch {}` that result and return `void`, so the one caller
|
|
49
|
+
* that promises "no service outlives the CLI" could not tell a kill from a
|
|
50
|
+
* timeout - and a loaded host, where a single tree kill genuinely needs more than
|
|
51
|
+
* the old 2000 ms, silently leaked every child it was asked to reap.
|
|
52
|
+
*
|
|
53
|
+
* The budget therefore lives in `PROC_LIMITS.killTreeSyncTimeoutMs`, sized from
|
|
54
|
+
* that measurement rather than from optimism, and the answer comes back to the
|
|
55
|
+
* caller so `reapSync` can raise `killIncomplete` instead of pretending.
|
|
56
|
+
*
|
|
57
|
+
* POSIX reports `true` when a signal was DELIVERED, which is as much as a signal
|
|
58
|
+
* can ever confirm.
|
|
41
59
|
*/
|
|
42
|
-
export declare function killTreeSync(pid: number | undefined):
|
|
60
|
+
export declare function killTreeSync(pid: number | undefined): boolean;
|
package/dist/proc/kill-tree.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { spawn, spawnSync } from 'node:child_process';
|
|
26
26
|
import process from 'node:process';
|
|
27
|
+
import { PROC_LIMITS } from './limits.js';
|
|
27
28
|
/** Is the pid still there? `signal 0` tests for existence without delivering. */
|
|
28
29
|
export function isAlive(pid) {
|
|
29
30
|
if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
|
|
@@ -88,32 +89,58 @@ export function killTree(pid, signal = 'SIGKILL') {
|
|
|
88
89
|
* The only kill primitive a signal handler, `process.on('exit')` or
|
|
89
90
|
* `handleFatal` may reach (I-9). No escalation ladder and no logging: a reaper
|
|
90
91
|
* that can block or throw is worse than one that misses a child.
|
|
92
|
+
*
|
|
93
|
+
* IT RETURNS WHETHER THE KILL WAS CONFIRMED, AND THAT IS NOT BOOKKEEPING. The
|
|
94
|
+
* Windows branch is a `taskkill` PROCESS, and `spawnSync`'s own `timeout` kills
|
|
95
|
+
* that process when the budget runs out. Measured on 2026-10-23: one
|
|
96
|
+
* `taskkill /t /f` of a shell plus its node child takes 1027-1058 ms on an IDLE
|
|
97
|
+
* machine (five samples), and a `taskkill` cut off at a 150 ms deadline leaves the
|
|
98
|
+
* tree ALIVE 1.5 s later (`ETIMEDOUT`, `status = null`, `signal = SIGTERM`). This
|
|
99
|
+
* function used to `catch {}` that result and return `void`, so the one caller
|
|
100
|
+
* that promises "no service outlives the CLI" could not tell a kill from a
|
|
101
|
+
* timeout - and a loaded host, where a single tree kill genuinely needs more than
|
|
102
|
+
* the old 2000 ms, silently leaked every child it was asked to reap.
|
|
103
|
+
*
|
|
104
|
+
* The budget therefore lives in `PROC_LIMITS.killTreeSyncTimeoutMs`, sized from
|
|
105
|
+
* that measurement rather than from optimism, and the answer comes back to the
|
|
106
|
+
* caller so `reapSync` can raise `killIncomplete` instead of pretending.
|
|
107
|
+
*
|
|
108
|
+
* POSIX reports `true` when a signal was DELIVERED, which is as much as a signal
|
|
109
|
+
* can ever confirm.
|
|
91
110
|
*/
|
|
92
111
|
export function killTreeSync(pid) {
|
|
93
112
|
if (pid === undefined || !Number.isFinite(pid) || pid <= 0)
|
|
94
|
-
return;
|
|
113
|
+
return false;
|
|
95
114
|
if (process.platform === 'win32') {
|
|
115
|
+
let result;
|
|
96
116
|
try {
|
|
97
|
-
spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
117
|
+
result = spawnSync('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
98
118
|
windowsHide: true,
|
|
99
119
|
stdio: 'ignore',
|
|
100
|
-
timeout:
|
|
120
|
+
timeout: PROC_LIMITS.killTreeSyncTimeoutMs,
|
|
101
121
|
});
|
|
102
122
|
}
|
|
103
123
|
catch {
|
|
104
|
-
|
|
124
|
+
return false;
|
|
105
125
|
}
|
|
106
|
-
|
|
126
|
+
// `error` is set for a spawn failure AND for the budget expiring (`ETIMEDOUT`,
|
|
127
|
+
// with `signal = SIGTERM`), and neither is a kill.
|
|
128
|
+
if (result.error)
|
|
129
|
+
return false;
|
|
130
|
+
return result.status === 0;
|
|
107
131
|
}
|
|
108
132
|
try {
|
|
109
133
|
process.kill(-pid, 'SIGKILL');
|
|
134
|
+
return true;
|
|
110
135
|
}
|
|
111
136
|
catch {
|
|
112
137
|
try {
|
|
113
138
|
process.kill(pid, 'SIGKILL');
|
|
139
|
+
return true;
|
|
114
140
|
}
|
|
115
141
|
catch {
|
|
116
142
|
/* already gone */
|
|
143
|
+
return false;
|
|
117
144
|
}
|
|
118
145
|
}
|
|
119
146
|
}
|
package/dist/proc/limits.d.ts
CHANGED
|
@@ -102,6 +102,25 @@ export declare const PROC_LIMITS: {
|
|
|
102
102
|
readonly killVerifyMs: 2000;
|
|
103
103
|
/** Liveness poll interval while waiting for a `SIGKILL` to land. */
|
|
104
104
|
readonly killVerifyPollMs: 50;
|
|
105
|
+
/**
|
|
106
|
+
* How long ONE synchronous tree kill (`killTreeSync`) may take.
|
|
107
|
+
*
|
|
108
|
+
* MEASURED, NOT GUESSED. A `taskkill /pid <shell> /t /f` of a shell plus its
|
|
109
|
+
* node child takes 1027-1058 ms on the reference machine on an IDLE host (five
|
|
110
|
+
* samples, 2026-10-23), so the old 2000 ms was 1.9x the honest cost and the
|
|
111
|
+
* 2026-10-23 release run - where the whole suite took 340 s against the usual
|
|
112
|
+
* 200-240 s - is comfortably inside the range where a single tree kill exceeds
|
|
113
|
+
* it. Crossing it is not a slow kill, it is NO kill: `spawnSync` kills
|
|
114
|
+
* `taskkill` itself at the deadline and the tree keeps running (verified: a
|
|
115
|
+
* 150 ms budget left the child alive 1.5 s later, `ETIMEDOUT` / `status null`).
|
|
116
|
+
*
|
|
117
|
+
* 5000 ms is ~5x the measured cost. It is a CEILING and not a cost: a tree that
|
|
118
|
+
* dies in the usual second still returns in that second. The worst case is
|
|
119
|
+
* `maxJobs` (16) trees at the ceiling on an exit hook, which is why this is not
|
|
120
|
+
* any larger - the reaper prefers a reported miss to an exit that never ends,
|
|
121
|
+
* and the miss is now reported (`ProcRecord.killIncomplete`).
|
|
122
|
+
*/
|
|
123
|
+
readonly killTreeSyncTimeoutMs: 5000;
|
|
105
124
|
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
106
125
|
readonly statusCompactCols: 92;
|
|
107
126
|
/**
|
package/dist/proc/limits.js
CHANGED
|
@@ -102,6 +102,25 @@ export const PROC_LIMITS = {
|
|
|
102
102
|
killVerifyMs: 2000,
|
|
103
103
|
/** Liveness poll interval while waiting for a `SIGKILL` to land. */
|
|
104
104
|
killVerifyPollMs: 50,
|
|
105
|
+
/**
|
|
106
|
+
* How long ONE synchronous tree kill (`killTreeSync`) may take.
|
|
107
|
+
*
|
|
108
|
+
* MEASURED, NOT GUESSED. A `taskkill /pid <shell> /t /f` of a shell plus its
|
|
109
|
+
* node child takes 1027-1058 ms on the reference machine on an IDLE host (five
|
|
110
|
+
* samples, 2026-10-23), so the old 2000 ms was 1.9x the honest cost and the
|
|
111
|
+
* 2026-10-23 release run - where the whole suite took 340 s against the usual
|
|
112
|
+
* 200-240 s - is comfortably inside the range where a single tree kill exceeds
|
|
113
|
+
* it. Crossing it is not a slow kill, it is NO kill: `spawnSync` kills
|
|
114
|
+
* `taskkill` itself at the deadline and the tree keeps running (verified: a
|
|
115
|
+
* 150 ms budget left the child alive 1.5 s later, `ETIMEDOUT` / `status null`).
|
|
116
|
+
*
|
|
117
|
+
* 5000 ms is ~5x the measured cost. It is a CEILING and not a cost: a tree that
|
|
118
|
+
* dies in the usual second still returns in that second. The worst case is
|
|
119
|
+
* `maxJobs` (16) trees at the ceiling on an exit hook, which is why this is not
|
|
120
|
+
* any larger - the reaper prefers a reported miss to an exit that never ends,
|
|
121
|
+
* and the miss is now reported (`ProcRecord.killIncomplete`).
|
|
122
|
+
*/
|
|
123
|
+
killTreeSyncTimeoutMs: 5000,
|
|
105
124
|
/** Below this many terminal columns the status chip degrades to `[N]`. */
|
|
106
125
|
statusCompactCols: 92,
|
|
107
126
|
// --- long-task-jobs: bounded waiting, progress, notices, log mirror --------
|
package/dist/proc/supervisor.js
CHANGED
|
@@ -404,9 +404,16 @@ export class ProcSupervisor {
|
|
|
404
404
|
for (const entry of this.services.values()) {
|
|
405
405
|
if (isTerminalStatus(entry.record.status))
|
|
406
406
|
continue;
|
|
407
|
-
|
|
407
|
+
// THE ANSWER IS PART OF THE REAP (AC-42, 2026-10-23 release). A `taskkill`
|
|
408
|
+
// that outlived its budget is killed itself and the tree survives, so an
|
|
409
|
+
// unconfirmed kill has to reach the record the card and `bash_output` read -
|
|
410
|
+
// otherwise "stopped" is a claim nobody made good on. `killIncomplete` is
|
|
411
|
+
// that field (P2-6), and it is the reason the reaper may keep a bound at all.
|
|
412
|
+
const killed = killTreeSync(entry.record.pid);
|
|
408
413
|
entry.record.status = 'stopped';
|
|
409
414
|
entry.record.endedAt = Date.now();
|
|
415
|
+
if (!killed)
|
|
416
|
+
entry.record.killIncomplete = true;
|
|
410
417
|
entry.watcher?.stop();
|
|
411
418
|
if (entry.outputTimer)
|
|
412
419
|
clearTimeout(entry.outputTimer);
|
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
|
/**
|