@aragon-agent/cli 0.6.39 → 0.6.40
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 +2589 -2523
- package/README.md +3706 -3614
- package/dist/agent/controller.d.ts +103 -9
- package/dist/agent/controller.js +196 -19
- 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 +14 -2
- package/dist/agent/system-prompt.js +7 -4
- package/dist/cli.js +21 -5
- package/dist/commands/builtins.js +12 -3
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -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/schema.d.ts +82 -6
- package/dist/config/schema.js +107 -9
- package/dist/config/store.js +7 -2
- 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/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- 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 +14 -1
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -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 +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ import type { CompactionEventListener, CompactionSnapshot, ContextUsageSnapshot
|
|
|
36
36
|
import type { AcceptanceItem, TodoEventListener, TodoSnapshot } from '../todo/types.js';
|
|
37
37
|
import { type HumanInputGate } from '../tools/human-input.js';
|
|
38
38
|
import { SkillService, type ApprovalGate } from '../skills/service.js';
|
|
39
|
+
import type { SkillRoutingReport } from '../skills/triage.js';
|
|
39
40
|
import { type ConversationResetOptions, type ConversationResetResult } from './conversation-reset.js';
|
|
40
41
|
import type { AgentMode } from './agent-mode.js';
|
|
41
42
|
import type { NoticeLevel } from './reducer.js';
|
|
@@ -183,16 +184,19 @@ export interface ControllerDeps {
|
|
|
183
184
|
*/
|
|
184
185
|
modelWindows?: ModelWindows;
|
|
185
186
|
}
|
|
186
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* What `setAgentMode` actually adopted — see `AgentController.setAgentMode`.
|
|
189
|
+
*
|
|
190
|
+
* THIS IS ALSO WHAT `/plan status` REPORTS. A `PlanStatus extends AgentModeState`
|
|
191
|
+
* with `askRoundsUsed` / `maxAskRounds` existed until the round cap was deleted;
|
|
192
|
+
* keeping an empty sub-interface around would only leave a comment that every
|
|
193
|
+
* later reader has to work through to learn that nothing is missing.
|
|
194
|
+
*/
|
|
187
195
|
export interface AgentModeState {
|
|
188
196
|
effective: AgentMode;
|
|
189
197
|
/** Non-null only while a `plan -> build` switch is waiting for `agent_end`. */
|
|
190
198
|
pending: AgentMode | null;
|
|
191
199
|
}
|
|
192
|
-
export interface PlanStatus extends AgentModeState {
|
|
193
|
-
askRoundsUsed: number;
|
|
194
|
-
maxAskRounds: number;
|
|
195
|
-
}
|
|
196
200
|
export interface PreflightResult {
|
|
197
201
|
ok: boolean;
|
|
198
202
|
message?: string;
|
|
@@ -235,6 +239,21 @@ export declare class AgentController {
|
|
|
235
239
|
private readonly skills;
|
|
236
240
|
private readonly skillsEnabled;
|
|
237
241
|
private onSkillsChanged?;
|
|
242
|
+
/**
|
|
243
|
+
* This turn's routing result, or `null` when triage is off or nothing scored.
|
|
244
|
+
*
|
|
245
|
+
* It also carries the task it was computed for (`SkillRoutingReport.task`), so
|
|
246
|
+
* there is no separate copy of the turn's text to keep in step with it.
|
|
247
|
+
*/
|
|
248
|
+
private turnRouting;
|
|
249
|
+
/** The previous turn's routing, for `/skills route` with no argument. */
|
|
250
|
+
private lastSkillRouting;
|
|
251
|
+
/** Did this turn call `skill(name)` successfully? The only proof of adoption. */
|
|
252
|
+
private skillLoadedThisTurn;
|
|
253
|
+
/** Tool calls this turn, EXCLUDING `skill` / `skill_find` (see §4.2.7). */
|
|
254
|
+
private toolCallsThisTurn;
|
|
255
|
+
/** Skills already reported as "not loaded" — a session says it once (I-T5). */
|
|
256
|
+
private readonly skillTriageNoticed;
|
|
238
257
|
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
239
258
|
private cwd;
|
|
240
259
|
/**
|
|
@@ -259,7 +278,20 @@ export declare class AgentController {
|
|
|
259
278
|
private readonly structuredResultBlock;
|
|
260
279
|
private effectiveMode;
|
|
261
280
|
private pendingMode;
|
|
262
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Fingerprints of the question SETS the user ANSWERED in this turn. Bounded,
|
|
283
|
+
* and the bound is storage hygiene rather than a refusal rule — see
|
|
284
|
+
* `ASK_GUARD_LIMITS`.
|
|
285
|
+
*
|
|
286
|
+
* Replaces a numeric round counter that capped `ask_user` at four calls. There
|
|
287
|
+
* is no cap now; the only thing this prevents is the same batch of questions
|
|
288
|
+
* being put to the user twice, which is noise rather than information.
|
|
289
|
+
*
|
|
290
|
+
* Cleared in `prepareUserTurn` for a NEW turn only. A steer continuation is the
|
|
291
|
+
* same turn — clearing there would disable the guard exactly when a long turn
|
|
292
|
+
* most needs it (§4.1.6).
|
|
293
|
+
*/
|
|
294
|
+
private readonly askedFingerprints;
|
|
263
295
|
/** Static: whether this session could ever render a question overlay. */
|
|
264
296
|
private readonly planInteractive;
|
|
265
297
|
private readonly teamRegistered;
|
|
@@ -445,6 +477,24 @@ export declare class AgentController {
|
|
|
445
477
|
* semantics depend on whether a notice happened to be pending.
|
|
446
478
|
*/
|
|
447
479
|
private flushPendingNotices;
|
|
480
|
+
/**
|
|
481
|
+
* THE BOUNDARY OF ONE TURN — and there are TWO call sites, which is the reason
|
|
482
|
+
* this takes an explicit flag instead of resetting everything unconditionally.
|
|
483
|
+
*
|
|
484
|
+
* `startPrompt` passes `{ steering: false }`: the user typed something new, so
|
|
485
|
+
* this really is a new turn. `continue()` passes `{ steering: true }` with the
|
|
486
|
+
* CONCATENATED queued steering text, which resumes the SAME turn.
|
|
487
|
+
*
|
|
488
|
+
* A steer continuation must not re-triage (the candidates would change mid-turn,
|
|
489
|
+
* and rebuilding the prompt aborts an idle compaction for no gain) and must not
|
|
490
|
+
* clear the answered-question guard (a long turn interrupted by one steer is
|
|
491
|
+
* exactly when a repeat question is most likely). Hence per-turn state is
|
|
492
|
+
* initialised in ONE branch.
|
|
493
|
+
*
|
|
494
|
+
* `steering` is REQUIRED and positional-second so each call site states which
|
|
495
|
+
* kind of turn it is starting, rather than letting a default make "new turn" the
|
|
496
|
+
* thoughtless option.
|
|
497
|
+
*/
|
|
448
498
|
private prepareUserTurn;
|
|
449
499
|
/** Cancel pending startup as well as any engine run already in progress. */
|
|
450
500
|
abort(): void;
|
|
@@ -583,9 +633,53 @@ export declare class AgentController {
|
|
|
583
633
|
*/
|
|
584
634
|
applyPendingMode(): AgentModeState | null;
|
|
585
635
|
/** Named read surface for `/plan status`, so no command reaches into internals. */
|
|
586
|
-
getPlanStatus():
|
|
587
|
-
/**
|
|
588
|
-
|
|
636
|
+
getPlanStatus(): AgentModeState;
|
|
637
|
+
/**
|
|
638
|
+
* READ-ONLY guard probe, called by `ask_user` BEFORE it opens the overlay.
|
|
639
|
+
*
|
|
640
|
+
* No side effect of any kind: a check that also writes cannot be placed before
|
|
641
|
+
* the question without recording an answer that has not happened yet, and the
|
|
642
|
+
* "a cancelled round stays askable" rule would then be unimplementable.
|
|
643
|
+
*/
|
|
644
|
+
private hasAnswered;
|
|
645
|
+
/**
|
|
646
|
+
* Record an ANSWERED question set, called by `ask_user` after a real answer.
|
|
647
|
+
*
|
|
648
|
+
* Evicts the oldest entry past `ASK_GUARD_LIMITS.maxRemembered`. That eviction
|
|
649
|
+
* is memory hygiene and NOT a refusal rule — a bounded rejection would be the
|
|
650
|
+
* deleted round cap wearing a bigger number.
|
|
651
|
+
*/
|
|
652
|
+
private noteAnswered;
|
|
653
|
+
/** `/skills route` with no argument: the LAST turn's real routing result. */
|
|
654
|
+
getSkillRouting(): SkillRoutingReport | null;
|
|
655
|
+
/**
|
|
656
|
+
* One line when a confidently-matched skill was NOT loaded — and silence every
|
|
657
|
+
* other time (D-8).
|
|
658
|
+
*
|
|
659
|
+
* THE DISCIPLINE IS "SILENT ON SUCCESS, VISIBLE ON FAILURE". Announcing the
|
|
660
|
+
* candidates every turn would be ignored by the third day; announcing nothing
|
|
661
|
+
* ever would make the whole feature unfalsifiable. "A skill covered this task,
|
|
662
|
+
* you did the work anyway, and you never opened it" is precisely the failure this
|
|
663
|
+
* feature exists to fix, so it is the one case that speaks — and it names the
|
|
664
|
+
* command that explains it.
|
|
665
|
+
*
|
|
666
|
+
* SEVEN CONDITIONS, and the first one is the one that is easy to get wrong.
|
|
667
|
+
*
|
|
668
|
+
* 0. This turn was NOT aborted and NOT ended by an error.
|
|
669
|
+
* `agent_end` is emitted unconditionally from a `finally` block in
|
|
670
|
+
* `core/src/engine/agent.ts` and its payload carries no reason, no `aborted`
|
|
671
|
+
* and no `error` field — so "did this turn finish normally?" has to be read
|
|
672
|
+
* from this class's own state. `stopEngine()` sets `abortRequested` before it
|
|
673
|
+
* calls `agent.abort()` and `agent_start` clears it, which makes it an exact
|
|
674
|
+
* answer. Without this condition an Esc mis-touch would produce a line
|
|
675
|
+
* criticising the model for not loading a skill: commentary in response to an
|
|
676
|
+
* interruption.
|
|
677
|
+
*
|
|
678
|
+
* Conditions 5 and 0 are not redundant: 0 decides what counts as the end of a
|
|
679
|
+
* turn, 5 decides whether the turn did any work. With only 5, a tool call made
|
|
680
|
+
* before an abort would still trigger the notice.
|
|
681
|
+
*/
|
|
682
|
+
private maybeReportSkillTriage;
|
|
589
683
|
/**
|
|
590
684
|
* Run `fn` with the idle watchdog paused.
|
|
591
685
|
*
|
package/dist/agent/controller.js
CHANGED
|
@@ -45,12 +45,14 @@ import { TodoStore } from '../todo/store.js';
|
|
|
45
45
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
46
46
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
47
47
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
48
|
-
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
48
|
+
import { ASK_GUARD_LIMITS, DENY_ALL_HUMAN_INPUT, questionFingerprint, } from '../tools/human-input.js';
|
|
49
49
|
import { getLogger } from '../logging/logger.js';
|
|
50
50
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
51
51
|
import { SkillService } from '../skills/service.js';
|
|
52
52
|
import { createSkillTools } from '../skills/tools.js';
|
|
53
|
+
import { SKILL_TRIAGE_LIMITS } from '../skills/triage.js';
|
|
53
54
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
55
|
+
import { buildSkillTriageBlock } from './skill-triage-prompt.js';
|
|
54
56
|
import { resetConversationWith, } from './conversation-reset.js';
|
|
55
57
|
import { archiveClearedConversation, } from '../session/clear-archive.js';
|
|
56
58
|
/**
|
|
@@ -92,6 +94,28 @@ export class AgentController {
|
|
|
92
94
|
skills;
|
|
93
95
|
skillsEnabled;
|
|
94
96
|
onSkillsChanged;
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
// Skill triage (skill-triage-and-unbounded-ask §4.2.5) — PER-TURN state.
|
|
99
|
+
//
|
|
100
|
+
// Every field here is written in `prepareUserTurn` and cleared on `agent_end`,
|
|
101
|
+
// because a stale candidate set in the prompt is worse than no candidate set:
|
|
102
|
+
// it points the model at a skill chosen for a task it is no longer doing.
|
|
103
|
+
// -----------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* This turn's routing result, or `null` when triage is off or nothing scored.
|
|
106
|
+
*
|
|
107
|
+
* It also carries the task it was computed for (`SkillRoutingReport.task`), so
|
|
108
|
+
* there is no separate copy of the turn's text to keep in step with it.
|
|
109
|
+
*/
|
|
110
|
+
turnRouting = null;
|
|
111
|
+
/** The previous turn's routing, for `/skills route` with no argument. */
|
|
112
|
+
lastSkillRouting = null;
|
|
113
|
+
/** Did this turn call `skill(name)` successfully? The only proof of adoption. */
|
|
114
|
+
skillLoadedThisTurn = false;
|
|
115
|
+
/** Tool calls this turn, EXCLUDING `skill` / `skill_find` (see §4.2.7). */
|
|
116
|
+
toolCallsThisTurn = 0;
|
|
117
|
+
/** Skills already reported as "not loaded" — a session says it once (I-T5). */
|
|
118
|
+
skillTriageNoticed = new Set();
|
|
95
119
|
/** Mutable session cwd — tools resolve relative paths against this. */
|
|
96
120
|
cwd;
|
|
97
121
|
/**
|
|
@@ -124,7 +148,20 @@ export class AgentController {
|
|
|
124
148
|
// -----------------------------------------------------------------------
|
|
125
149
|
effectiveMode;
|
|
126
150
|
pendingMode = null;
|
|
127
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Fingerprints of the question SETS the user ANSWERED in this turn. Bounded,
|
|
153
|
+
* and the bound is storage hygiene rather than a refusal rule — see
|
|
154
|
+
* `ASK_GUARD_LIMITS`.
|
|
155
|
+
*
|
|
156
|
+
* Replaces a numeric round counter that capped `ask_user` at four calls. There
|
|
157
|
+
* is no cap now; the only thing this prevents is the same batch of questions
|
|
158
|
+
* being put to the user twice, which is noise rather than information.
|
|
159
|
+
*
|
|
160
|
+
* Cleared in `prepareUserTurn` for a NEW turn only. A steer continuation is the
|
|
161
|
+
* same turn — clearing there would disable the guard exactly when a long turn
|
|
162
|
+
* most needs it (§4.1.6).
|
|
163
|
+
*/
|
|
164
|
+
askedFingerprints = new Set();
|
|
128
165
|
/** Static: whether this session could ever render a question overlay. */
|
|
129
166
|
planInteractive;
|
|
130
167
|
// -----------------------------------------------------------------------
|
|
@@ -491,7 +528,12 @@ export class AgentController {
|
|
|
491
528
|
this.jobsRegistered = config.jobs.enabled && config.bash.background;
|
|
492
529
|
this.jobMirror =
|
|
493
530
|
this.jobsRegistered && config.jobs.persistLog
|
|
494
|
-
? new JobLogMirror({
|
|
531
|
+
? new JobLogMirror({
|
|
532
|
+
root: () => join(getHomeRoot(), 'jobs', this.jobSessionTag),
|
|
533
|
+
// NAMED, NOT DERIVED. The mirror sweeps only this directory; the session
|
|
534
|
+
// tagging above is what keeps its own run out of the sweep's reach.
|
|
535
|
+
jobsRoot: () => join(getHomeRoot(), 'jobs'),
|
|
536
|
+
})
|
|
495
537
|
: undefined;
|
|
496
538
|
this.procs = new ProcSupervisor({
|
|
497
539
|
readyTimeoutMs: () => this.config.bash.readyTimeoutMs,
|
|
@@ -585,7 +627,13 @@ export class AgentController {
|
|
|
585
627
|
// (or 11 with skills) it is today.
|
|
586
628
|
planTools: createPlanTools({
|
|
587
629
|
gate: humanInput,
|
|
588
|
-
|
|
630
|
+
// TWO CALLBACKS, AND THE SPLIT IS LOAD-BEARING (§4.1.2). One
|
|
631
|
+
// read-and-write accessor could only be called before the question
|
|
632
|
+
// (recording an answer nobody gave, which makes "a cancelled round stays
|
|
633
|
+
// askable" impossible) or after it (which cannot stop a repeat). The guard
|
|
634
|
+
// is "read before, write only after a real answer".
|
|
635
|
+
hasAnsweredQuestions: (questions) => this.hasAnswered(questions),
|
|
636
|
+
noteAnsweredQuestions: (questions) => this.noteAnswered(questions),
|
|
589
637
|
withHumanWait: (fn) => this.withHumanWait(fn),
|
|
590
638
|
onPlanApproved: () => this.setAgentMode('build', { force: true }),
|
|
591
639
|
}),
|
|
@@ -909,6 +957,17 @@ export class AgentController {
|
|
|
909
957
|
this.activeToolCalls += 1;
|
|
910
958
|
if (event.type === 'tool_execution_end') {
|
|
911
959
|
this.activeToolCalls = Math.max(0, this.activeToolCalls - 1);
|
|
960
|
+
// The ONLY proof a skill was adopted is a successful `skill` call. Reading
|
|
961
|
+
// the model's prose for a skill name would be a heuristic; this is a fact
|
|
962
|
+
// the executor already reports.
|
|
963
|
+
if (event.toolName === 'skill' && event.isError !== true)
|
|
964
|
+
this.skillLoadedThisTurn = true;
|
|
965
|
+
// `skill` / `skill_find` do not count as "work done": the notice below is
|
|
966
|
+
// for a turn that changed something without reading its skill, and a turn
|
|
967
|
+
// that ONLY searched for a skill is a turn that already tried.
|
|
968
|
+
if (event.toolName !== 'skill' && event.toolName !== 'skill_find') {
|
|
969
|
+
this.toolCallsThisTurn += 1;
|
|
970
|
+
}
|
|
912
971
|
// THE ONE PUSH SITE. A notice that could not be pushed here is picked up
|
|
913
972
|
// by the next tool result (the pull channel) or by the next user turn
|
|
914
973
|
// (the fill channel), so nothing is lost by declining.
|
|
@@ -925,6 +984,9 @@ export class AgentController {
|
|
|
925
984
|
// required: `turn_start` covers the next turn of the same run, and this
|
|
926
985
|
// covers a run that ended with a notice stranded in the queue.
|
|
927
986
|
this.jobRelay?.requeueUndelivered();
|
|
987
|
+
// AFTER the requeue, so a turn that ends with a stranded notice still gets
|
|
988
|
+
// its feedback line. Order between the two is otherwise irrelevant.
|
|
989
|
+
this.maybeReportSkillTriage();
|
|
928
990
|
}
|
|
929
991
|
});
|
|
930
992
|
if (this.fast) {
|
|
@@ -973,13 +1035,27 @@ export class AgentController {
|
|
|
973
1035
|
const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
|
|
974
1036
|
const fastAvailable = config === this.config ? this.fast?.available() === true
|
|
975
1037
|
: this.fast?.isRegistered() && config.fast.enabled && tier.ok;
|
|
1038
|
+
const skillsCatalog = this.skillsEnabled ? this.skills.catalogBlock() : '';
|
|
1039
|
+
// READ LIVE FROM THE SERVICE, never `config.skills` (§4.2.5 / R-12).
|
|
1040
|
+
// `SkillService.updateConfig` replaces the SERVICE's own config object, so
|
|
1041
|
+
// `this.config.skills` goes stale the moment `/skills triage off` runs — and a
|
|
1042
|
+
// stale read would leave the block in the prompt until the next launch, which
|
|
1043
|
+
// looks exactly like a broken switch. Same discipline as `/todo off`.
|
|
1044
|
+
const triageOn = this.skillsEnabled && this.skills.getConfig().triage === true;
|
|
1045
|
+
// An empty catalog means there is nothing to triage against, so the block is
|
|
1046
|
+
// absent rather than present-and-useless (I-T1 / AC-T18).
|
|
1047
|
+
const triageBlock = triageOn && skillsCatalog.length > 0
|
|
1048
|
+
? buildSkillTriageBlock({ candidates: this.turnRouting?.candidates ?? [] })
|
|
1049
|
+
: '';
|
|
976
1050
|
return buildSystemPrompt({
|
|
977
1051
|
cwd: this.cwd,
|
|
978
1052
|
tools: this.tools,
|
|
979
|
-
skillsBlock: this.skillsEnabled ?
|
|
1053
|
+
skillsBlock: this.skillsEnabled ? skillsCatalog + this.skills.alwaysBlock() : '',
|
|
1054
|
+
// Immediately after the catalog it refers to, before the plan block whose
|
|
1055
|
+
// first step defers to it (§4.2.4).
|
|
1056
|
+
skillTriageBlock: triageBlock,
|
|
980
1057
|
agentMode: this.effectiveMode,
|
|
981
1058
|
planInteractive: this.planInteractive,
|
|
982
|
-
planMaxAskRounds: config.planModeMaxAskRounds,
|
|
983
1059
|
// BOTH flags, and both are required (§4.5). `teamRegistered` false means
|
|
984
1060
|
// there is no `task` tool to advertise; `teamEnabled` false means the user
|
|
985
1061
|
// turned it off for this session. Advertising in either case would
|
|
@@ -1183,7 +1259,7 @@ export class AgentController {
|
|
|
1183
1259
|
}
|
|
1184
1260
|
/** Prepare before clearing the plan, then enter the engine without yielding. */
|
|
1185
1261
|
async startPrompt(text, options, request) {
|
|
1186
|
-
this.prepareUserTurn(text);
|
|
1262
|
+
this.prepareUserTurn(text, { steering: false });
|
|
1187
1263
|
if (request !== this.startupSequence) {
|
|
1188
1264
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1189
1265
|
}
|
|
@@ -1212,10 +1288,39 @@ export class AgentController {
|
|
|
1212
1288
|
// failure AC-25 exists to prevent.
|
|
1213
1289
|
this.jobRelay?.flushForTurn();
|
|
1214
1290
|
}
|
|
1215
|
-
|
|
1291
|
+
/**
|
|
1292
|
+
* THE BOUNDARY OF ONE TURN — and there are TWO call sites, which is the reason
|
|
1293
|
+
* this takes an explicit flag instead of resetting everything unconditionally.
|
|
1294
|
+
*
|
|
1295
|
+
* `startPrompt` passes `{ steering: false }`: the user typed something new, so
|
|
1296
|
+
* this really is a new turn. `continue()` passes `{ steering: true }` with the
|
|
1297
|
+
* CONCATENATED queued steering text, which resumes the SAME turn.
|
|
1298
|
+
*
|
|
1299
|
+
* A steer continuation must not re-triage (the candidates would change mid-turn,
|
|
1300
|
+
* and rebuilding the prompt aborts an idle compaction for no gain) and must not
|
|
1301
|
+
* clear the answered-question guard (a long turn interrupted by one steer is
|
|
1302
|
+
* exactly when a repeat question is most likely). Hence per-turn state is
|
|
1303
|
+
* initialised in ONE branch.
|
|
1304
|
+
*
|
|
1305
|
+
* `steering` is REQUIRED and positional-second so each call site states which
|
|
1306
|
+
* kind of turn it is starting, rather than letting a default make "new turn" the
|
|
1307
|
+
* thoughtless option.
|
|
1308
|
+
*/
|
|
1309
|
+
prepareUserTurn(text, options) {
|
|
1216
1310
|
if (this.skillsEnabled)
|
|
1217
1311
|
this.skills.beginUserTurn();
|
|
1218
|
-
|
|
1312
|
+
if (!options.steering) {
|
|
1313
|
+
this.askedFingerprints.clear();
|
|
1314
|
+
this.turnRouting = this.skillsEnabled ? this.skills.triage(text) : null;
|
|
1315
|
+
this.skillLoadedThisTurn = false;
|
|
1316
|
+
this.toolCallsThisTurn = 0;
|
|
1317
|
+
// BEFORE the engine is entered, because `runAgentLoop` captures the system
|
|
1318
|
+
// prompt BY VALUE on its way in — a rebuild after `agent.prompt()` would be
|
|
1319
|
+
// one turn late. This is also why steer does not rebuild: `continue()` runs
|
|
1320
|
+
// while the loop already holds its snapshot, so the cost would be an idle
|
|
1321
|
+
// compaction aborted for a prompt nothing has read yet (§4.2.5).
|
|
1322
|
+
this.rebuildSystemPrompt();
|
|
1323
|
+
}
|
|
1219
1324
|
this.fast?.setGoal(text);
|
|
1220
1325
|
}
|
|
1221
1326
|
/** Cancel pending startup as well as any engine run already in progress. */
|
|
@@ -1393,7 +1498,7 @@ export class AgentController {
|
|
|
1393
1498
|
}
|
|
1394
1499
|
if (this.pendingUserSteering.size > 0) {
|
|
1395
1500
|
const request = ++this.startupSequence;
|
|
1396
|
-
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'));
|
|
1501
|
+
this.prepareUserTurn([...this.pendingUserSteering.values()].join('\n\n'), { steering: true });
|
|
1397
1502
|
if (request !== this.startupSequence) {
|
|
1398
1503
|
return { status: 'not-started', reason: 'cancelled' };
|
|
1399
1504
|
}
|
|
@@ -1494,18 +1599,90 @@ export class AgentController {
|
|
|
1494
1599
|
return {
|
|
1495
1600
|
effective: this.effectiveMode,
|
|
1496
1601
|
pending: this.pendingMode,
|
|
1497
|
-
askRoundsUsed: this.askRounds,
|
|
1498
|
-
maxAskRounds: this.config.planModeMaxAskRounds,
|
|
1499
1602
|
};
|
|
1500
1603
|
}
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1604
|
+
/**
|
|
1605
|
+
* READ-ONLY guard probe, called by `ask_user` BEFORE it opens the overlay.
|
|
1606
|
+
*
|
|
1607
|
+
* No side effect of any kind: a check that also writes cannot be placed before
|
|
1608
|
+
* the question without recording an answer that has not happened yet, and the
|
|
1609
|
+
* "a cancelled round stays askable" rule would then be unimplementable.
|
|
1610
|
+
*/
|
|
1611
|
+
hasAnswered(questions) {
|
|
1612
|
+
return this.askedFingerprints.has(questionFingerprint(questions));
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Record an ANSWERED question set, called by `ask_user` after a real answer.
|
|
1616
|
+
*
|
|
1617
|
+
* Evicts the oldest entry past `ASK_GUARD_LIMITS.maxRemembered`. That eviction
|
|
1618
|
+
* is memory hygiene and NOT a refusal rule — a bounded rejection would be the
|
|
1619
|
+
* deleted round cap wearing a bigger number.
|
|
1620
|
+
*/
|
|
1621
|
+
noteAnswered(questions) {
|
|
1622
|
+
this.askedFingerprints.add(questionFingerprint(questions));
|
|
1623
|
+
while (this.askedFingerprints.size > ASK_GUARD_LIMITS.maxRemembered) {
|
|
1624
|
+
const oldest = this.askedFingerprints.values().next();
|
|
1625
|
+
if (oldest.done === true)
|
|
1626
|
+
break;
|
|
1627
|
+
this.askedFingerprints.delete(oldest.value);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/** `/skills route` with no argument: the LAST turn's real routing result. */
|
|
1631
|
+
getSkillRouting() {
|
|
1632
|
+
return this.lastSkillRouting;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* One line when a confidently-matched skill was NOT loaded — and silence every
|
|
1636
|
+
* other time (D-8).
|
|
1637
|
+
*
|
|
1638
|
+
* THE DISCIPLINE IS "SILENT ON SUCCESS, VISIBLE ON FAILURE". Announcing the
|
|
1639
|
+
* candidates every turn would be ignored by the third day; announcing nothing
|
|
1640
|
+
* ever would make the whole feature unfalsifiable. "A skill covered this task,
|
|
1641
|
+
* you did the work anyway, and you never opened it" is precisely the failure this
|
|
1642
|
+
* feature exists to fix, so it is the one case that speaks — and it names the
|
|
1643
|
+
* command that explains it.
|
|
1644
|
+
*
|
|
1645
|
+
* SEVEN CONDITIONS, and the first one is the one that is easy to get wrong.
|
|
1646
|
+
*
|
|
1647
|
+
* 0. This turn was NOT aborted and NOT ended by an error.
|
|
1648
|
+
* `agent_end` is emitted unconditionally from a `finally` block in
|
|
1649
|
+
* `core/src/engine/agent.ts` and its payload carries no reason, no `aborted`
|
|
1650
|
+
* and no `error` field — so "did this turn finish normally?" has to be read
|
|
1651
|
+
* from this class's own state. `stopEngine()` sets `abortRequested` before it
|
|
1652
|
+
* calls `agent.abort()` and `agent_start` clears it, which makes it an exact
|
|
1653
|
+
* answer. Without this condition an Esc mis-touch would produce a line
|
|
1654
|
+
* criticising the model for not loading a skill: commentary in response to an
|
|
1655
|
+
* interruption.
|
|
1656
|
+
*
|
|
1657
|
+
* Conditions 5 and 0 are not redundant: 0 decides what counts as the end of a
|
|
1658
|
+
* turn, 5 decides whether the turn did any work. With only 5, a tool call made
|
|
1659
|
+
* before an abort would still trigger the notice.
|
|
1660
|
+
*/
|
|
1661
|
+
maybeReportSkillTriage() {
|
|
1662
|
+
const routing = this.turnRouting;
|
|
1663
|
+
const best = routing?.candidates[0];
|
|
1664
|
+
const shouldReport = this.abortRequested === false &&
|
|
1665
|
+
this.skillsEnabled &&
|
|
1666
|
+
this.skills.getConfig().triage === true &&
|
|
1667
|
+
routing !== null &&
|
|
1668
|
+
best !== undefined &&
|
|
1669
|
+
best.score >= SKILL_TRIAGE_LIMITS.confidentScore &&
|
|
1670
|
+
this.skillLoadedThisTurn === false &&
|
|
1671
|
+
this.toolCallsThisTurn >= 1 &&
|
|
1672
|
+
!this.skillTriageNoticed.has(best.name);
|
|
1673
|
+
if (shouldReport && best !== undefined) {
|
|
1674
|
+
this.skillTriageNoticed.add(best.name);
|
|
1675
|
+
this.notifyHostFn?.('info', `No skill was loaded; the closest installed match was "${best.name}". ` +
|
|
1676
|
+
'Run /skills route to see why.');
|
|
1506
1677
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1678
|
+
// The routing result outlives the turn so `/skills route` can explain it; the
|
|
1679
|
+
// candidates themselves must not, or the next `rebuildSystemPrompt()` would
|
|
1680
|
+
// resurrect a set computed for a task that is over.
|
|
1681
|
+
if (routing !== null)
|
|
1682
|
+
this.lastSkillRouting = routing;
|
|
1683
|
+
this.turnRouting = null;
|
|
1684
|
+
this.skillLoadedThisTurn = false;
|
|
1685
|
+
this.toolCallsThisTurn = 0;
|
|
1509
1686
|
}
|
|
1510
1687
|
/**
|
|
1511
1688
|
* Run `fn` with the idle watchdog paused.
|
|
@@ -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
|
*
|