@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/agent/controller.js
CHANGED
|
@@ -21,6 +21,9 @@ import { createFastOverseerProvider } from '../team/overseer.js';
|
|
|
21
21
|
import { createTaskTool } from '../team/task-tool.js';
|
|
22
22
|
import { resolveSubagentModel } from '../team/model.js';
|
|
23
23
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
24
|
+
// THE THIRD LAYER's prompt half (team-command-forced-dispatch §3.6): spliced
|
|
25
|
+
// after `<team_mode>` and before the subagent block, only while armed.
|
|
26
|
+
import { buildForcedTeamBlock } from '../team/forced-prompt.js';
|
|
24
27
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
25
28
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
26
29
|
import { buildBackgroundJobsBlock, buildBackgroundServicesBlock } from '../proc/prompt.js';
|
|
@@ -45,12 +48,14 @@ import { TodoStore } from '../todo/store.js';
|
|
|
45
48
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
46
49
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
47
50
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
48
|
-
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
51
|
+
import { ASK_GUARD_LIMITS, DENY_ALL_HUMAN_INPUT, questionFingerprint, } from '../tools/human-input.js';
|
|
49
52
|
import { getLogger } from '../logging/logger.js';
|
|
50
53
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
51
54
|
import { SkillService } from '../skills/service.js';
|
|
52
55
|
import { createSkillTools } from '../skills/tools.js';
|
|
56
|
+
import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
53
57
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
58
|
+
import { buildSkillTriageBlock } from './skill-triage-prompt.js';
|
|
54
59
|
import { resetConversationWith, } from './conversation-reset.js';
|
|
55
60
|
import { archiveClearedConversation, } from '../session/clear-archive.js';
|
|
56
61
|
/**
|
|
@@ -92,6 +97,28 @@ export class AgentController {
|
|
|
92
97
|
skills;
|
|
93
98
|
skillsEnabled;
|
|
94
99
|
onSkillsChanged;
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
// Skill triage (skill-triage-and-unbounded-ask §4.2.5) — PER-TURN state.
|
|
102
|
+
//
|
|
103
|
+
// Every field here is written in `prepareUserTurn` and cleared on `agent_end`,
|
|
104
|
+
// because a stale candidate set in the prompt is worse than no candidate set:
|
|
105
|
+
// it points the model at a skill chosen for a task it is no longer doing.
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* This turn's routing result, or `null` when triage is off or nothing scored.
|
|
109
|
+
*
|
|
110
|
+
* It also carries the task it was computed for (`SkillRoutingReport.task`), so
|
|
111
|
+
* there is no separate copy of the turn's text to keep in step with it.
|
|
112
|
+
*/
|
|
113
|
+
turnRouting = null;
|
|
114
|
+
/** The previous turn's routing, for `/skills route` with no argument. */
|
|
115
|
+
lastSkillRouting = null;
|
|
116
|
+
/** Did this turn call `skill(name)` successfully? The only proof of adoption. */
|
|
117
|
+
skillLoadedThisTurn = false;
|
|
118
|
+
/** Tool calls this turn, EXCLUDING `skill` / `skill_find` (see §4.2.7). */
|
|
119
|
+
toolCallsThisTurn = 0;
|
|
120
|
+
/** Skills already reported as "not loaded" — a session says it once (I-T5). */
|
|
121
|
+
skillTriageNoticed = new Set();
|
|
95
122
|
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
96
123
|
cwd;
|
|
97
124
|
/**
|
|
@@ -124,7 +151,20 @@ export class AgentController {
|
|
|
124
151
|
// -----------------------------------------------------------------------
|
|
125
152
|
effectiveMode;
|
|
126
153
|
pendingMode = null;
|
|
127
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Fingerprints of the question SETS the user ANSWERED in this turn. Bounded,
|
|
156
|
+
* and the bound is storage hygiene rather than a refusal rule — see
|
|
157
|
+
* `ASK_GUARD_LIMITS`.
|
|
158
|
+
*
|
|
159
|
+
* Replaces a numeric round counter that capped `ask_user` at four calls. There
|
|
160
|
+
* is no cap now; the only thing this prevents is the same batch of questions
|
|
161
|
+
* being put to the user twice, which is noise rather than information.
|
|
162
|
+
*
|
|
163
|
+
* Cleared in `prepareUserTurn` for a NEW turn only. A steer continuation is the
|
|
164
|
+
* same turn — clearing there would disable the guard exactly when a long turn
|
|
165
|
+
* most needs it (§4.1.6).
|
|
166
|
+
*/
|
|
167
|
+
askedFingerprints = new Set();
|
|
128
168
|
/** Static: whether this session could ever render a question overlay. */
|
|
129
169
|
planInteractive;
|
|
130
170
|
// -----------------------------------------------------------------------
|
|
@@ -135,18 +175,32 @@ export class AgentController {
|
|
|
135
175
|
// immutable for the life of the session (see the note in the constructor),
|
|
136
176
|
// so `/team on` in a session started with `--no-team` CANNOT add it.
|
|
137
177
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
178
|
+
// The LIVE switch is NOT a second field (team-command-forced-dispatch §3.5 /
|
|
179
|
+
// P1-4). It used to be one — `teamEnabled`, flipped by `/team on|off` and
|
|
180
|
+
// hand-synchronised with `config.team.enabled` — which was a second truth
|
|
181
|
+
// waiting to separate. The moment the settings screen grew a `Team mode` row,
|
|
182
|
+
// `applyModelSettingsSnapshot()` replaced `this.config` WITHOUT touching that
|
|
183
|
+
// copy, and the screen would have said `on` while `task` still answered
|
|
184
|
+
// TEAM_OFF_REFUSAL. `isTeamEnabled()` is now one derivation over
|
|
185
|
+
// `config.team.enabled`, and `setTeamEnabled()` is one write to it.
|
|
141
186
|
//
|
|
142
|
-
// Conflating
|
|
143
|
-
// registered, every call would come back
|
|
144
|
-
// have no way to discover why (D-17 / R-18).
|
|
187
|
+
// Conflating the two flags was P0-2 in the original feature: the prompt would
|
|
188
|
+
// advertise a tool that was never registered, every call would come back
|
|
189
|
+
// "unknown tool", and the model would have no way to discover why (D-17 / R-18).
|
|
145
190
|
// -----------------------------------------------------------------------
|
|
146
191
|
teamRegistered;
|
|
147
|
-
teamEnabled;
|
|
148
192
|
teamRuntime;
|
|
149
193
|
// -----------------------------------------------------------------------
|
|
194
|
+
// Forced team mode (team-command-forced-dispatch §3.5 / D-4).
|
|
195
|
+
//
|
|
196
|
+
// SESSION STATE, NEVER PERSISTED: `null` means "not armed". `/team <brief>`
|
|
197
|
+
// and `/team on` set it, `/team off` clears it, and the process exiting is the
|
|
198
|
+
// third way it ends. It is not a `config.json` key on purpose — a persisted
|
|
199
|
+
// "always force a fan-out" would be inherited by `aragon exec` and by every
|
|
200
|
+
// launch after one experiment, at four children a turn (D-4 / AC-24).
|
|
201
|
+
// -----------------------------------------------------------------------
|
|
202
|
+
forcedTeam = null;
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
150
204
|
// Todo planning (todo-plan-execution §3.6a) — THE SAME TWO FLAGS, FOR THE
|
|
151
205
|
// SAME REASON.
|
|
152
206
|
//
|
|
@@ -389,8 +443,10 @@ export class AgentController {
|
|
|
389
443
|
const fastTier = resolveFastTier(config, (id, role) => this.hasApiKey(id, role));
|
|
390
444
|
const fastRegistered = fastTier.ok;
|
|
391
445
|
// --- Team mode ---------------------------------------------------------
|
|
446
|
+
// ONE FLAG HERE (team-command-forced-dispatch P1-4): this is the IMMUTABLE
|
|
447
|
+
// snapshot of "is `task` in this.tools at all". The live switch is
|
|
448
|
+
// `config.team.enabled`, read through `isTeamEnabled()` — no second copy.
|
|
392
449
|
this.teamRegistered = config.team.enabled;
|
|
393
|
-
this.teamEnabled = config.team.enabled;
|
|
394
450
|
this.teamRuntime = this.teamRegistered
|
|
395
451
|
? new TeamRuntime({
|
|
396
452
|
getConfig: () => this.config,
|
|
@@ -491,7 +547,12 @@ export class AgentController {
|
|
|
491
547
|
this.jobsRegistered = config.jobs.enabled && config.bash.background;
|
|
492
548
|
this.jobMirror =
|
|
493
549
|
this.jobsRegistered && config.jobs.persistLog
|
|
494
|
-
? new JobLogMirror({
|
|
550
|
+
? new JobLogMirror({
|
|
551
|
+
root: () => join(getHomeRoot(), 'jobs', this.jobSessionTag),
|
|
552
|
+
// NAMED, NOT DERIVED. The mirror sweeps only this directory; the session
|
|
553
|
+
// tagging above is what keeps its own run out of the sweep's reach.
|
|
554
|
+
jobsRoot: () => join(getHomeRoot(), 'jobs'),
|
|
555
|
+
})
|
|
495
556
|
: undefined;
|
|
496
557
|
this.procs = new ProcSupervisor({
|
|
497
558
|
readyTimeoutMs: () => this.config.bash.readyTimeoutMs,
|
|
@@ -585,7 +646,13 @@ export class AgentController {
|
|
|
585
646
|
// (or 11 with skills) it is today.
|
|
586
647
|
planTools: createPlanTools({
|
|
587
648
|
gate: humanInput,
|
|
588
|
-
|
|
649
|
+
// TWO CALLBACKS, AND THE SPLIT IS LOAD-BEARING (§4.1.2). One
|
|
650
|
+
// read-and-write accessor could only be called before the question
|
|
651
|
+
// (recording an answer nobody gave, which makes "a cancelled round stays
|
|
652
|
+
// askable" impossible) or after it (which cannot stop a repeat). The guard
|
|
653
|
+
// is "read before, write only after a real answer".
|
|
654
|
+
hasAnsweredQuestions: (questions) => this.hasAnswered(questions),
|
|
655
|
+
noteAnsweredQuestions: (questions) => this.noteAnswered(questions),
|
|
589
656
|
withHumanWait: (fn) => this.withHumanWait(fn),
|
|
590
657
|
onPlanApproved: () => this.setAgentMode('build', { force: true }),
|
|
591
658
|
}),
|
|
@@ -663,7 +730,10 @@ export class AgentController {
|
|
|
663
730
|
createTaskTool({
|
|
664
731
|
runtime: this.teamRuntime,
|
|
665
732
|
// Read LIVE: `/team off` flips the flag, it cannot unregister.
|
|
666
|
-
|
|
733
|
+
// THROUGH THE METHOD, not the config: one derivation, so the
|
|
734
|
+
// task tool, `<team_mode>` and the settings screen can never
|
|
735
|
+
// disagree about whether this session may dispatch (P1-4).
|
|
736
|
+
isTeamEnabled: () => this.isTeamEnabled(),
|
|
667
737
|
maxSubagents: () => this.config.team.maxSubagents,
|
|
668
738
|
hasApiKey: () => this.hasApiKey(),
|
|
669
739
|
activeProvider: () => this.config.provider,
|
|
@@ -909,6 +979,17 @@ export class AgentController {
|
|
|
909
979
|
this.activeToolCalls += 1;
|
|
910
980
|
if (event.type === 'tool_execution_end') {
|
|
911
981
|
this.activeToolCalls = Math.max(0, this.activeToolCalls - 1);
|
|
982
|
+
// The ONLY proof a skill was adopted is a successful `skill` call. Reading
|
|
983
|
+
// the model's prose for a skill name would be a heuristic; this is a fact
|
|
984
|
+
// the executor already reports.
|
|
985
|
+
if (event.toolName === 'skill' && event.isError !== true)
|
|
986
|
+
this.skillLoadedThisTurn = true;
|
|
987
|
+
// `skill` / `skill_find` do not count as "work done": the notice below is
|
|
988
|
+
// for a turn that changed something without reading its skill, and a turn
|
|
989
|
+
// that ONLY searched for a skill is a turn that already tried.
|
|
990
|
+
if (event.toolName !== 'skill' && event.toolName !== 'skill_find') {
|
|
991
|
+
this.toolCallsThisTurn += 1;
|
|
992
|
+
}
|
|
912
993
|
// THE ONE PUSH SITE. A notice that could not be pushed here is picked up
|
|
913
994
|
// by the next tool result (the pull channel) or by the next user turn
|
|
914
995
|
// (the fill channel), so nothing is lost by declining.
|
|
@@ -925,6 +1006,9 @@ export class AgentController {
|
|
|
925
1006
|
// required: `turn_start` covers the next turn of the same run, and this
|
|
926
1007
|
// covers a run that ended with a notice stranded in the queue.
|
|
927
1008
|
this.jobRelay?.requeueUndelivered();
|
|
1009
|
+
// AFTER the requeue, so a turn that ends with a stranded notice still gets
|
|
1010
|
+
// its feedback line. Order between the two is otherwise irrelevant.
|
|
1011
|
+
this.maybeReportSkillTriage();
|
|
928
1012
|
}
|
|
929
1013
|
});
|
|
930
1014
|
if (this.fast) {
|
|
@@ -973,23 +1057,52 @@ export class AgentController {
|
|
|
973
1057
|
const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
|
|
974
1058
|
const fastAvailable = config === this.config ? this.fast?.available() === true
|
|
975
1059
|
: this.fast?.isRegistered() && config.fast.enabled && tier.ok;
|
|
1060
|
+
const skillsCatalog = this.skillsEnabled ? this.skills.catalogBlock() : '';
|
|
1061
|
+
// READ LIVE FROM THE SERVICE, never `config.skills` (§4.2.5 / R-12).
|
|
1062
|
+
// `SkillService.updateConfig` replaces the SERVICE's own config object, so
|
|
1063
|
+
// `this.config.skills` goes stale the moment `/skills triage off` runs — and a
|
|
1064
|
+
// stale read would leave the block in the prompt until the next launch, which
|
|
1065
|
+
// looks exactly like a broken switch. Same discipline as `/todo off`.
|
|
1066
|
+
const triageOn = this.skillsEnabled && this.skills.getConfig().triage === true;
|
|
1067
|
+
// An empty catalog means there is nothing to triage against, so the block is
|
|
1068
|
+
// absent rather than present-and-useless (I-T1 / AC-T18).
|
|
1069
|
+
const triageBlock = triageOn && skillsCatalog.length > 0
|
|
1070
|
+
? buildSkillTriageBlock({ candidates: this.turnRouting?.candidates ?? [] })
|
|
1071
|
+
: '';
|
|
976
1072
|
return buildSystemPrompt({
|
|
977
1073
|
cwd: this.cwd,
|
|
978
1074
|
tools: this.tools,
|
|
979
|
-
skillsBlock: this.skillsEnabled ?
|
|
1075
|
+
skillsBlock: this.skillsEnabled ? skillsCatalog + this.skills.alwaysBlock() : '',
|
|
1076
|
+
// Immediately after the catalog it refers to, before the plan block whose
|
|
1077
|
+
// first step defers to it (§4.2.4).
|
|
1078
|
+
skillTriageBlock: triageBlock,
|
|
980
1079
|
agentMode: this.effectiveMode,
|
|
981
1080
|
planInteractive: this.planInteractive,
|
|
982
|
-
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
|
|
1081
|
+
// BOTH halves, and both are required (§4.5). `teamRegistered` false means
|
|
1082
|
+
// there is no `task` tool to advertise; `config.team.enabled` false means
|
|
1083
|
+
// the user turned the capability off (in settings — the only writer).
|
|
1084
|
+
// Advertising in either case would describe a capability the model does not
|
|
1085
|
+
// have. Read through the field rather than `this.isTeamEnabled()` so the
|
|
1086
|
+
// sentence and the code agree, as the two flags always have here (P1-4).
|
|
1087
|
+
teamBlock: this.teamRegistered && this.config.team.enabled
|
|
988
1088
|
? buildTeamBlock({
|
|
989
1089
|
maxSubagents: config.team.maxSubagents,
|
|
990
1090
|
maxConcurrent: config.team.maxConcurrent,
|
|
991
1091
|
})
|
|
992
1092
|
: '',
|
|
1093
|
+
// THE THIRD LAYER (team-command-forced-dispatch §3.6): "you MUST dispatch"
|
|
1094
|
+
// on top of `<team_mode>`'s "you may". THREE conditions and all three are
|
|
1095
|
+
// load-bearing - armed for this session, the tool actually registered, and
|
|
1096
|
+
// the capability still on. The last two are `<team_mode>`'s own pair, so a
|
|
1097
|
+
// session can never be told to use a tool the prompt above it does not
|
|
1098
|
+
// mention. Empty everywhere else, which is what keeps I-2 (byte-identical
|
|
1099
|
+
// prompt) true for every session that never types `/team`.
|
|
1100
|
+
forcedTeamBlock: this.forcedTeam && this.teamRegistered && this.config.team.enabled
|
|
1101
|
+
? buildForcedTeamBlock({
|
|
1102
|
+
count: this.forcedTeam.count,
|
|
1103
|
+
planMode: this.effectiveMode === 'plan',
|
|
1104
|
+
})
|
|
1105
|
+
: '',
|
|
993
1106
|
// BOTH FLAGS, exactly as team mode uses both, and for the identical
|
|
994
1107
|
// reason: `todoRegistered` false means there is no `todo_write` to
|
|
995
1108
|
// advertise, `todoEnabled` false means the user turned it off for this
|
|
@@ -1183,7 +1296,7 @@ export class AgentController {
|
|
|
1183
1296
|
}
|
|
1184
1297
|
/** Prepare before clearing the plan, then enter the engine without yielding. */
|
|
1185
1298
|
async startPrompt(text, options, request) {
|
|
1186
|
-
this.prepareUserTurn(text);
|
|
1299
|
+
this.prepareUserTurn(text, { steering: false });
|
|
1187
1300
|
if (request !== this.startupSequence) {
|
|
1188
1301
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1189
1302
|
}
|
|
@@ -1212,10 +1325,39 @@ export class AgentController {
|
|
|
1212
1325
|
// failure AC-25 exists to prevent.
|
|
1213
1326
|
this.jobRelay?.flushForTurn();
|
|
1214
1327
|
}
|
|
1215
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* THE BOUNDARY OF ONE TURN — and there are TWO call sites, which is the reason
|
|
1330
|
+
* this takes an explicit flag instead of resetting everything unconditionally.
|
|
1331
|
+
*
|
|
1332
|
+
* `startPrompt` passes `{ steering: false }`: the user typed something new, so
|
|
1333
|
+
* this really is a new turn. `continue()` passes `{ steering: true }` with the
|
|
1334
|
+
* CONCATENATED queued steering text, which resumes the SAME turn.
|
|
1335
|
+
*
|
|
1336
|
+
* A steer continuation must not re-triage (the candidates would change mid-turn,
|
|
1337
|
+
* and rebuilding the prompt aborts an idle compaction for no gain) and must not
|
|
1338
|
+
* clear the answered-question guard (a long turn interrupted by one steer is
|
|
1339
|
+
* exactly when a repeat question is most likely). Hence per-turn state is
|
|
1340
|
+
* initialised in ONE branch.
|
|
1341
|
+
*
|
|
1342
|
+
* `steering` is REQUIRED and positional-second so each call site states which
|
|
1343
|
+
* kind of turn it is starting, rather than letting a default make "new turn" the
|
|
1344
|
+
* thoughtless option.
|
|
1345
|
+
*/
|
|
1346
|
+
prepareUserTurn(text, options) {
|
|
1216
1347
|
if (this.skillsEnabled)
|
|
1217
1348
|
this.skills.beginUserTurn();
|
|
1218
|
-
|
|
1349
|
+
if (!options.steering) {
|
|
1350
|
+
this.askedFingerprints.clear();
|
|
1351
|
+
this.turnRouting = this.skillsEnabled ? this.skills.triage(text) : null;
|
|
1352
|
+
this.skillLoadedThisTurn = false;
|
|
1353
|
+
this.toolCallsThisTurn = 0;
|
|
1354
|
+
// BEFORE the engine is entered, because `runAgentLoop` captures the system
|
|
1355
|
+
// prompt BY VALUE on its way in — a rebuild after `agent.prompt()` would be
|
|
1356
|
+
// one turn late. This is also why steer does not rebuild: `continue()` runs
|
|
1357
|
+
// while the loop already holds its snapshot, so the cost would be an idle
|
|
1358
|
+
// compaction aborted for a prompt nothing has read yet (§4.2.5).
|
|
1359
|
+
this.rebuildSystemPrompt();
|
|
1360
|
+
}
|
|
1219
1361
|
this.fast?.setGoal(text);
|
|
1220
1362
|
}
|
|
1221
1363
|
/** Cancel pending startup as well as any engine run already in progress. */
|
|
@@ -1393,7 +1535,7 @@ export class AgentController {
|
|
|
1393
1535
|
}
|
|
1394
1536
|
if (this.pendingUserSteering.size > 0) {
|
|
1395
1537
|
const request = ++this.startupSequence;
|
|
1396
|
-
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'));
|
|
1538
|
+
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'), { steering: true });
|
|
1397
1539
|
if (request !== this.startupSequence) {
|
|
1398
1540
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1399
1541
|
}
|
|
@@ -1494,18 +1636,90 @@ export class AgentController {
|
|
|
1494
1636
|
return {
|
|
1495
1637
|
effective: this.effectiveMode,
|
|
1496
1638
|
pending: this.pendingMode,
|
|
1497
|
-
askRoundsUsed: this.askRounds,
|
|
1498
|
-
maxAskRounds: this.config.planModeMaxAskRounds,
|
|
1499
1639
|
};
|
|
1500
1640
|
}
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1641
|
+
/**
|
|
1642
|
+
* READ-ONLY guard probe, called by `ask_user` BEFORE it opens the overlay.
|
|
1643
|
+
*
|
|
1644
|
+
* No side effect of any kind: a check that also writes cannot be placed before
|
|
1645
|
+
* the question without recording an answer that has not happened yet, and the
|
|
1646
|
+
* "a cancelled round stays askable" rule would then be unimplementable.
|
|
1647
|
+
*/
|
|
1648
|
+
hasAnswered(questions) {
|
|
1649
|
+
return this.askedFingerprints.has(questionFingerprint(questions));
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Record an ANSWERED question set, called by `ask_user` after a real answer.
|
|
1653
|
+
*
|
|
1654
|
+
* Evicts the oldest entry past `ASK_GUARD_LIMITS.maxRemembered`. That eviction
|
|
1655
|
+
* is memory hygiene and NOT a refusal rule — a bounded rejection would be the
|
|
1656
|
+
* deleted round cap wearing a bigger number.
|
|
1657
|
+
*/
|
|
1658
|
+
noteAnswered(questions) {
|
|
1659
|
+
this.askedFingerprints.add(questionFingerprint(questions));
|
|
1660
|
+
while (this.askedFingerprints.size > ASK_GUARD_LIMITS.maxRemembered) {
|
|
1661
|
+
const oldest = this.askedFingerprints.values().next();
|
|
1662
|
+
if (oldest.done === true)
|
|
1663
|
+
break;
|
|
1664
|
+
this.askedFingerprints.delete(oldest.value);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/** `/skills route` with no argument: the LAST turn's real routing result. */
|
|
1668
|
+
getSkillRouting() {
|
|
1669
|
+
return this.lastSkillRouting;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* One line when a confidently-matched skill was NOT loaded — and silence every
|
|
1673
|
+
* other time (D-8).
|
|
1674
|
+
*
|
|
1675
|
+
* THE DISCIPLINE IS "SILENT ON SUCCESS, VISIBLE ON FAILURE". Announcing the
|
|
1676
|
+
* candidates every turn would be ignored by the third day; announcing nothing
|
|
1677
|
+
* ever would make the whole feature unfalsifiable. "A skill covered this task,
|
|
1678
|
+
* you did the work anyway, and you never opened it" is precisely the failure this
|
|
1679
|
+
* feature exists to fix, so it is the one case that speaks — and it names the
|
|
1680
|
+
* command that explains it.
|
|
1681
|
+
*
|
|
1682
|
+
* SEVEN CONDITIONS, and the first one is the one that is easy to get wrong.
|
|
1683
|
+
*
|
|
1684
|
+
* 0. This turn was NOT aborted and NOT ended by an error.
|
|
1685
|
+
* `agent_end` is emitted unconditionally from a `finally` block in
|
|
1686
|
+
* `core/src/engine/agent.ts` and its payload carries no reason, no `aborted`
|
|
1687
|
+
* and no `error` field — so "did this turn finish normally?" has to be read
|
|
1688
|
+
* from this class's own state. `stopEngine()` sets `abortRequested` before it
|
|
1689
|
+
* calls `agent.abort()` and `agent_start` clears it, which makes it an exact
|
|
1690
|
+
* answer. Without this condition an Esc mis-touch would produce a line
|
|
1691
|
+
* criticising the model for not loading a skill: commentary in response to an
|
|
1692
|
+
* interruption.
|
|
1693
|
+
*
|
|
1694
|
+
* Conditions 5 and 0 are not redundant: 0 decides what counts as the end of a
|
|
1695
|
+
* turn, 5 decides whether the turn did any work. With only 5, a tool call made
|
|
1696
|
+
* before an abort would still trigger the notice.
|
|
1697
|
+
*/
|
|
1698
|
+
maybeReportSkillTriage() {
|
|
1699
|
+
const routing = this.turnRouting;
|
|
1700
|
+
const best = routing?.candidates[0];
|
|
1701
|
+
const shouldReport = this.abortRequested === false &&
|
|
1702
|
+
this.skillsEnabled &&
|
|
1703
|
+
this.skills.getConfig().triage === true &&
|
|
1704
|
+
routing !== null &&
|
|
1705
|
+
best !== undefined &&
|
|
1706
|
+
best.score >= SKILL_TRIAGE_LIMITS.confidentScore &&
|
|
1707
|
+
this.skillLoadedThisTurn === false &&
|
|
1708
|
+
this.toolCallsThisTurn >= 1 &&
|
|
1709
|
+
!this.skillTriageNoticed.has(best.name);
|
|
1710
|
+
if (shouldReport && best !== undefined) {
|
|
1711
|
+
this.skillTriageNoticed.add(best.name);
|
|
1712
|
+
this.notifyHostFn?.('info', `No skill was loaded; the closest installed match was "${best.name}". ` +
|
|
1713
|
+
'Run /skills route to see why.');
|
|
1506
1714
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1715
|
+
// The routing result outlives the turn so `/skills route` can explain it; the
|
|
1716
|
+
// candidates themselves must not, or the next `rebuildSystemPrompt()` would
|
|
1717
|
+
// resurrect a set computed for a task that is over.
|
|
1718
|
+
if (routing !== null)
|
|
1719
|
+
this.lastSkillRouting = routing;
|
|
1720
|
+
this.turnRouting = null;
|
|
1721
|
+
this.skillLoadedThisTurn = false;
|
|
1722
|
+
this.toolCallsThisTurn = 0;
|
|
1509
1723
|
}
|
|
1510
1724
|
/**
|
|
1511
1725
|
* Run `fn` with the idle watchdog paused.
|
|
@@ -1546,22 +1760,67 @@ export class AgentController {
|
|
|
1546
1760
|
isTeamRegistered() {
|
|
1547
1761
|
return this.teamRegistered;
|
|
1548
1762
|
}
|
|
1549
|
-
/**
|
|
1763
|
+
/**
|
|
1764
|
+
* Whether `task` will actually dispatch - a DERIVED value.
|
|
1765
|
+
*
|
|
1766
|
+
* One truth (`config.team.enabled`) plus one immutable snapshot
|
|
1767
|
+
* (`teamRegistered`). The previous version read a private copy that `/team
|
|
1768
|
+
* on|off` hand-synchronised, which worked only while that command was the sole
|
|
1769
|
+
* writer; the settings screen writes the config directly, so the copy would
|
|
1770
|
+
* have kept saying `on` (P1-4 / AC-25).
|
|
1771
|
+
*/
|
|
1550
1772
|
isTeamEnabled() {
|
|
1551
|
-
return this.
|
|
1773
|
+
return this.teamRegistered && this.config.team.enabled;
|
|
1552
1774
|
}
|
|
1553
1775
|
/**
|
|
1554
1776
|
* Flip the live switch and re-render the prompt.
|
|
1555
1777
|
*
|
|
1778
|
+
* THE NAME IS KEPT (three call sites plus the settings path) but the body is
|
|
1779
|
+
* one write: `setTeamConfig()` already clamps, bumps the revision and rebuilds,
|
|
1780
|
+
* so there is no second place where a half-applied state can exist.
|
|
1781
|
+
*
|
|
1556
1782
|
* Turning it ON in a session that started with `--no-team` deliberately does
|
|
1557
1783
|
* NOT register the tool (D-17): the array is immutable, and advertising an
|
|
1558
1784
|
* unregistered tool is the dead end P0-2 is about. `/team` reports that
|
|
1559
1785
|
* honestly by checking `isTeamRegistered()` first.
|
|
1560
1786
|
*/
|
|
1561
1787
|
setTeamEnabled(enabled) {
|
|
1562
|
-
this.
|
|
1563
|
-
|
|
1788
|
+
this.setTeamConfig({ enabled });
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Arm forced team mode for this session (team-command-forced-dispatch §3.5).
|
|
1792
|
+
*
|
|
1793
|
+
* Returns the count actually adopted. The clamp is `[1, team.maxSubagents]`:
|
|
1794
|
+
* `0`, a negative and a fraction are refused by the parser upstream (`-n 0` is
|
|
1795
|
+
* a syntax error, not a clamp), so this is the boundary check of a value that
|
|
1796
|
+
* crossed a module line - which is the one place `CLAUDE.md` asks for it, not a
|
|
1797
|
+
* second validation of the same input.
|
|
1798
|
+
*
|
|
1799
|
+
* THE COUNT IS FROZEN for the rest of the session: `/team -n 2 <brief>` then a
|
|
1800
|
+
* plain message keeps two children per turn, so the user's one expressed intent
|
|
1801
|
+
* does not silently widen back to four.
|
|
1802
|
+
*/
|
|
1803
|
+
armForcedTeam(count) {
|
|
1804
|
+
const ceiling = Math.max(1, this.config.team.maxSubagents);
|
|
1805
|
+
const adopted = Math.max(1, Math.min(Math.floor(count), ceiling));
|
|
1806
|
+
this.forcedTeam = { count: adopted };
|
|
1807
|
+
// RE-RENDER: the `<forced_team_mode>` block has to be in the very next
|
|
1808
|
+
// request, and `rebuildSystemPrompt()` is the live path that does it (it is
|
|
1809
|
+
// also why the command body prints a notice: this produces no AgentEvent, so
|
|
1810
|
+
// without one React would not repaint the `Team N` chip - P1-6).
|
|
1564
1811
|
this.rebuildSystemPrompt();
|
|
1812
|
+
return adopted;
|
|
1813
|
+
}
|
|
1814
|
+
/** Disarm. IDEMPOTENT: disarming an unarmed session rewrites nothing. */
|
|
1815
|
+
disarmForcedTeam() {
|
|
1816
|
+
if (!this.forcedTeam)
|
|
1817
|
+
return;
|
|
1818
|
+
this.forcedTeam = null;
|
|
1819
|
+
this.rebuildSystemPrompt();
|
|
1820
|
+
}
|
|
1821
|
+
/** The armed state, read at render time by the chip and by `/team`'s status. */
|
|
1822
|
+
getForcedTeam() {
|
|
1823
|
+
return this.forcedTeam;
|
|
1565
1824
|
}
|
|
1566
1825
|
getTeamConfig() {
|
|
1567
1826
|
return this.config.team;
|
|
@@ -1584,8 +1843,8 @@ export class AgentController {
|
|
|
1584
1843
|
* One authority for the `task` report's disclosure line and its price
|
|
1585
1844
|
* table: both read through here, so the header can never name one model
|
|
1586
1845
|
* while pricing another. Re-resolved on every call for the reason
|
|
1587
|
-
* `resolveFastTier` records - `/fast off`, a settings save and
|
|
1588
|
-
* all rewrite the inputs mid-session.
|
|
1846
|
+
* `resolveFastTier` records - `/fast off`, a settings save and the `Subagent
|
|
1847
|
+
* model` row all rewrite the inputs mid-session.
|
|
1589
1848
|
*/
|
|
1590
1849
|
subagentModelResolution() {
|
|
1591
1850
|
return resolveSubagentModel(this.config, (id, role) => this.hasApiKey(id, role));
|
|
@@ -1670,7 +1929,7 @@ export class AgentController {
|
|
|
1670
1929
|
* THIS EXISTS BECAUSE `persistConfig` DOES NOT TOUCH THE RUNTIME (P1-2 /
|
|
1671
1930
|
* AC-38). `App` reads `controller.getConfig()` on every render, and that
|
|
1672
1931
|
* returns THIS object; the App's `persistConfig` only writes the file. So
|
|
1673
|
-
* `/todo panel off` must call both, exactly as
|
|
1932
|
+
* `/todo panel off` must call both, exactly as settings' `Team` page does — with
|
|
1674
1933
|
* the persist call the command reports success and changes nothing until the
|
|
1675
1934
|
* next launch.
|
|
1676
1935
|
*
|
|
@@ -97,8 +97,19 @@ export function releasedSlots(draft) {
|
|
|
97
97
|
|| hasConnectionPatch(draft.patch, role))
|
|
98
98
|
released.add(role);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// ONLY A MODEL BINDING DECISION RELEASES THE SLOT (subagent-fanout D-7). The
|
|
101
|
+
// fan-out width and the supervisor live in the SAME `team` section but are not
|
|
102
|
+
// the kit's business: `{ maxSubagents: 6 }` used to hit the old
|
|
103
|
+
// `patch.team !== undefined` test, so narrowing the fan-out from the settings
|
|
104
|
+
// screen silently dropped the subagent model a session kit had installed for
|
|
105
|
+
// this session - with every value on screen still correct.
|
|
106
|
+
//
|
|
107
|
+
// `'model' in patch`, not `patch.model !== undefined`: an explicit
|
|
108
|
+
// `model: undefined` still means "this transaction spoke about the model".
|
|
109
|
+
const teamPatch = draft.patch.team;
|
|
110
|
+
if (teamPatch && ('model' in teamPatch || 'modelProfileId' in teamPatch)) {
|
|
101
111
|
released.add('subagent');
|
|
112
|
+
}
|
|
102
113
|
return [...released].filter((slot) => held.includes(slot));
|
|
103
114
|
}
|
|
104
115
|
/** The session kit as it stands AFTER this transaction's explicit decisions. */
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
9
9
|
*/
|
|
10
10
|
/** Bump when the wording changes, so a transcript can be dated. */
|
|
11
|
-
export declare const PLAN_MODE_BLOCK_VERSION = "
|
|
11
|
+
export declare const PLAN_MODE_BLOCK_VERSION = "v3-2026-10-26";
|
|
12
12
|
export interface PlanModeBlockOptions {
|
|
13
13
|
/** False under `-p` / piped stdin: no overlay can be rendered, so no tools. */
|
|
14
14
|
interactive: boolean;
|
|
15
|
-
/** `planModeMaxAskRounds`; substituted into the guidance so the two agree. */
|
|
16
|
-
maxAskRounds: number;
|
|
17
15
|
}
|
|
18
16
|
export declare function buildPlanModeBlock(options: PlanModeBlockOptions): string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ASCII ONLY — `agent/` is inside the glyph scanner's scope.
|
|
9
9
|
*/
|
|
10
10
|
/** Bump when the wording changes, so a transcript can be dated. */
|
|
11
|
-
export const PLAN_MODE_BLOCK_VERSION = '
|
|
11
|
+
export const PLAN_MODE_BLOCK_VERSION = 'v3-2026-10-26';
|
|
12
12
|
/**
|
|
13
13
|
* The closing paragraph of the interactive block.
|
|
14
14
|
*
|
|
@@ -42,13 +42,18 @@ const HEADLESS_PARAGRAPH = [
|
|
|
42
42
|
' write the finished plan as your final message in markdown.',
|
|
43
43
|
].join('\n');
|
|
44
44
|
export function buildPlanModeBlock(options) {
|
|
45
|
-
const { interactive
|
|
45
|
+
const { interactive } = options;
|
|
46
|
+
// NO ROUND COUNT APPEARS HERE, and none may be reintroduced: the number used
|
|
47
|
+
// to be substituted from `planModeMaxAskRounds`, which meant the prompt itself
|
|
48
|
+
// told the model it would run out. "3-5 questions" was also wrong against a
|
|
49
|
+
// schema and normalizer that both accept 1-5; correcting it here is one line.
|
|
46
50
|
const askStep = interactive
|
|
47
51
|
? [
|
|
48
52
|
'2. If a decision would change the work and you cannot settle it by reading, call',
|
|
49
|
-
' ask_user with
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
' ask_user with 1-5 multiple-choice questions in ONE call. Mark exactly one option',
|
|
54
|
+
' recommended:true and put the reason in its description. There is NO round limit -',
|
|
55
|
+
' ask as many rounds as the work needs - but each round costs the user time, so batch',
|
|
56
|
+
' the questions and skip anything the workspace already answers.',
|
|
52
57
|
]
|
|
53
58
|
: [
|
|
54
59
|
'2. If a decision would change the work and you cannot settle it by reading, state',
|
|
@@ -76,8 +81,9 @@ export function buildPlanModeBlock(options) {
|
|
|
76
81
|
'',
|
|
77
82
|
'Work in this order:',
|
|
78
83
|
'1. Follow the project-guidance protocol: read applicable guidance and index navigation',
|
|
79
|
-
' before any source search or read. Then
|
|
80
|
-
'
|
|
84
|
+
' before any source search or read. Then run the skill triage described above and load',
|
|
85
|
+
' any skill that covers this task, BEFORE inspecting the real code with read_file,',
|
|
86
|
+
' list_dir, glob and grep. Never plan against unread structure.',
|
|
81
87
|
...askStep,
|
|
82
88
|
...submitStep,
|
|
83
89
|
'',
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { AgentEvent, ModelCost, ToolResult, TokenUsage } from '@aragon-agen
|
|
|
15
15
|
import type { AgentMode } from './agent-mode.js';
|
|
16
16
|
import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '../team/types.js';
|
|
17
17
|
import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
18
|
-
import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
|
|
18
|
+
import type { CompactionMode, CompactionPhase, CompactionRecord, CompactionSnapshot, CompactionUiTrigger, ContextUsageSnapshot } from '../compaction/types.js';
|
|
19
19
|
import type { AcceptanceSnapshot, TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
20
20
|
import type { JobKind, ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
21
21
|
import type { RetryPhase } from './retry-view.js';
|
|
@@ -237,6 +237,22 @@ export type Entry = {
|
|
|
237
237
|
messages: number;
|
|
238
238
|
charsRemoved: number;
|
|
239
239
|
};
|
|
240
|
+
/**
|
|
241
|
+
* The stage the LIVE card is in (context-compaction-never-fires C).
|
|
242
|
+
*
|
|
243
|
+
* Absent on a settled card and on every card written before this field
|
|
244
|
+
* existed, which is why the renderer treats it as optional and falls back to
|
|
245
|
+
* the old `summarizing with <model>` line.
|
|
246
|
+
*/
|
|
247
|
+
phase?: CompactionPhase;
|
|
248
|
+
/** Set when the cut relaxed `keepRecentTurns` to exist at all (B). */
|
|
249
|
+
keepRelaxed?: boolean;
|
|
250
|
+
retainedTurns?: number;
|
|
251
|
+
/** Messages the relaxed cut left verbatim - the number the card states. */
|
|
252
|
+
retainedMessages?: number;
|
|
253
|
+
keepRecentTurnsUsed?: number;
|
|
254
|
+
/** Messages the digest left out, so they cannot be quoted (A1). */
|
|
255
|
+
elidedMessages?: number;
|
|
240
256
|
live: boolean;
|
|
241
257
|
} | {
|
|
242
258
|
id: string;
|
|
@@ -683,6 +699,18 @@ export type ViewAction = {
|
|
|
683
699
|
type: 'compactionEnd';
|
|
684
700
|
record: CompactionRecord;
|
|
685
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* The live card's stage (context-compaction-never-fires C).
|
|
704
|
+
*
|
|
705
|
+
* Its own action rather than a field on `compactionStart` because the phases
|
|
706
|
+
* arrive AFTER it, one per step, and because a phase must be able to land on a
|
|
707
|
+
* card that a previous attempt already settled into the same entry id.
|
|
708
|
+
*/
|
|
709
|
+
| {
|
|
710
|
+
type: 'compactionPhase';
|
|
711
|
+
index: number;
|
|
712
|
+
phase: CompactionPhase;
|
|
713
|
+
}
|
|
686
714
|
/**
|
|
687
715
|
* Compaction spend.
|
|
688
716
|
*
|