@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
|
@@ -42,7 +42,7 @@ function renderAssistant(message, calls) {
|
|
|
42
42
|
}
|
|
43
43
|
return { text: text.join('\n'), visible };
|
|
44
44
|
}
|
|
45
|
-
function renderEntry(message, messageId, calls
|
|
45
|
+
function renderEntry(message, messageId, calls) {
|
|
46
46
|
const rendered = message.role === 'assistant'
|
|
47
47
|
? renderAssistant(message, calls)
|
|
48
48
|
: contentText(message.content);
|
|
@@ -52,6 +52,12 @@ function renderEntry(message, messageId, calls, result) {
|
|
|
52
52
|
source.isLegacyReference = true;
|
|
53
53
|
heading += ' (legacy read-only reference, not current user acceptance)';
|
|
54
54
|
}
|
|
55
|
+
const entry = {
|
|
56
|
+
id: messageId,
|
|
57
|
+
text: `${heading}\n${rendered.text}`,
|
|
58
|
+
source,
|
|
59
|
+
summarized: message.role !== 'user',
|
|
60
|
+
};
|
|
55
61
|
if (message.role === 'tool_result') {
|
|
56
62
|
source.toolCallId = message.toolCallId;
|
|
57
63
|
source.toolName = calls.get(message.toolCallId);
|
|
@@ -65,15 +71,12 @@ function renderEntry(message, messageId, calls, result) {
|
|
|
65
71
|
const first = rendered.text.slice(0, edge);
|
|
66
72
|
const last = rendered.text.slice(-edge);
|
|
67
73
|
source.visibleText = clippedVisibleSections(rendered.visible, rendered.text, edge);
|
|
68
|
-
|
|
74
|
+
entry.text = `${heading}\n${first}\n[clipped originalChars=${rendered.text.length}`
|
|
69
75
|
+ ` omittedChars=${omittedChars}]\n${last}`;
|
|
70
|
-
|
|
76
|
+
entry.clip = { messageId, omittedChars };
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
if (message.role !== 'user')
|
|
75
|
-
result.coverage.summarizedMessages += 1;
|
|
76
|
-
return `${heading}\n${rendered.text}`;
|
|
79
|
+
return entry;
|
|
77
80
|
}
|
|
78
81
|
function clippedVisibleSections(parts, rendered, edge) {
|
|
79
82
|
const visible = [];
|
|
@@ -90,7 +93,86 @@ function clippedVisibleSections(parts, rendered, edge) {
|
|
|
90
93
|
}
|
|
91
94
|
return visible.filter(Boolean);
|
|
92
95
|
}
|
|
93
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The elision marker, in the text the summarizer reads.
|
|
98
|
+
*
|
|
99
|
+
* IT CARRIES THE ID RANGE AND SAYS "NOT QUOTABLE", because the model's output is
|
|
100
|
+
* validated against the source map: a model that cannot tell which messages it did
|
|
101
|
+
* not see will cite one, and the delta is then rejected as `invalid_memory_delta`.
|
|
102
|
+
* Naming the range turns a mystery failure into a readable instruction.
|
|
103
|
+
*/
|
|
104
|
+
function elisionMarker(firstId, lastId, count) {
|
|
105
|
+
return `[elided ${count} messages, ids ${firstId}..${lastId} - not quotable]`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Decide the split: the FIRST and LAST entries verbatim, the middle elided.
|
|
109
|
+
*
|
|
110
|
+
* THIS IS THE SHAPE THE USER ASKED FOR ("keep the front and the back, compress
|
|
111
|
+
* the middle"), and it is also the only shape that keeps the digest useful: the
|
|
112
|
+
* front carries the task as it unfolded and the back carries what the run was
|
|
113
|
+
* doing when the budget ran out, which is what the next turn needs.
|
|
114
|
+
*
|
|
115
|
+
* FOUR RULES, AND EACH ONE FIXES A CASE THE OBVIOUS VERSION GETS WRONG:
|
|
116
|
+
*
|
|
117
|
+
* 1. The first entry is kept unless it ALONE overruns the whole budget - a
|
|
118
|
+
* pathological single 130 k message must not evict everything else, and the
|
|
119
|
+
* user's own instruction is protected by the anchor anyway (never elided).
|
|
120
|
+
* 2. The front stops at half the budget, so the back always gets a share. A
|
|
121
|
+
* front that greedily filled the budget would leave the most recent history
|
|
122
|
+
* out of the digest, which is the exact opposite of what the tail is for.
|
|
123
|
+
* 3. The LAST entry gets a floor of one, even when rule 2 spent the budget. The
|
|
124
|
+
* budget is a soft target here: the hard gate is the summarizer's window
|
|
125
|
+
* projection in `compactor.ts`, and exceeding a soft target by one entry is
|
|
126
|
+
* strictly better than handing the model a digest that predates the turn it is
|
|
127
|
+
* about to continue from.
|
|
128
|
+
* 4. At least ONE entry always survives, even at a zero budget, which is what
|
|
129
|
+
* makes `digestBudgetChars(7000) === 0` produce a quotable digest instead of
|
|
130
|
+
* an empty one that `validateMemoryDelta` accepts as an empty delta.
|
|
131
|
+
*/
|
|
132
|
+
function planElision(lengths, available) {
|
|
133
|
+
const total = lengths.length;
|
|
134
|
+
if (total === 0)
|
|
135
|
+
return { front: 0, back: 0 };
|
|
136
|
+
const usable = Math.max(0, available);
|
|
137
|
+
const half = Math.floor(usable / 2);
|
|
138
|
+
let front = lengths[0] > usable ? 0 : 1;
|
|
139
|
+
let frontChars = front === 1 ? lengths[0] : 0;
|
|
140
|
+
while (front < total && frontChars + lengths[front] <= half) {
|
|
141
|
+
frontChars += lengths[front];
|
|
142
|
+
front += 1;
|
|
143
|
+
}
|
|
144
|
+
let back = total - front > 0 ? 1 : 0;
|
|
145
|
+
let backChars = back === 1 ? lengths[total - 1] : 0;
|
|
146
|
+
while (back < total - front && backChars + lengths[total - 1 - back] <= usable - frontChars) {
|
|
147
|
+
backChars += lengths[total - 1 - back];
|
|
148
|
+
back += 1;
|
|
149
|
+
}
|
|
150
|
+
return { front, back };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build every head entry without mutating messages.
|
|
154
|
+
*
|
|
155
|
+
* NEVER REJECTS FOR LENGTH (context-compaction-never-fires A1). It used to throw
|
|
156
|
+
* `digest_budget_exceeded` whenever the rendered head exceeded the budget, which is
|
|
157
|
+
* the gate that turned 96 of 96 compaction attempts on the reporting machine into
|
|
158
|
+
* no-ops - the digests were simply too big, and every caller (including the
|
|
159
|
+
* preflight in `memory-input.ts`, which discards the text and only wanted the two
|
|
160
|
+
* counts) inherited the rejection.
|
|
161
|
+
*
|
|
162
|
+
* WHAT IS ELIDED AND WHAT IS NOT. The anchor, the prior memory and the legacy
|
|
163
|
+
* summary are never elided: the first is the user's own instruction and the other
|
|
164
|
+
* two are the memory this compaction is extending. Only `head` entries are
|
|
165
|
+
* candidates, and an elided entry is dropped WHOLE - never half-rendered, which
|
|
166
|
+
* would produce a quoted fragment with a source id that does not correspond to
|
|
167
|
+
* anything the model actually read.
|
|
168
|
+
*
|
|
169
|
+
* AN ELIDED ENTRY IS NOT QUOTABLE, BUT IT IS STILL COUNTED. It does not enter
|
|
170
|
+
* `sourceMap` (so a delta citing it fails validation rather than inventing
|
|
171
|
+
* evidence) and it DOES count towards `coverage.summarizedMessages`, because that
|
|
172
|
+
* counter is what tells `validateMemoryDelta` that this head contained something
|
|
173
|
+
* summarizable at all - an all-elided digest that counted zero would let an empty
|
|
174
|
+
* delta through and record a "successful" compaction with an empty memory.
|
|
175
|
+
*/
|
|
94
176
|
export function buildDigest(input) {
|
|
95
177
|
const generation = input.generation ?? 1;
|
|
96
178
|
const start = input.startIndex ?? 0;
|
|
@@ -98,30 +180,70 @@ export function buildDigest(input) {
|
|
|
98
180
|
!Number.isSafeInteger(start) || start < 0) {
|
|
99
181
|
throw new MemoryError('invalid_memory_delta', '$.sourceIds');
|
|
100
182
|
}
|
|
183
|
+
const budgetChars = digestBudgetChars(input.summarizerWindow);
|
|
101
184
|
const result = {
|
|
102
185
|
text: '',
|
|
103
186
|
omitted: 0,
|
|
104
|
-
budgetChars
|
|
187
|
+
budgetChars,
|
|
105
188
|
sourceMap: new Map(),
|
|
106
189
|
coverage: { summarizedMessages: 0, clippedToolResults: [] },
|
|
107
190
|
};
|
|
108
|
-
const
|
|
191
|
+
const prefix = [];
|
|
109
192
|
if (input.anchor) {
|
|
110
|
-
|
|
193
|
+
prefix.push('ORIGINAL TASK (read-only reference, not current evidence):\n'
|
|
111
194
|
+ contentText(input.anchor.content).text);
|
|
112
195
|
}
|
|
113
196
|
if (input.priorMemory) {
|
|
114
|
-
|
|
197
|
+
prefix.push(`PRIOR MEMORY (read-only):\n${serializeMemory(input.priorMemory)}`);
|
|
115
198
|
}
|
|
116
199
|
if (input.legacySummary && !input.priorMemory?.legacySummary) {
|
|
117
|
-
|
|
200
|
+
prefix.push(`LEGACY SUMMARY (read-only, incomplete provenance):\n${input.legacySummary}`);
|
|
118
201
|
}
|
|
119
202
|
const calls = new Map();
|
|
120
|
-
input.head.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
const entries = input.head.map((message, index) => renderEntry(message, `g${generation}:m${start + index}`, calls));
|
|
204
|
+
const prefixChars = prefix.join('\n\n').length;
|
|
205
|
+
const budget = Math.max(budgetChars, LIMITS.digestMinChars);
|
|
206
|
+
const available = budget - prefixChars;
|
|
207
|
+
const fits = entries.reduce((sum, entry) => sum + entry.text.length, 0) <= available;
|
|
208
|
+
const kept = fits
|
|
209
|
+
? { front: entries.length, back: 0 }
|
|
210
|
+
: planElision(entries.map((entry) => entry.text.length), available);
|
|
211
|
+
const elidedFrom = kept.front;
|
|
212
|
+
const elidedTo = entries.length - kept.back;
|
|
213
|
+
if (elidedTo > elidedFrom) {
|
|
214
|
+
// The elided middle still names its tool calls, so a retained `tool_result`
|
|
215
|
+
// whose `tool_call` was dropped keeps `tool=<name>` instead of `unknown`.
|
|
216
|
+
for (const message of input.head.slice(elidedFrom, elidedTo)) {
|
|
217
|
+
if (message.role !== 'assistant')
|
|
218
|
+
continue;
|
|
219
|
+
for (const block of message.content) {
|
|
220
|
+
if (block.type === 'tool_call')
|
|
221
|
+
calls.set(block.toolCallId, block.toolName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
226
|
+
if (index >= elidedFrom && index < elidedTo) {
|
|
227
|
+
// ELIDED: not quotable, but still counted (see the function's header).
|
|
228
|
+
if (entries[index].summarized)
|
|
229
|
+
result.coverage.summarizedMessages += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const entry = entries[index];
|
|
233
|
+
result.sourceMap.set(entry.id, entry.source);
|
|
234
|
+
if (entry.summarized)
|
|
235
|
+
result.coverage.summarizedMessages += 1;
|
|
236
|
+
if (entry.clip)
|
|
237
|
+
result.coverage.clippedToolResults.push(entry.clip);
|
|
124
238
|
}
|
|
239
|
+
result.omitted = Math.max(0, elidedTo - elidedFrom);
|
|
240
|
+
const parts = [...prefix];
|
|
241
|
+
parts.push(...entries.slice(0, kept.front).map((entry) => entry.text));
|
|
242
|
+
if (result.omitted > 0) {
|
|
243
|
+
parts.push(elisionMarker(entries[elidedFrom].id, entries[elidedTo - 1].id, result.omitted));
|
|
244
|
+
}
|
|
245
|
+
parts.push(...entries.slice(elidedTo).map((entry) => entry.text));
|
|
246
|
+
result.text = parts.join('\n\n');
|
|
125
247
|
if (result.coverage.clippedToolResults.length > LIMITS.coverageClips) {
|
|
126
248
|
throw new MemoryError('protected_memory_too_large', '$.coverage');
|
|
127
249
|
}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
/** Structural limits for v2 compaction. All character counts are UTF-16 units. */
|
|
2
2
|
export declare const COMPACTION_LIMITS: {
|
|
3
3
|
readonly digestMaxChars: 120000;
|
|
4
|
+
/**
|
|
5
|
+
* The smallest digest worth sending when the budget collapses (never-fires A1).
|
|
6
|
+
*
|
|
7
|
+
* `digestBudgetChars(7000)` is 0 - a summarizer whose window is smaller than the
|
|
8
|
+
* reserve has no input room - and before this change a 0 budget simply threw.
|
|
9
|
+
* Eliding is only safe while SOMETHING survives: a digest with no messages and no
|
|
10
|
+
* source map forces the model to invent a delta (`invalid_memory_delta`) or to
|
|
11
|
+
* return none, so the floor is spent on the first entry, the last entry and the
|
|
12
|
+
* elision marker.
|
|
13
|
+
*/
|
|
14
|
+
readonly digestMinChars: 2000;
|
|
15
|
+
/**
|
|
16
|
+
* The tail budget the SECOND planning pass may fall back to (never-fires B).
|
|
17
|
+
*
|
|
18
|
+
* A MEANINGFUL TAIL, NOT A TOKEN-LEVEL FLOOR. `planCompaction` walks down from
|
|
19
|
+
* the LARGEST safe cut, so the cut it relaxes to leaves exactly this many
|
|
20
|
+
* messages behind; too small a number would satisfy the planner while breaking
|
|
21
|
+
* the thing the user asked for (the recent instructions stay verbatim). Tens of
|
|
22
|
+
* messages is one or two agentic turns' worth of tool calls.
|
|
23
|
+
*/
|
|
24
|
+
readonly minRetainedMessages: 40;
|
|
4
25
|
readonly summarizerReserveTokens: 8000;
|
|
5
26
|
readonly summaryMaxChars: 32000;
|
|
6
27
|
readonly summaryOutputTokens: 4096;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
/** Structural limits for v2 compaction. All character counts are UTF-16 units. */
|
|
2
2
|
export const COMPACTION_LIMITS = {
|
|
3
3
|
digestMaxChars: 120_000,
|
|
4
|
+
/**
|
|
5
|
+
* The smallest digest worth sending when the budget collapses (never-fires A1).
|
|
6
|
+
*
|
|
7
|
+
* `digestBudgetChars(7000)` is 0 - a summarizer whose window is smaller than the
|
|
8
|
+
* reserve has no input room - and before this change a 0 budget simply threw.
|
|
9
|
+
* Eliding is only safe while SOMETHING survives: a digest with no messages and no
|
|
10
|
+
* source map forces the model to invent a delta (`invalid_memory_delta`) or to
|
|
11
|
+
* return none, so the floor is spent on the first entry, the last entry and the
|
|
12
|
+
* elision marker.
|
|
13
|
+
*/
|
|
14
|
+
digestMinChars: 2_000,
|
|
15
|
+
/**
|
|
16
|
+
* The tail budget the SECOND planning pass may fall back to (never-fires B).
|
|
17
|
+
*
|
|
18
|
+
* A MEANINGFUL TAIL, NOT A TOKEN-LEVEL FLOOR. `planCompaction` walks down from
|
|
19
|
+
* the LARGEST safe cut, so the cut it relaxes to leaves exactly this many
|
|
20
|
+
* messages behind; too small a number would satisfy the planner while breaking
|
|
21
|
+
* the thing the user asked for (the recent instructions stay verbatim). Tens of
|
|
22
|
+
* messages is one or two agentic turns' worth of tool calls.
|
|
23
|
+
*/
|
|
24
|
+
minRetainedMessages: 40,
|
|
4
25
|
summarizerReserveTokens: 8_000,
|
|
5
26
|
summaryMaxChars: 32_000,
|
|
6
27
|
summaryOutputTokens: 4_096,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human wording for compaction outcomes (context-compaction-never-fires C).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
5
|
+
* (`glyphs.test.ts::inScope`), so every glyph the card draws comes from
|
|
6
|
+
* `pickGlyphs` and nothing here may carry a non-ASCII byte.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS MODULE EXISTS AT ALL. The card used to print the raw rejection code
|
|
9
|
+
* (`context not compacted #4: digest_budget_exceeded: $.digest`), which names the
|
|
10
|
+
* internal gate and tells the user nothing they can act on. A decline is a
|
|
11
|
+
* QUESTION ("why, and what do I do now"), so each code gets one phrase and - for
|
|
12
|
+
* the ones a user can act on - one remedy.
|
|
13
|
+
*
|
|
14
|
+
* IT IS ALSO THE SELF-DISABLE NOTICE'S SOURCE. The one line that says
|
|
15
|
+
* auto-compaction has paused reads the same phrase, so the card and the toast
|
|
16
|
+
* cannot describe one failure two ways.
|
|
17
|
+
*/
|
|
18
|
+
import type { CompactionPhase } from './types.js';
|
|
19
|
+
/** One phrase plus, where it helps, the thing the user can do about it. */
|
|
20
|
+
export interface ReasonText {
|
|
21
|
+
/** What happened, in one clause. Never contains the raw code. */
|
|
22
|
+
text: string;
|
|
23
|
+
/** The remedy, or `undefined` when there is nothing to offer. */
|
|
24
|
+
action?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A phrase for a rejection code, falling back to the code itself.
|
|
28
|
+
*
|
|
29
|
+
* THE FALLBACK IS THE RAW CODE, deliberately: a new gate nobody wrote wording for
|
|
30
|
+
* must still say SOMETHING specific on the card, and an invented sentence would
|
|
31
|
+
* be worse than a code the next reviewer can grep.
|
|
32
|
+
*/
|
|
33
|
+
export declare function describeCompactionReason(reason: string): ReasonText;
|
|
34
|
+
/** The card's one-line reason, with the remedy appended when there is one. */
|
|
35
|
+
export declare function compactionReasonLine(reason: string): string;
|
|
36
|
+
export declare function compactionPhaseLabel(phase: CompactionPhase): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const REASONS = {
|
|
2
|
+
nothing_to_drop: {
|
|
3
|
+
text: 'no safe cut - this run has too few user turns to keep',
|
|
4
|
+
action: 'keep talking, or lower /compact keep',
|
|
5
|
+
},
|
|
6
|
+
digest_budget_exceeded: {
|
|
7
|
+
text: 'the summary input was too large for one call',
|
|
8
|
+
action: 'retry - the middle is elided now',
|
|
9
|
+
},
|
|
10
|
+
summarizer_window_exceeded: {
|
|
11
|
+
text: 'the summary request does not fit the summarizer window',
|
|
12
|
+
action: 'pick a summarizer with a larger window',
|
|
13
|
+
},
|
|
14
|
+
protected_memory_too_large: {
|
|
15
|
+
text: 'the task memory does not fit its protection budget',
|
|
16
|
+
action: 'start a fresh conversation',
|
|
17
|
+
},
|
|
18
|
+
invalid_memory_delta: {
|
|
19
|
+
text: 'the summarizer returned an unusable memory delta',
|
|
20
|
+
action: 'retry',
|
|
21
|
+
},
|
|
22
|
+
empty_memory_delta: {
|
|
23
|
+
text: 'the summarizer returned an empty memory delta',
|
|
24
|
+
action: 'retry',
|
|
25
|
+
},
|
|
26
|
+
no_token_reclaim: {
|
|
27
|
+
text: 'the summary would not have reclaimed any tokens',
|
|
28
|
+
},
|
|
29
|
+
timeout: { text: 'the summarizer timed out', action: 'retry' },
|
|
30
|
+
no_summarizer_model: {
|
|
31
|
+
text: 'no summarizer model resolves',
|
|
32
|
+
action: 'set a model or add a provider key',
|
|
33
|
+
},
|
|
34
|
+
no_api_key: { text: 'the summarizer provider has no API key' },
|
|
35
|
+
already_running: { text: 'another compaction is already running' },
|
|
36
|
+
invalid_prior_memory: {
|
|
37
|
+
text: 'the existing task memory does not match this history',
|
|
38
|
+
action: 'start a fresh conversation',
|
|
39
|
+
},
|
|
40
|
+
invalid_history: { text: 'this history cannot be split safely' },
|
|
41
|
+
protected_multimodal_tail: {
|
|
42
|
+
text: 'an image in the middle of this history would have to be dropped',
|
|
43
|
+
},
|
|
44
|
+
insufficient_reclaim: {
|
|
45
|
+
text: 'the splice freed too little to stay below the threshold',
|
|
46
|
+
},
|
|
47
|
+
disabled: { text: 'auto-compaction is off for this session', action: '/compact on' },
|
|
48
|
+
cancelled: { text: 'cancelled' },
|
|
49
|
+
aborted: { text: 'cancelled' },
|
|
50
|
+
stale_history: { text: 'the history changed while the summary was running' },
|
|
51
|
+
adoption_failed: { text: 'the compacted history could not be adopted' },
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A phrase for a rejection code, falling back to the code itself.
|
|
55
|
+
*
|
|
56
|
+
* THE FALLBACK IS THE RAW CODE, deliberately: a new gate nobody wrote wording for
|
|
57
|
+
* must still say SOMETHING specific on the card, and an invented sentence would
|
|
58
|
+
* be worse than a code the next reviewer can grep.
|
|
59
|
+
*/
|
|
60
|
+
export function describeCompactionReason(reason) {
|
|
61
|
+
const code = reason.split(':', 1)[0].trim();
|
|
62
|
+
return REASONS[code] ?? { text: reason };
|
|
63
|
+
}
|
|
64
|
+
/** The card's one-line reason, with the remedy appended when there is one. */
|
|
65
|
+
export function compactionReasonLine(reason) {
|
|
66
|
+
const { text, action } = describeCompactionReason(reason);
|
|
67
|
+
return action ? `${text} (${action})` : text;
|
|
68
|
+
}
|
|
69
|
+
const PHASES = {
|
|
70
|
+
planning: 'planning the cut',
|
|
71
|
+
digesting: 'building the digest',
|
|
72
|
+
summarizing: 'summarizing',
|
|
73
|
+
validating: 'validating the summary',
|
|
74
|
+
applying: 'applying',
|
|
75
|
+
};
|
|
76
|
+
export function compactionPhaseLabel(phase) {
|
|
77
|
+
return PHASES[phase];
|
|
78
|
+
}
|
|
@@ -55,6 +55,23 @@ export type CompactionMode = 'summarized' | 'truncated' | 'relieved' | 'none';
|
|
|
55
55
|
* thing a CLI-local type is for.
|
|
56
56
|
*/
|
|
57
57
|
export type CompactionUiTrigger = 'pressure' | 'overflow' | 'manual';
|
|
58
|
+
/**
|
|
59
|
+
* Where one attempt currently is, so "compacting context" can also say WHAT it is
|
|
60
|
+
* doing for the forty-five seconds it is on screen.
|
|
61
|
+
*
|
|
62
|
+
* IT EXISTS BECAUSE THE GATES WERE INVISIBLE (context-compaction-never-fires
|
|
63
|
+
* RC-3). Planning and digesting both run BEFORE any model call, and both used to
|
|
64
|
+
* be able to reject while the only surface said `Context 95%` and nothing else.
|
|
65
|
+
* A phase is the CLI's answer to "is this stuck, or merely slow", and it is
|
|
66
|
+
* CLI-LOCAL for the reason the whole stream is: the engine has no phases, only a
|
|
67
|
+
* splice.
|
|
68
|
+
*
|
|
69
|
+
* THERE IS NO `settled` MEMBER, deliberately: the settle is already the
|
|
70
|
+
* `compaction_end` record. A phase restating it would give the card two ways to
|
|
71
|
+
* learn one fact, and the extra phase would race the record whenever the engine's
|
|
72
|
+
* verdict comes back first.
|
|
73
|
+
*/
|
|
74
|
+
export type CompactionPhase = 'planning' | 'digesting' | 'summarizing' | 'validating' | 'applying';
|
|
58
75
|
/** Whether a figure was measured by the provider or estimated by us. */
|
|
59
76
|
export type PressureSource = 'usage' | 'estimate';
|
|
60
77
|
/** The exact occupancy evidence at the compaction checkpoint. */
|
|
@@ -183,6 +200,36 @@ export interface CompactionRecord {
|
|
|
183
200
|
usage?: TokenUsage;
|
|
184
201
|
/** Set when an overflow attempt halved `keepRecentTurns` for itself (D-24). */
|
|
185
202
|
keepRecentTurnsUsed?: number;
|
|
203
|
+
/**
|
|
204
|
+
* Set when `planCompaction` had to relax `keepRecentTurns` to find a cut
|
|
205
|
+
* (context-compaction-never-fires B).
|
|
206
|
+
*
|
|
207
|
+
* DISCLOSED ON THE CARD, never silent: the relaxation trades "the last N turns
|
|
208
|
+
* are verbatim" for "compaction happens at all", and a user who is not told
|
|
209
|
+
* which side they got cannot tell a lossless compaction from a lossy one.
|
|
210
|
+
* `retainedTurns` and `retainedMessages` are what actually survived, so the card
|
|
211
|
+
* can state all three numbers instead of implying one.
|
|
212
|
+
*/
|
|
213
|
+
keepRelaxed?: boolean;
|
|
214
|
+
retainedTurns?: number;
|
|
215
|
+
/**
|
|
216
|
+
* How many MESSAGES the relaxed cut left verbatim.
|
|
217
|
+
*
|
|
218
|
+
* `retainedTurns` COUNTS USER TURNS, and in the sessions this fix exists for - one
|
|
219
|
+
* prompt followed by a hundred tool calls - that number is zero even though forty
|
|
220
|
+
* messages survived word for word. A card reading "kept 0 recent turns" at that
|
|
221
|
+
* moment says the opposite of what happened, so both numbers are recorded.
|
|
222
|
+
*/
|
|
223
|
+
retainedMessages?: number;
|
|
224
|
+
/**
|
|
225
|
+
* Messages left OUT of the digest text and therefore not quotable by the
|
|
226
|
+
* summarizer (context-compaction-never-fires A1).
|
|
227
|
+
*
|
|
228
|
+
* The elision is LOUD in three places: the digest text itself carries an
|
|
229
|
+
* `[elided ...]` marker, this field puts the count on the card, and the elided
|
|
230
|
+
* ids never enter the source map, so a delta citing one is rejected.
|
|
231
|
+
*/
|
|
232
|
+
elidedMessages?: number;
|
|
186
233
|
/**
|
|
187
234
|
* Present ONLY when tail relief fired (hardening §3.3.4 / W2).
|
|
188
235
|
*
|
|
@@ -221,14 +268,24 @@ export interface CompactionSnapshot {
|
|
|
221
268
|
/** The summarizer's model id, or `''`. */
|
|
222
269
|
model: string;
|
|
223
270
|
/**
|
|
224
|
-
* How many compactions ANNOUNCED themselves
|
|
271
|
+
* How many compactions ANNOUNCED themselves, i.e. how many cards the user saw.
|
|
225
272
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* the
|
|
273
|
+
* IT COUNTS ATTEMPTS, NOT SPLICES (context-compaction-never-fires C). Before
|
|
274
|
+
* this change only attempts that committed to work announced, so this was
|
|
275
|
+
* "compactions that ran"; now every attempt opens a card - including the ones
|
|
276
|
+
* that decline at the planner or the digest - because a silent decline is
|
|
277
|
+
* indistinguishable from a dead feature, which is the bug this feature shipped
|
|
278
|
+
* with. `applied` below is the other half, and `/compact status` prints both.
|
|
230
279
|
*/
|
|
231
280
|
compactions: number;
|
|
281
|
+
/**
|
|
282
|
+
* How many of those attempts actually replaced the history.
|
|
283
|
+
*
|
|
284
|
+
* OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE STAYS UNTOUCHED. Absent means
|
|
285
|
+
* "not reported", which `/compact status` renders exactly as the pre-change
|
|
286
|
+
* wording did.
|
|
287
|
+
*/
|
|
288
|
+
applied?: number;
|
|
232
289
|
/**
|
|
233
290
|
* How many `pressure` checkpoints declined BEFORE committing to any work
|
|
234
291
|
* (quiet-noop D-9 / P1-2).
|
|
@@ -260,19 +317,26 @@ export interface CompactionSnapshot {
|
|
|
260
317
|
/**
|
|
261
318
|
* THE COMPACTION EVENT STREAM.
|
|
262
319
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
320
|
+
* EVERY ATTEMPT ANNOUNCES ITSELF (context-compaction-never-fires C, which
|
|
321
|
+
* REVERSES quiet-noop §3.1). A `pressure` compaction used to reach
|
|
322
|
+
* `compaction_start` only after it committed to paid work, so an attempt that
|
|
323
|
+
* declined at the planner (`nothing_to_drop`) or at the digest budget produced
|
|
324
|
+
* no card at all - and a user sitting at 95 % with a dead auto-compaction had no
|
|
325
|
+
* way to learn that the feature was trying and failing 96 times. The card is now
|
|
326
|
+
* opened at the entry of `compact()` and ALWAYS settled, so `#N` counts attempts
|
|
327
|
+
* and the transcript carries one line per attempt, applied or not.
|
|
328
|
+
*
|
|
329
|
+
* WHY THE OLD DESIGN WAS DEFENSIBLE AND IS STILL WRONG. Its stated worry was a
|
|
330
|
+
* red `context not compacted` card every two turns flooding the transcript. The
|
|
331
|
+
* guards bound that to `stuckLimit` declines per run (`limits.ts`), and after
|
|
332
|
+
* them the auto path self-disables and says so once - so the flood never
|
|
333
|
+
* happens, while the silence happened every time.
|
|
268
334
|
*
|
|
269
335
|
* The pair is still BALANCED: every `compaction_start` is followed by exactly
|
|
270
336
|
* one `compaction_end` (a `finally` in the engine guarantees the verdict, and
|
|
271
337
|
* `settlePending` synthesizes a skeleton record when the compactor's own has not
|
|
272
|
-
* arrived).
|
|
273
|
-
*
|
|
274
|
-
* CORE's own `compaction_start` / `compaction_end` pair still fires for every
|
|
275
|
-
* attempt, declined or not, and that is the stream the log subscribes to.
|
|
338
|
+
* arrived). `phase` events land BETWEEN them, in order, and only for the live
|
|
339
|
+
* card.
|
|
276
340
|
*/
|
|
277
341
|
export type CompactionEvent = {
|
|
278
342
|
type: 'compaction_start';
|
|
@@ -282,6 +346,10 @@ export type CompactionEvent = {
|
|
|
282
346
|
} | {
|
|
283
347
|
type: 'compaction_end';
|
|
284
348
|
record: CompactionRecord;
|
|
349
|
+
} | {
|
|
350
|
+
type: 'phase';
|
|
351
|
+
index: number;
|
|
352
|
+
phase: CompactionPhase;
|
|
285
353
|
} | ({
|
|
286
354
|
type: 'usage';
|
|
287
355
|
usage: TokenUsage;
|
|
@@ -27,14 +27,19 @@ import type { CompactionIdentity } from './memory-identity.js';
|
|
|
27
27
|
* `/compact off`, `/compact threshold 0.8` and a settings-screen save take effect
|
|
28
28
|
* mid-session without touching the `Agent`.
|
|
29
29
|
*
|
|
30
|
-
* DECLINED COMPACTIONS
|
|
31
|
-
*
|
|
32
|
-
* `compaction_start`, so `onCompactorEvent` never
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
30
|
+
* DECLINED COMPACTIONS ARE VISIBLE (context-compaction-never-fires C, which
|
|
31
|
+
* REVERSES quiet-noop §3.6). A `pressure` compaction that changes nothing used to
|
|
32
|
+
* emit no `compaction_start`, so `onCompactorEvent` never opened a card and the
|
|
33
|
+
* only trace of an attempt was a counter in `/compact status`. The compactor now
|
|
34
|
+
* announces at the entry of `compact()` and always settles, so this class holds a
|
|
35
|
+
* record for every attempt and `settlePending` emits it - declined attempts
|
|
36
|
+
* included, with the reason on the card.
|
|
37
|
+
*
|
|
38
|
+
* WHAT THE OLD RULE PROTECTED IS STILL PROTECTED, by a different mechanism: the
|
|
39
|
+
* transcript cannot flood, because `maxPerRun` (5) and `stuckLimit` (2) bound the
|
|
40
|
+
* attempts and the auto path self-disables and says so once. `onCompactorEvent`'s
|
|
41
|
+
* `if (!this.open)` guard is still load-bearing: it is what stops a late record
|
|
42
|
+
* from re-opening a card the engine's verdict already closed.
|
|
38
43
|
*/
|
|
39
44
|
import type { ModelRole } from '../config/model-profiles.js';
|
|
40
45
|
import { type AgentEvent, type ContextManager, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ProviderRegistryOptions, type TokenUsage } from '@aragon-agent/core';
|
|
@@ -368,6 +368,7 @@ export class CompactionWiring {
|
|
|
368
368
|
live: this.enabled && summarizer !== null,
|
|
369
369
|
model: summarizer?.ref.modelId ?? '',
|
|
370
370
|
compactions: totals.compactions,
|
|
371
|
+
applied: totals.applied,
|
|
371
372
|
// OMITTED WHEN ZERO (quiet-noop D-9), so a healthy session's snapshot is
|
|
372
373
|
// shaped exactly as it was before this feature and `/compact status` has
|
|
373
374
|
// nothing extra to render.
|
package/dist/config/load.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
9
|
import { JOB_WAIT_MARGIN_MS } from '../proc/limits.js';
|
|
10
|
-
import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS,
|
|
10
|
+
import { DEFAULT_CONFIG, DEFAULT_SCROLL_MODE, IDLE_TIMEOUT_MARGIN_MS, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollMode, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, legacyTeamFanoutPatch, LEGACY_TEAM_FANOUT_DEFAULTS, parseThresholdInput, } from './schema.js';
|
|
11
11
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
12
12
|
import { readConfigFile, updatePersistedConfig } from './store.js';
|
|
13
13
|
import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey, } from './model-profile-resolution.js';
|
|
@@ -517,6 +517,49 @@ function migrateLegacyTeamCeilings(raw) {
|
|
|
517
517
|
keys: Object.keys(patch).join(','),
|
|
518
518
|
});
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Rewrite the cli<=0.6.x persisted fan-out defaults (5 / 3) to the new ones
|
|
522
|
+
* (8 / 8) and persist the change (subagent-fanout D-3 / D-4).
|
|
523
|
+
*
|
|
524
|
+
* ONE-SHOT and IDEMPOTENT, and the FILE LAYER ONLY, for the reasons
|
|
525
|
+
* `migrateLegacyTeamCeilings` records one block above: env (`ARAGON_TEAM_MAX`)
|
|
526
|
+
* and `--team-max` are THIS launch's intent and are never rewritten, the raw
|
|
527
|
+
* file object is patched IN PLACE so the `file` layer resolved below already
|
|
528
|
+
* reads 8/8, and a failed write leaves the marker absent so the next launch
|
|
529
|
+
* retries the whole migration.
|
|
530
|
+
*
|
|
531
|
+
* WHY A MARKER AND NOT JUST THE NUMBERS: `fanoutMigrated` is what keeps a user
|
|
532
|
+
* who deliberately returns to 5/3 from being migrated again on every launch
|
|
533
|
+
* (`legacyTeamFanoutPatch` returns `null` once it is set). The price of
|
|
534
|
+
* exact-equality is that one deliberate 5/3 is widened once; the marker bounds
|
|
535
|
+
* that to at most once.
|
|
536
|
+
*/
|
|
537
|
+
function migrateLegacyTeamFanout(raw) {
|
|
538
|
+
const patch = legacyTeamFanoutPatch(raw?.team);
|
|
539
|
+
if (patch === null)
|
|
540
|
+
return;
|
|
541
|
+
// READ THE STARTING POINT BEFORE THE IN-PLACE PATCH, and never hardcode
|
|
542
|
+
// `5/3`: rule 4 of the matcher accepts an ABSENT `maxConcurrent`, in which
|
|
543
|
+
// case the honest starting point is "unset" - a log line the user cannot find
|
|
544
|
+
// on disk is worse than no log line.
|
|
545
|
+
const from = `${LEGACY_TEAM_FANOUT_DEFAULTS.maxSubagents}/${raw?.team?.maxConcurrent ?? 'unset'}`;
|
|
546
|
+
// THE FULL SECTION, not the bare patch: the write port types `team` as a
|
|
547
|
+
// whole `TeamConfig`, and the section the file already carries plus the three
|
|
548
|
+
// keys IS the section we want on disk.
|
|
549
|
+
const migratedTeam = { ...raw.team, ...patch };
|
|
550
|
+
Object.assign(raw.team, patch);
|
|
551
|
+
try {
|
|
552
|
+
updatePersistedConfig({ team: migratedTeam });
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// Unwritable config dir: in-memory values are already migrated; retry next
|
|
556
|
+
// launch (idempotent, because the marker never made it to disk).
|
|
557
|
+
}
|
|
558
|
+
getLogger().warn('config', 'team_fanout_migrated', {
|
|
559
|
+
from,
|
|
560
|
+
to: `${patch.maxSubagents}/${patch.maxConcurrent}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
520
563
|
/**
|
|
521
564
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
522
565
|
* (non-persisted) fields and enforce the timeout invariant.
|
|
@@ -527,6 +570,12 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
527
570
|
loadDotenv(cwd);
|
|
528
571
|
const read = readConfigFile();
|
|
529
572
|
migrateLegacyTeamCeilings(read.config);
|
|
573
|
+
// THE NEXT LINE, not a shared pass: the two migrations are independent, and
|
|
574
|
+
// keeping them as two statements keeps each one's "returns null when nothing
|
|
575
|
+
// matches" contract visible. Both run BEFORE `projectModelProfiles` for the
|
|
576
|
+
// same reason - the `file` layer resolved below must already read the migrated
|
|
577
|
+
// numbers, and neither migration needs a second projection path.
|
|
578
|
+
migrateLegacyTeamFanout(read.config);
|
|
530
579
|
const projection = projectModelProfiles(read.config ?? {});
|
|
531
580
|
const file = projection.file;
|
|
532
581
|
const env = readEnvConfig();
|
|
@@ -570,7 +619,6 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
570
619
|
// a config file that turned plan mode on.
|
|
571
620
|
const startInPlanMode = pick(flags.plan, env.partial.planModeDefault, file.planModeDefault) ??
|
|
572
621
|
DEFAULT_CONFIG.planModeDefault;
|
|
573
|
-
const planModeMaxAskRounds = clampAskRounds(file.planModeMaxAskRounds, DEFAULT_CONFIG.planModeMaxAskRounds);
|
|
574
622
|
const planModeHumanTimeoutMs = clampHumanTimeout(file.planModeHumanTimeoutMs, DEFAULT_CONFIG.planModeHumanTimeoutMs);
|
|
575
623
|
const toolTimeoutMs = coercePositiveInt(pick(flags.toolTimeout, file.toolTimeoutMs), DEFAULT_CONFIG.toolTimeoutMs);
|
|
576
624
|
// THE `jobs` SECTION IS RESOLVED HERE, ABOVE THE TIMEOUT BLOCK, and the
|
|
@@ -706,7 +754,6 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
706
754
|
// From `<home>/state.json`, not the config file — see `ui-state.ts`.
|
|
707
755
|
submitCount: readSubmitCount(),
|
|
708
756
|
startInPlanMode,
|
|
709
|
-
planModeMaxAskRounds,
|
|
710
757
|
planModeHumanTimeoutMs,
|
|
711
758
|
skills,
|
|
712
759
|
skillsRuntime: {
|