@aragon-agent/cli 0.6.39 → 0.6.42
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 +109 -0
- package/README.md +126 -20
- package/dist/agent/controller.d.ts +139 -14
- package/dist/agent/controller.js +299 -40
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +29 -1
- package/dist/agent/reducer.js +34 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +33 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +28 -185
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +30 -0
- package/dist/commands/subagent-command.js +39 -0
- package/dist/commands/team-command.d.ts +49 -0
- package/dist/commands/team-command.js +259 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/load.js +50 -3
- package/dist/config/model-kits.d.ts +2 -1
- package/dist/config/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +10 -4
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/dispatch-gate.d.ts +86 -0
- package/dist/team/dispatch-gate.js +75 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -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 +61 -1
- package/dist/team/limits.js +69 -1
- package/dist/team/model.d.ts +5 -5
- package/dist/team/model.js +5 -5
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/ui/App.js +130 -11
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +2 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/layout/status-layout.d.ts +27 -0
- package/dist/ui/layout/status-layout.js +27 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -4
- package/dist/ui/overlays/SettingsScreen.d.ts +14 -0
- package/dist/ui/overlays/SettingsScreen.js +39 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +51 -0
- package/dist/ui/overlays/TeamFanoutPage.js +90 -0
- package/dist/ui/overlays/kit-settings-state.js +3 -2
- package/dist/ui/overlays/model-profile-state.d.ts +20 -2
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +106 -0
- package/dist/ui/overlays/team-fanout-state.js +155 -0
- package/package.json +81 -81
package/dist/commands/skills.js
CHANGED
|
@@ -12,6 +12,7 @@ import { checkManifest } from '../skills/manifest.js';
|
|
|
12
12
|
import { updateAllSkills, updateSkill, isHardFailure } from '../skills/updater.js';
|
|
13
13
|
import { currentPlatform } from '../skills/node-host.js';
|
|
14
14
|
import { listUsage, resetUsage, getUsagePath } from '../skills/usage.js';
|
|
15
|
+
import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
15
16
|
import { HOST_TOOL_NAMES, SKILL_TOOL_FLOOR } from '../tools/index.js';
|
|
16
17
|
import { SKILLS_TOOL_POLICY_MODES, isSkillsToolPolicyMode } from '../config/schema.js';
|
|
17
18
|
import { pickGlyphs } from '../ui/glyphs.js';
|
|
@@ -92,12 +93,96 @@ async function runSkills(ctx, service, version) {
|
|
|
92
93
|
return unloadFrames(ctx, service);
|
|
93
94
|
case 'usage':
|
|
94
95
|
return usageFromCommand(ctx, service, argument);
|
|
96
|
+
case 'triage':
|
|
97
|
+
return triageFromCommand(ctx, service, argument);
|
|
98
|
+
case 'route':
|
|
99
|
+
return routeFromCommand(ctx, service, argument);
|
|
95
100
|
default:
|
|
96
101
|
ctx.notify('warn', `Unknown subcommand "/skills ${sub}". Try: list, info, install, update, remove, enable, ` +
|
|
97
|
-
'disable, reload, create, trust, untrust, policy, unload, usage.');
|
|
102
|
+
'disable, reload, create, trust, untrust, policy, unload, usage, triage, route.');
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
// ---------------------------------------------------------------------------
|
|
106
|
+
// /skills triage · route (skill-triage-and-unbounded-ask §4.2.8)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
/**
|
|
109
|
+
* `/skills triage [on|off]`.
|
|
110
|
+
*
|
|
111
|
+
* The report names the switch AND the last turn's result, because "on" alone does
|
|
112
|
+
* not answer the question a user actually has, which is "is it finding anything?".
|
|
113
|
+
* `lastSkillRouting` comes from the controller — it is per-turn session state, not
|
|
114
|
+
* skill-service state, and the service must not start owning a copy of it.
|
|
115
|
+
*/
|
|
116
|
+
function triageFromCommand(ctx, service, argument) {
|
|
117
|
+
const requested = argument.trim().toLowerCase();
|
|
118
|
+
if (requested.length === 0) {
|
|
119
|
+
const on = service.getConfig().triage === true;
|
|
120
|
+
const last = ctx.controller.getSkillRouting();
|
|
121
|
+
const summary = last === null
|
|
122
|
+
? 'No turn has been triaged yet.'
|
|
123
|
+
: last.candidates.length === 0
|
|
124
|
+
? `Last turn: no candidate (${last.eligible} eligible skill(s) scored).`
|
|
125
|
+
: `Last turn: ${last.candidates.length} candidate(s), best ${last.candidates[0].name} (${last.candidates[0].score}).`;
|
|
126
|
+
ctx.notify('info', `Skill triage: ${on ? 'on' : 'off'}. ${summary}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (requested !== 'on' && requested !== 'off') {
|
|
130
|
+
ctx.notify('warn', `Unknown argument "${argument}" - use /skills triage [on|off].`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const enabled = requested === 'on';
|
|
134
|
+
// `setTriageEnabled` persists AND fires `onChange`, which rebuilds the prompt in
|
|
135
|
+
// this session. Without that second half the switch would look applied and take
|
|
136
|
+
// effect only after a restart (AC-T23).
|
|
137
|
+
service.setTriageEnabled(enabled);
|
|
138
|
+
// A toast, not a transcript notice: flipping a switch is an acknowledgement, and
|
|
139
|
+
// `toast` is this package's channel for exactly that (see `toggleSkill`). The
|
|
140
|
+
// level still differs by direction, so "off" does not read as a win.
|
|
141
|
+
ctx.toast(enabled ? 'success' : 'info', `Skill triage: ${enabled ? 'on' : 'off'}. ` +
|
|
142
|
+
(enabled
|
|
143
|
+
? 'Each new task gets a short candidate list.'
|
|
144
|
+
: 'The system prompt no longer carries the triage block.'));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `/skills route [text]`.
|
|
148
|
+
*
|
|
149
|
+
* Two modes, one output shape. With text it routes that text; without text it
|
|
150
|
+
* replays the previous turn's real routing, which is the case a user is most often
|
|
151
|
+
* in — they just watched the model ignore a suggestion. The "never triaged" text
|
|
152
|
+
* names the form that works instead of leaving them to guess.
|
|
153
|
+
*/
|
|
154
|
+
function routeFromCommand(ctx, service, argument) {
|
|
155
|
+
const task = argument.trim();
|
|
156
|
+
if (task.length === 0) {
|
|
157
|
+
const last = ctx.controller.getSkillRouting();
|
|
158
|
+
if (last === null) {
|
|
159
|
+
ctx.notify('info', 'No turn has been triaged yet. Run /skills route <text> to try one.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ctx.notify('info', renderRoutingReport(last));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// A fresh computation, NOT the cached one: `/skills route <text>` is how a user
|
|
166
|
+
// tests whether a phrasing would have matched.
|
|
167
|
+
ctx.notify('info', renderRoutingReport(service.route(task)));
|
|
168
|
+
}
|
|
169
|
+
/** One `info` notification, one line per candidate, ASCII only (`commands/`). */
|
|
170
|
+
function renderRoutingReport(report) {
|
|
171
|
+
if (report.candidates.length === 0) {
|
|
172
|
+
const close = report.topUnqualified === null
|
|
173
|
+
? ''
|
|
174
|
+
: ` Closest was ${report.topUnqualified.name} (${report.topUnqualified.score}); ` +
|
|
175
|
+
`the threshold is ${SKILL_TRIAGE_LIMITS.minScore}.`;
|
|
176
|
+
return (`No skill matched "${report.task}". The catalog has ${report.eligible} eligible skills.` +
|
|
177
|
+
close);
|
|
178
|
+
}
|
|
179
|
+
const lines = report.candidates.map((candidate) => {
|
|
180
|
+
const reasons = candidate.reasons.length > 0 ? candidate.reasons.join('+') : 'none';
|
|
181
|
+
return `${candidate.name} (${candidate.scope}) score ${candidate.score} [${reasons}]`;
|
|
182
|
+
});
|
|
183
|
+
return [`Routed "${report.task}" (${report.tokenCount} token(s)):`, ...lines].join('\n');
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
101
186
|
// /skills policy · unload · usage (§5.5 / §9)
|
|
102
187
|
// ---------------------------------------------------------------------------
|
|
103
188
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/subagent`: a DEPRECATED ALIAS of `/team` (team-command-forced-dispatch
|
|
3
|
+
* §3.10 / D-12 / I-8).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT STILL EXISTS. It shipped, it is in five places in the README, and it is
|
|
8
|
+
* in the muscle memory of everyone who used the forced fan-out before `/team`
|
|
9
|
+
* grew its meaning. Removing the name would be a breaking change that buys
|
|
10
|
+
* nothing the alias does not buy for one `toast`.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT HAS NO BODY. `subagent-fanout-and-forced-dispatch` implemented the
|
|
13
|
+
* whole grammar - control words, two ceilings, four parse errors, plan-mode
|
|
14
|
+
* disclosure - and every one of those bytes is now `/team`'s. A second copy here
|
|
15
|
+
* would drift on the first refinement, and the drift would be invisible: both
|
|
16
|
+
* commands would keep working, one of them would quietly be the old one. The
|
|
17
|
+
* file therefore holds ONE delegation and one line of deprecation text, and
|
|
18
|
+
* `subagent-command.test.ts` asserts both (I-8).
|
|
19
|
+
*/
|
|
20
|
+
import type { CommandContext, CommandRegistry } from './registry.js';
|
|
21
|
+
/**
|
|
22
|
+
* Run `/team`, having first said out loud that this is the same thing.
|
|
23
|
+
*
|
|
24
|
+
* THE TOAST COMES FIRST ON PURPOSE: it is one ephemeral line above whatever
|
|
25
|
+
* `/team` prints, so the sequence reads "you used the old name; here is the
|
|
26
|
+
* answer" rather than a full answer followed by a correction nobody notices.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runSubagentCommand(ctx: CommandContext): void;
|
|
29
|
+
/** Registered from `builtins.ts`: the name, and nothing else, lives here. */
|
|
30
|
+
export declare function registerSubagentCommand(registry: CommandRegistry): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/subagent`: a DEPRECATED ALIAS of `/team` (team-command-forced-dispatch
|
|
3
|
+
* §3.10 / D-12 / I-8).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT STILL EXISTS. It shipped, it is in five places in the README, and it is
|
|
8
|
+
* in the muscle memory of everyone who used the forced fan-out before `/team`
|
|
9
|
+
* grew its meaning. Removing the name would be a breaking change that buys
|
|
10
|
+
* nothing the alias does not buy for one `toast`.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT HAS NO BODY. `subagent-fanout-and-forced-dispatch` implemented the
|
|
13
|
+
* whole grammar - control words, two ceilings, four parse errors, plan-mode
|
|
14
|
+
* disclosure - and every one of those bytes is now `/team`'s. A second copy here
|
|
15
|
+
* would drift on the first refinement, and the drift would be invisible: both
|
|
16
|
+
* commands would keep working, one of them would quietly be the old one. The
|
|
17
|
+
* file therefore holds ONE delegation and one line of deprecation text, and
|
|
18
|
+
* `subagent-command.test.ts` asserts both (I-8).
|
|
19
|
+
*/
|
|
20
|
+
import { runTeamCommand } from './team-command.js';
|
|
21
|
+
/**
|
|
22
|
+
* Run `/team`, having first said out loud that this is the same thing.
|
|
23
|
+
*
|
|
24
|
+
* THE TOAST COMES FIRST ON PURPOSE: it is one ephemeral line above whatever
|
|
25
|
+
* `/team` prints, so the sequence reads "you used the old name; here is the
|
|
26
|
+
* answer" rather than a full answer followed by a correction nobody notices.
|
|
27
|
+
*/
|
|
28
|
+
export function runSubagentCommand(ctx) {
|
|
29
|
+
ctx.toast('info', '/subagent is now /team - same arguments.');
|
|
30
|
+
runTeamCommand(ctx);
|
|
31
|
+
}
|
|
32
|
+
/** Registered from `builtins.ts`: the name, and nothing else, lives here. */
|
|
33
|
+
export function registerSubagentCommand(registry) {
|
|
34
|
+
registry.register({
|
|
35
|
+
name: 'subagent',
|
|
36
|
+
description: 'Deprecated alias of /team - same arguments',
|
|
37
|
+
run: (ctx) => { runSubagentCommand(ctx); },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/team`: force the session into team mode and start work, or leave it
|
|
3
|
+
* (team-command-forced-dispatch §3.3 / §3.4 / §5.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS COMMAND IS. `/team <brief>` parses the user's intent, ARMS a
|
|
8
|
+
* session-level forced mode and submits ONE normal user turn. The lead analyses
|
|
9
|
+
* and plans that turn and then has to call `task` - three layers say so: the
|
|
10
|
+
* directive it is handed, the `<forced_team_mode>` block in every later system
|
|
11
|
+
* prompt, and the mechanical gate in `ui/App.tsx` that nudges once if a whole
|
|
12
|
+
* turn went by without a dispatch. This command never dispatches itself (I-1):
|
|
13
|
+
* Esc, the watchdog, the report, spend accounting and the concurrency lock all
|
|
14
|
+
* stay on the one path that already has them.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT IS NOT ANY MORE. It used to be the CONFIG command - `status | on | off
|
|
17
|
+
* | max <n> | overseer on|off | model <name>`. All five verbs now live in the
|
|
18
|
+
* settings screen (`Settings > Team`, plus the root `Subagent model` row), and
|
|
19
|
+
* this command writes not one byte of `config.json` (I-4). The reason is the one
|
|
20
|
+
* the requirement names: a command that both changes configuration and starts
|
|
21
|
+
* work leaves the user unable to predict what it did. `on` and `off` survive as
|
|
22
|
+
* SESSION CONTROL words - a mode needs enter, leave and report - and they never
|
|
23
|
+
* persist; the settings screen is the only persistent writer (I-4 / D-11).
|
|
24
|
+
*
|
|
25
|
+
* THE GUARD ORDER IS A CONTRACT (§3.4, R-P1-10 / R-P1-11) and it is not
|
|
26
|
+
* alphabetical:
|
|
27
|
+
*
|
|
28
|
+
* status (no guard) - "what is on?" is a question, answerable even in a
|
|
29
|
+
* `--no-team` session, which is where users ask it.
|
|
30
|
+
* off (no guard) - leaving is always possible, even mid-dispatch.
|
|
31
|
+
* registered? - a `--no-team` session cannot grow the tool (I-7).
|
|
32
|
+
* enabled? - NOT a refusal: it is turned on for this session
|
|
33
|
+
* (D-5), because "start now" must not detour through a
|
|
34
|
+
* settings screen. Nothing is persisted.
|
|
35
|
+
* not busy / idle - two separate sentences, because both would otherwise
|
|
36
|
+
* surface as `task`'s own refusal card.
|
|
37
|
+
* key present? - names the missing provider.
|
|
38
|
+
* on - arm only; the next message dispatches.
|
|
39
|
+
*
|
|
40
|
+
* EVERY `armForcedTeam()` / `disarmForcedTeam()` CALL SITE BELOW IS FOLLOWED BY A
|
|
41
|
+
* `ctx.notify` / `ctx.toast` IN THE SAME BRANCH (P1-6 / AC-28), because arming
|
|
42
|
+
* itself produces no `AgentEvent` and therefore no render: without the notice the
|
|
43
|
+
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
|
+
*/
|
|
45
|
+
import type { CommandContext, CommandRegistry } from './registry.js';
|
|
46
|
+
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
47
|
+
export declare function runTeamCommand(ctx: CommandContext): void;
|
|
48
|
+
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
49
|
+
export declare function registerTeamCommand(registry: CommandRegistry): void;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/team`: force the session into team mode and start work, or leave it
|
|
3
|
+
* (team-command-forced-dispatch §3.3 / §3.4 / §5.1).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/commands/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* WHAT THIS COMMAND IS. `/team <brief>` parses the user's intent, ARMS a
|
|
8
|
+
* session-level forced mode and submits ONE normal user turn. The lead analyses
|
|
9
|
+
* and plans that turn and then has to call `task` - three layers say so: the
|
|
10
|
+
* directive it is handed, the `<forced_team_mode>` block in every later system
|
|
11
|
+
* prompt, and the mechanical gate in `ui/App.tsx` that nudges once if a whole
|
|
12
|
+
* turn went by without a dispatch. This command never dispatches itself (I-1):
|
|
13
|
+
* Esc, the watchdog, the report, spend accounting and the concurrency lock all
|
|
14
|
+
* stay on the one path that already has them.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IT IS NOT ANY MORE. It used to be the CONFIG command - `status | on | off
|
|
17
|
+
* | max <n> | overseer on|off | model <name>`. All five verbs now live in the
|
|
18
|
+
* settings screen (`Settings > Team`, plus the root `Subagent model` row), and
|
|
19
|
+
* this command writes not one byte of `config.json` (I-4). The reason is the one
|
|
20
|
+
* the requirement names: a command that both changes configuration and starts
|
|
21
|
+
* work leaves the user unable to predict what it did. `on` and `off` survive as
|
|
22
|
+
* SESSION CONTROL words - a mode needs enter, leave and report - and they never
|
|
23
|
+
* persist; the settings screen is the only persistent writer (I-4 / D-11).
|
|
24
|
+
*
|
|
25
|
+
* THE GUARD ORDER IS A CONTRACT (§3.4, R-P1-10 / R-P1-11) and it is not
|
|
26
|
+
* alphabetical:
|
|
27
|
+
*
|
|
28
|
+
* status (no guard) - "what is on?" is a question, answerable even in a
|
|
29
|
+
* `--no-team` session, which is where users ask it.
|
|
30
|
+
* off (no guard) - leaving is always possible, even mid-dispatch.
|
|
31
|
+
* registered? - a `--no-team` session cannot grow the tool (I-7).
|
|
32
|
+
* enabled? - NOT a refusal: it is turned on for this session
|
|
33
|
+
* (D-5), because "start now" must not detour through a
|
|
34
|
+
* settings screen. Nothing is persisted.
|
|
35
|
+
* not busy / idle - two separate sentences, because both would otherwise
|
|
36
|
+
* surface as `task`'s own refusal card.
|
|
37
|
+
* key present? - names the missing provider.
|
|
38
|
+
* on - arm only; the next message dispatches.
|
|
39
|
+
*
|
|
40
|
+
* EVERY `armForcedTeam()` / `disarmForcedTeam()` CALL SITE BELOW IS FOLLOWED BY A
|
|
41
|
+
* `ctx.notify` / `ctx.toast` IN THE SAME BRANCH (P1-6 / AC-28), because arming
|
|
42
|
+
* itself produces no `AgentEvent` and therefore no render: without the notice the
|
|
43
|
+
* `Team 4` chip would wait for an unrelated state change before appearing.
|
|
44
|
+
*/
|
|
45
|
+
import { buildForcedDirective } from '../team/forced-dispatch.js';
|
|
46
|
+
import { DEFAULT_FORCED_COUNT, planForced, planNotice, readTeamIntent, } from '../team/forced-mode.js';
|
|
47
|
+
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
48
|
+
/**
|
|
49
|
+
* The subagent model line, phrased by the same authority the settings root row
|
|
50
|
+
* uses (`subagentModelSummary`), fed the six inputs only this layer can gather.
|
|
51
|
+
*
|
|
52
|
+
* IT IS A FUNCTION AND NOT AN INLINE CALL because the six inputs are exactly
|
|
53
|
+
* what a second copy gets wrong, and the settings root row feeds the same
|
|
54
|
+
* authority (`subagentModelSummary`) the same six - which is what keeps the two
|
|
55
|
+
* surfaces from describing one setting differently.
|
|
56
|
+
*/
|
|
57
|
+
function modelLine(ctx) {
|
|
58
|
+
const config = ctx.controller.getConfig();
|
|
59
|
+
const team = ctx.controller.getTeamConfig();
|
|
60
|
+
const fast = config.fast;
|
|
61
|
+
return subagentModelSummary({
|
|
62
|
+
mode: team.model,
|
|
63
|
+
modelProfileId: team.modelProfileId,
|
|
64
|
+
mainConnection: `${config.provider}:${config.model}`,
|
|
65
|
+
fastConnection: fast.enabled && fast.model
|
|
66
|
+
? `${fast.provider || config.provider}:${fast.model}` : null,
|
|
67
|
+
profiles: config.modelProfiles,
|
|
68
|
+
hasKey: (id) => ctx.controller.hasApiKey(id),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The no-argument answer: five lines, honest in every session state (§5.1).
|
|
73
|
+
*
|
|
74
|
+
* IT RUNS BEFORE EVERY GUARD. "How do I use this?" is a question, not a request,
|
|
75
|
+
* and it is answerable in a `--no-team` session - which is exactly where the user
|
|
76
|
+
* most needs the answer.
|
|
77
|
+
*/
|
|
78
|
+
function reportStatus(ctx) {
|
|
79
|
+
const team = ctx.controller.getTeamConfig();
|
|
80
|
+
const forced = ctx.controller.getForcedTeam();
|
|
81
|
+
const registered = ctx.controller.isTeamRegistered();
|
|
82
|
+
const lines = [
|
|
83
|
+
forced
|
|
84
|
+
? `Forced team mode: on, ${forced.count} subagents per turn.`
|
|
85
|
+
: 'Forced team mode: off.',
|
|
86
|
+
registered
|
|
87
|
+
? `Team: ${ctx.controller.isTeamEnabled() ? 'on' : 'off'}; `
|
|
88
|
+
+ `${team.maxSubagents} per dispatch, ${team.maxConcurrent} at once; `
|
|
89
|
+
+ `supervisor ${team.overseer ? 'on' : 'off'}.`
|
|
90
|
+
: 'Team: off for this session (started with --no-team); /team cannot dispatch.',
|
|
91
|
+
`Subagent model: ${modelLine(ctx)}`,
|
|
92
|
+
'Usage: /team <brief> arms it and starts now; /team off leaves it.',
|
|
93
|
+
'Settings > Team has the persistent switches (mode, width, supervisor).',
|
|
94
|
+
];
|
|
95
|
+
// DISCLOSED, NEVER REFUSED (§5.1 third degraded form): plan mode narrows what a
|
|
96
|
+
// child may do, and a user who is told so will not read the restraint as a bug.
|
|
97
|
+
if (forced && ctx.controller.getAgentMode() === 'plan') {
|
|
98
|
+
lines.push('Plan mode is on: dispatch read-only research, then submit_plan.');
|
|
99
|
+
}
|
|
100
|
+
ctx.notify('info', lines.join('\n'));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Guard 5's sentence. The provider is named because "no API key" alone leaves the
|
|
104
|
+
* user guessing which of their configured providers is the empty one.
|
|
105
|
+
*/
|
|
106
|
+
function reportMissingKey(ctx, team) {
|
|
107
|
+
const config = ctx.controller.getConfig();
|
|
108
|
+
const resolution = ctx.controller.subagentModelResolution();
|
|
109
|
+
const provider = resolution.effective === 'main'
|
|
110
|
+
? config.provider : resolution.ref.providerId;
|
|
111
|
+
const note = team.model === 'fast' && !ctx.controller.hasApiKey(undefined, 'fast')
|
|
112
|
+
? ' Fast tier has no key either; children will fail and the report will say so.'
|
|
113
|
+
: '';
|
|
114
|
+
ctx.notify('warn', `No API key for ${provider}. Set one with /settings or an environment variable.${note}`);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The capability guards, in §3.4's exact order: registered -> enabled -> busy ->
|
|
118
|
+
* running -> key.
|
|
119
|
+
*
|
|
120
|
+
* RETURNS `true` WHEN THE CALLER MAY CONTINUE. Step 5 is the one place a guard
|
|
121
|
+
* becomes an ACTION (D-5) and it sits at its documented position rather than at
|
|
122
|
+
* the end for a behavioural reason: the user's own sentence is "start now", so
|
|
123
|
+
* the switch is turned on before any sentence about *why we cannot* is composed.
|
|
124
|
+
* The action is still followed by a notice in the same branch (P1-6 / AC-28),
|
|
125
|
+
* because `setTeamEnabled()` rebuilds the prompt and produces no `AgentEvent`, so
|
|
126
|
+
* without it the `Team N` chip would wait for an unrelated render.
|
|
127
|
+
*/
|
|
128
|
+
function passGuards(ctx, team) {
|
|
129
|
+
if (!ctx.controller.isTeamRegistered()) {
|
|
130
|
+
ctx.notify('warn', 'Team mode is off for this session (started with --no-team). '
|
|
131
|
+
+ 'Restart without --no-team to use /team.');
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// STEP 5, THE ONE GUARD THAT IS NOT A REFUSAL: "start now" must not detour
|
|
135
|
+
// through a settings screen. Nothing is persisted here (I-4) - `Settings >
|
|
136
|
+
// Team` stays the only persistent writer.
|
|
137
|
+
if (!ctx.controller.isTeamEnabled()) {
|
|
138
|
+
ctx.controller.setTeamEnabled(true);
|
|
139
|
+
ctx.notify('info', 'Team mode was off; turned on for this session (Settings > Team saves it).');
|
|
140
|
+
}
|
|
141
|
+
// The team lock and the lead's turn are separate refusals for one reason: both
|
|
142
|
+
// would collide with `task`'s own BUSY_REFUSAL, and the user would see a failed
|
|
143
|
+
// tool card instead of a sentence explaining why nothing ran.
|
|
144
|
+
if (ctx.controller.isTeamBusy()) {
|
|
145
|
+
ctx.notify('warn', 'A team dispatch is already running. '
|
|
146
|
+
+ 'Wait for its report, or press Esc to stop it.');
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (ctx.controller.isRunning()) {
|
|
150
|
+
ctx.notify('warn', 'A turn is in progress. Press Esc to stop it, then run /team again.');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (!ctx.controller.hasApiKey()) {
|
|
154
|
+
reportMissingKey(ctx, team);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
/** The four parse failures, each with the one sentence that fixes it. */
|
|
160
|
+
function reportParseError(ctx, intent) {
|
|
161
|
+
if (intent.error === 'count') {
|
|
162
|
+
ctx.notify('warn', '/team: -n needs a number of 1 or more, e.g. -n 4.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (intent.error === 'conflict') {
|
|
166
|
+
ctx.notify('warn', '/team: use -n or --parts, not both.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (intent.error === 'flag') {
|
|
170
|
+
ctx.notify('warn', `/team: unknown option "${intent.token ?? ''}". Try /team for usage.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
ctx.notify('warn', '/team <brief> - describe the work; add -n <count> to fix the fan-out.');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* `/team off`: disarm the session, and say plainly that the CAPABILITY was not
|
|
177
|
+
* touched.
|
|
178
|
+
*
|
|
179
|
+
* THE SECOND HALF OF THE SENTENCE IS THE POINT (D-2 / D-11): with the switch
|
|
180
|
+
* moved to settings, "off" can only ever mean "this conversation stops being
|
|
181
|
+
* forced". A user who reads it as "team mode is now disabled everywhere" would
|
|
182
|
+
* next find `/team` still working tomorrow, and no test would describe that as a
|
|
183
|
+
* defect because no single surface would be wrong.
|
|
184
|
+
*/
|
|
185
|
+
function leaveForcedMode(ctx) {
|
|
186
|
+
if (!ctx.controller.getForcedTeam()) {
|
|
187
|
+
// IDEMPOTENT, and it says so rather than reporting a change it did not make.
|
|
188
|
+
ctx.notify('info', 'Forced team mode was not on.');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
ctx.controller.disarmForcedTeam();
|
|
192
|
+
ctx.notify('info', 'Forced team mode off. Team capability unchanged (Settings > Team).');
|
|
193
|
+
}
|
|
194
|
+
/** `/team on`: arm with the default count and stop there - no turn is submitted. */
|
|
195
|
+
function enterForcedMode(ctx) {
|
|
196
|
+
if (!passGuards(ctx, ctx.controller.getTeamConfig()))
|
|
197
|
+
return;
|
|
198
|
+
const adopted = ctx.controller.armForcedTeam(DEFAULT_FORCED_COUNT);
|
|
199
|
+
ctx.notify('info', `Forced team mode on: ${adopted} subagents per turn. `
|
|
200
|
+
+ 'Your next message dispatches.');
|
|
201
|
+
}
|
|
202
|
+
/** `/team [-n n|--parts a,b] [--read-only] <brief>`: arm and submit in one act. */
|
|
203
|
+
function armAndSubmit(ctx, intent) {
|
|
204
|
+
const team = ctx.controller.getTeamConfig();
|
|
205
|
+
if (!passGuards(ctx, team))
|
|
206
|
+
return;
|
|
207
|
+
const planned = planForced(intent.args, team);
|
|
208
|
+
// ARMED BEFORE THE TURN IS SUBMITTED, and with the SAME narrowed number the
|
|
209
|
+
// directive names: the later turns reuse this count, so the first turn and the
|
|
210
|
+
// rest of the session cannot disagree about how wide the fan-out is (D-7).
|
|
211
|
+
ctx.controller.armForcedTeam(planned.count);
|
|
212
|
+
ctx.notify('info', planNotice(planned, team));
|
|
213
|
+
const directive = buildForcedDirective({
|
|
214
|
+
brief: planned.brief,
|
|
215
|
+
count: planned.count,
|
|
216
|
+
readOnly: planned.readOnly,
|
|
217
|
+
parts: planned.parts,
|
|
218
|
+
maxSubagents: team.maxSubagents,
|
|
219
|
+
clamped: planned.requested > planned.count,
|
|
220
|
+
requested: planned.requested,
|
|
221
|
+
planMode: ctx.controller.getAgentMode() === 'plan',
|
|
222
|
+
});
|
|
223
|
+
// `userInitiated: false`: a directive the user did not type belongs in neither
|
|
224
|
+
// the prompt history nor the hint-fade counter (todo-plan-followthrough C-12).
|
|
225
|
+
ctx.submit(directive, { userInitiated: false });
|
|
226
|
+
}
|
|
227
|
+
/** The command body; `registerTeamCommand` is the only registration site. */
|
|
228
|
+
export function runTeamCommand(ctx) {
|
|
229
|
+
const intent = readTeamIntent(ctx.args);
|
|
230
|
+
if (intent.kind === 'status') {
|
|
231
|
+
reportStatus(ctx);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (intent.kind === 'off') {
|
|
235
|
+
leaveForcedMode(ctx);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (intent.kind === 'on') {
|
|
239
|
+
enterForcedMode(ctx);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (intent.kind === 'error') {
|
|
243
|
+
// PARSED BEFORE THE CAPABILITY GUARDS, unlike `on` / `<brief>`. A syntax hint
|
|
244
|
+
// is the answer to a syntax mistake in ANY session: sending a `--no-team` user
|
|
245
|
+
// to fix their flags first would send them to repair the one thing that is
|
|
246
|
+
// not broken.
|
|
247
|
+
reportParseError(ctx, intent);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
armAndSubmit(ctx, intent);
|
|
251
|
+
}
|
|
252
|
+
/** Registered from `builtins.ts` with one line, next to every other subsystem. */
|
|
253
|
+
export function registerTeamCommand(registry) {
|
|
254
|
+
registry.register({
|
|
255
|
+
name: 'team',
|
|
256
|
+
description: 'Force team mode and start now: /team <brief>; /team [on|off|status]',
|
|
257
|
+
run: (ctx) => { runTeamCommand(ctx); },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -16,6 +16,7 @@ import { parseThresholdInput } from '../config/schema.js';
|
|
|
16
16
|
import { formatCost, formatTokens } from '../agent/usage.js';
|
|
17
17
|
import { computeCost } from '../agent/usage.js';
|
|
18
18
|
import { COMPACTION_LIMITS } from './limits.js';
|
|
19
|
+
import { compactionReasonLine } from './reason-text.js';
|
|
19
20
|
import { findArchive, listArchives } from './archive.js';
|
|
20
21
|
/**
|
|
21
22
|
* `/compact status` — THE GUARANTEED REPORTING SURFACE (§6.5 / AC-17).
|
|
@@ -46,7 +47,7 @@ export function formatCompactionStatus(ctx) {
|
|
|
46
47
|
return lines.join('\n');
|
|
47
48
|
}
|
|
48
49
|
const state = snapshot.selfDisabled
|
|
49
|
-
? `self-disabled (${snapshot.selfDisabledReason ?? 'no progress'})`
|
|
50
|
+
? `self-disabled (${compactionReasonLine(snapshot.selfDisabledReason ?? 'no progress')})`
|
|
50
51
|
: controller.isCompactionEnabled()
|
|
51
52
|
? snapshot.live
|
|
52
53
|
? 'on'
|
|
@@ -61,7 +62,13 @@ export function formatCompactionStatus(ctx) {
|
|
|
61
62
|
`[${p.source === 'usage' ? 'measured' : 'estimated'}]`);
|
|
62
63
|
lines.push(` Automatic trigger: occupancy >= ${(config.threshold * 100).toFixed(2)}%; ` +
|
|
63
64
|
`manual: /compact (${Math.ceil(p.contextWindow * config.threshold)} tokens)`);
|
|
65
|
+
// TWO LINES, BECAUSE THE TAIL GUARANTEE IS NOW CONDITIONAL (never-fires B).
|
|
66
|
+
// `keepRecentTurns` stays the promise; the second line is what happens when a run
|
|
67
|
+
// has too few user turns for that promise to be satisfiable, and saying it here
|
|
68
|
+
// is what makes the card's `kept M of N` readable rather than surprising.
|
|
64
69
|
lines.push(` Keeps: the original task + ${config.keepRecentTurns} recent turns, verbatim`);
|
|
70
|
+
lines.push(` Relaxes to: the last ${COMPACTION_LIMITS.minRetainedMessages} messages ` +
|
|
71
|
+
'when a run has too few user turns for that');
|
|
65
72
|
lines.push(` Summarizer: ${snapshot.model || '(none)'}` +
|
|
66
73
|
(config.useFastTier ? ' (fast tier preferred)' : ' (fast tier not used)'));
|
|
67
74
|
lines.push(' On summarize failure: ' + (config.onFailure === 'truncate'
|
|
@@ -79,8 +86,14 @@ export function formatCompactionStatus(ctx) {
|
|
|
79
86
|
if (declined > 0) {
|
|
80
87
|
sessionNotes.push(`${declined} ${declined === 1 ? 'checkpoint' : 'checkpoints'} declined`);
|
|
81
88
|
}
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
// ATTEMPTS, THEN APPLIED (never-fires C). `compactions` counts every card the
|
|
90
|
+
// user saw, which now includes the ones that declined, so reporting it alone
|
|
91
|
+
// would overstate how much the feature actually achieved.
|
|
92
|
+
const attempts = snapshot.applied === undefined
|
|
93
|
+
? `${snapshot.compactions} ${snapshot.compactions === 1 ? 'compaction' : 'compactions'}`
|
|
94
|
+
: `${snapshot.compactions} ${snapshot.compactions === 1 ? 'attempt' : 'attempts'}` +
|
|
95
|
+
` (${snapshot.applied} applied)`;
|
|
96
|
+
lines.push(` This session: ${attempts}` +
|
|
84
97
|
(sessionNotes.length > 0 ? ` (${sessionNotes.join(', ')})` : '') +
|
|
85
98
|
`, ${formatTokens(snapshot.tokensReclaimed)} tokens reclaimed`);
|
|
86
99
|
const cost = snapshot.pricingUnknown
|
|
@@ -93,6 +93,7 @@ export declare class Compactor implements ContextManager {
|
|
|
93
93
|
private compactionsThisRun;
|
|
94
94
|
private lastCompactionTurn;
|
|
95
95
|
private compactions;
|
|
96
|
+
private appliedCompactions;
|
|
96
97
|
private declined;
|
|
97
98
|
private tokensReclaimed;
|
|
98
99
|
private sessionUsage;
|
|
@@ -132,8 +133,31 @@ export declare class Compactor implements ContextManager {
|
|
|
132
133
|
private callOnce;
|
|
133
134
|
private checkCandidate;
|
|
134
135
|
private isCurrent;
|
|
136
|
+
/**
|
|
137
|
+
* Open the card for THIS attempt (C).
|
|
138
|
+
*
|
|
139
|
+
* CALLED FROM `compact()`'S ENTRY, so the two pre-model rejections are visible
|
|
140
|
+
* too. The old position - inside `callOnce` - made an attempt that never called
|
|
141
|
+
* a model indistinguishable from a checkpoint that never fired, which is exactly
|
|
142
|
+
* how this feature spent 96 attempts without a single user-visible symptom.
|
|
143
|
+
*
|
|
144
|
+
* `state.index !== undefined` keeps it idempotent: `callOnce` no longer calls it,
|
|
145
|
+
* but the guard stays so a future re-announce cannot open a second card for one
|
|
146
|
+
* attempt.
|
|
147
|
+
*/
|
|
135
148
|
private announce;
|
|
149
|
+
/** Publish the current phase, for the live card only. */
|
|
150
|
+
private phase;
|
|
136
151
|
private keep;
|
|
152
|
+
/**
|
|
153
|
+
* The relaxation disclosure, as record fields.
|
|
154
|
+
*
|
|
155
|
+
* ONE PLACE BUILDS IT, so the card, the reducer and the diagnostic log cannot
|
|
156
|
+
* describe one relaxed cut three different ways. Absent entirely when the first
|
|
157
|
+
* planning pass found a cut, which is what keeps a normal compaction's record
|
|
158
|
+
* byte-identical to the round before this change.
|
|
159
|
+
*/
|
|
160
|
+
private relaxedFields;
|
|
137
161
|
private finishRecord;
|
|
138
162
|
/** Commit only the engine/host verdict; candidates have no authority before this point. */
|
|
139
163
|
settleOperation(verdict: {
|
|
@@ -152,6 +176,7 @@ export declare class Compactor implements ContextManager {
|
|
|
152
176
|
sessionTotals(): {
|
|
153
177
|
selfDisabledReason?: string | undefined;
|
|
154
178
|
compactions: number;
|
|
179
|
+
applied: number;
|
|
155
180
|
declined: number;
|
|
156
181
|
tokensReclaimed: number;
|
|
157
182
|
usage: TokenUsage;
|