@aragon-agent/cli 0.6.1 → 0.6.3
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 +216 -0
- package/README.md +241 -18
- package/dist/agent/agent-mode.d.ts +39 -0
- package/dist/agent/agent-mode.js +36 -0
- package/dist/agent/controller.d.ts +74 -1
- package/dist/agent/controller.js +200 -1
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +120 -0
- package/dist/agent/reducer.js +201 -2
- package/dist/cli.js +297 -6
- package/dist/commands/builtins.js +73 -23
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +113 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +342 -0
- package/dist/compaction/compactor.d.ts +268 -0
- package/dist/compaction/compactor.js +689 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +179 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +255 -0
- package/dist/compaction/wiring.js +556 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +92 -2
- package/dist/config/load.d.ts +31 -0
- package/dist/config/load.js +63 -1
- package/dist/config/schema.d.ts +159 -0
- package/dist/config/schema.js +137 -0
- package/dist/config/store.js +17 -1
- package/dist/config/ui-state.d.ts +44 -6
- package/dist/config/ui-state.js +21 -12
- package/dist/diagnostics/doctor.js +41 -1
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/index.js +8 -4
- package/dist/exec/runner.js +44 -2
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/stdin-mouse-filter.d.ts +8 -3
- package/dist/logging/install.d.ts +51 -1
- package/dist/logging/install.js +28 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/cli-commands.js +23 -1
- package/dist/session/persist.js +19 -0
- package/dist/session/store.d.ts +18 -0
- package/dist/session/store.js +59 -2
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +48 -1
- package/dist/ui/App.js +317 -17
- package/dist/ui/Composer.d.ts +26 -1
- package/dist/ui/Composer.js +6 -6
- package/dist/ui/PromptInput.d.ts +34 -1
- package/dist/ui/PromptInput.js +54 -3
- package/dist/ui/StatusBar.d.ts +40 -0
- package/dist/ui/StatusBar.js +13 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +32 -2
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +10 -3
- package/dist/ui/layout/budget.js +10 -3
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +34 -0
- package/dist/ui/overlays/HelpOverlay.js +18 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +60 -1
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/screen.d.ts +22 -0
- package/dist/ui/screen.js +70 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +23 -0
- package/dist/ui/use-startup-notices.d.ts +62 -5
- package/dist/ui/use-startup-notices.js +81 -17
- package/dist/ui/use-wheel-routing.js +8 -0
- package/dist/ui/win-vt-force.d.ts +136 -0
- package/dist/ui/win-vt-force.js +237 -0
- package/package.json +4 -2
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of the history, rendered as a bounded plain-text transcript
|
|
3
|
+
* (context-auto-compaction §3.6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* PURE. Two reasons this is a digest rather than raw messages, and the second
|
|
8
|
+
* one is the one that bites:
|
|
9
|
+
*
|
|
10
|
+
* - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
|
|
11
|
+
* says almost nothing about what was decided.
|
|
12
|
+
* - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
|
|
13
|
+
* COMPACTED. The whole point of using a cheap model is that it is cheap, and
|
|
14
|
+
* cheap models are exactly the ones with 32 k windows. Sending 180 k of
|
|
15
|
+
* history to summarize would fail with the same error we are recovering from
|
|
16
|
+
* (R-4).
|
|
17
|
+
*/
|
|
18
|
+
import { type Message } from '@aragon-agent/core';
|
|
19
|
+
/**
|
|
20
|
+
* Is this `user` message a previous `<compacted_context>` block?
|
|
21
|
+
*
|
|
22
|
+
* Recognized on the OPENING TAG rather than a full parse: the block carries
|
|
23
|
+
* attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
|
|
24
|
+
* module has no business knowing about, and a stricter match would fail on the
|
|
25
|
+
* first attribute anybody adds.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isPriorSummaryBlock(message: Message): boolean;
|
|
28
|
+
export interface DigestInput {
|
|
29
|
+
/** The head — `messages[protectedPrefix..cutIndex)`. */
|
|
30
|
+
head: readonly Message[];
|
|
31
|
+
/**
|
|
32
|
+
* The summarizer's own context window, or `undefined` when it is unknown.
|
|
33
|
+
*
|
|
34
|
+
* NOT the lead model's. This is the whole reason a digest exists rather than
|
|
35
|
+
* raw messages (R-4).
|
|
36
|
+
*/
|
|
37
|
+
summarizerWindow?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface DigestResult {
|
|
40
|
+
text: string;
|
|
41
|
+
/** Entries the budget forced out. `> 0` means the marker is present. */
|
|
42
|
+
omitted: number;
|
|
43
|
+
/** The budget actually used, after the window derivation. */
|
|
44
|
+
budgetChars: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The character budget for one digest.
|
|
48
|
+
*
|
|
49
|
+
* `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
|
|
50
|
+
* `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
|
|
51
|
+
* chars-per-token constants that must agree and nothing checking that they do is
|
|
52
|
+
* how a digest budget and an occupancy estimate drift apart.
|
|
53
|
+
*/
|
|
54
|
+
export declare function digestBudgetChars(summarizerWindow?: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Render the head into a bounded transcript.
|
|
57
|
+
*
|
|
58
|
+
* OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
|
|
59
|
+
* evicted last of all. A single omission marker is prepended so the summary can
|
|
60
|
+
* say that it is incomplete instead of pretending.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildDigest(input: DigestInput): DigestResult;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of the history, rendered as a bounded plain-text transcript
|
|
3
|
+
* (context-auto-compaction §3.6.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* PURE. Two reasons this is a digest rather than raw messages, and the second
|
|
8
|
+
* one is the one that bites:
|
|
9
|
+
*
|
|
10
|
+
* - Tool results are the bulk. Four 60 KB file reads are 240 KB of body that
|
|
11
|
+
* says almost nothing about what was decided.
|
|
12
|
+
* - THE SUMMARIZER MAY HAVE A SMALLER CONTEXT WINDOW THAN THE MODEL BEING
|
|
13
|
+
* COMPACTED. The whole point of using a cheap model is that it is cheap, and
|
|
14
|
+
* cheap models are exactly the ones with 32 k windows. Sending 180 k of
|
|
15
|
+
* history to summarize would fail with the same error we are recovering from
|
|
16
|
+
* (R-4).
|
|
17
|
+
*/
|
|
18
|
+
import { CHARS_PER_TOKEN } from '@aragon-agent/core';
|
|
19
|
+
import { BLOCK_TAG_CLOSE, BLOCK_TAG_OPEN, COMPACTION_LIMITS } from './limits.js';
|
|
20
|
+
/** Prepended when the budget forced entries out, so the summary can say so. */
|
|
21
|
+
const OMISSION_MARKER = (n) => `[... ${n} earlier ${n === 1 ? 'message' : 'messages'} omitted from this digest ...]`;
|
|
22
|
+
function clip(text, max) {
|
|
23
|
+
if (text.length <= max)
|
|
24
|
+
return text;
|
|
25
|
+
return `${text.slice(0, max)}...`;
|
|
26
|
+
}
|
|
27
|
+
/** Flatten `string | ContentPart[]` into text, naming images rather than dropping them. */
|
|
28
|
+
function partsToText(content) {
|
|
29
|
+
if (typeof content === 'string')
|
|
30
|
+
return content;
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return '';
|
|
33
|
+
return content
|
|
34
|
+
.map((part) => (typeof part?.text === 'string' ? part.text : '[image]'))
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Is this `user` message a previous `<compacted_context>` block?
|
|
39
|
+
*
|
|
40
|
+
* Recognized on the OPENING TAG rather than a full parse: the block carries
|
|
41
|
+
* attributes (`version`, `replaced`, `turns`, `anchor`, `generation`) that this
|
|
42
|
+
* module has no business knowing about, and a stricter match would fail on the
|
|
43
|
+
* first attribute anybody adds.
|
|
44
|
+
*/
|
|
45
|
+
export function isPriorSummaryBlock(message) {
|
|
46
|
+
if (message.role !== 'user')
|
|
47
|
+
return false;
|
|
48
|
+
const text = partsToText(message.content).trimStart();
|
|
49
|
+
return text.startsWith(BLOCK_TAG_OPEN);
|
|
50
|
+
}
|
|
51
|
+
/** The block's body with its framing removed, for re-rendering as PRIOR SUMMARY. */
|
|
52
|
+
function stripBlockTags(text) {
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
const open = trimmed.indexOf('>');
|
|
55
|
+
const close = trimmed.lastIndexOf(BLOCK_TAG_CLOSE);
|
|
56
|
+
if (open < 0)
|
|
57
|
+
return trimmed;
|
|
58
|
+
const body = close > open ? trimmed.slice(open + 1, close) : trimmed.slice(open + 1);
|
|
59
|
+
return body.trim();
|
|
60
|
+
}
|
|
61
|
+
function renderMessage(message) {
|
|
62
|
+
if (message.role === 'user') {
|
|
63
|
+
if (isPriorSummaryBlock(message)) {
|
|
64
|
+
// CARRIED WHOLE, up to `summaryMaxChars` (D-22 / P1-9).
|
|
65
|
+
//
|
|
66
|
+
// A `<compacted_context>` block IS a `user` message (D-5, deliberately),
|
|
67
|
+
// so a per-kind table that clipped it to `userChars` (2 000) would discard
|
|
68
|
+
// up to two thirds of the previous summary - which §5.4 permits to be
|
|
69
|
+
// 6 000 long - BEFORE summarizing it, and then report `summarized`. Every
|
|
70
|
+
// compaction after the first would decay the record silently.
|
|
71
|
+
const body = stripBlockTags(partsToText(message.content));
|
|
72
|
+
return [{ text: `PRIOR SUMMARY: ${clip(body, COMPACTION_LIMITS.summaryMaxChars)}`, pinned: true }];
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
{ text: `USER: ${clip(partsToText(message.content), COMPACTION_LIMITS.userChars)}`, pinned: false },
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (message.role === 'tool_result') {
|
|
79
|
+
const status = message.isError ? 'error' : 'ok';
|
|
80
|
+
const body = clip(partsToText(message.content), COMPACTION_LIMITS.toolResultChars);
|
|
81
|
+
return [{ text: `RESULT[${status}] ${body}`, pinned: false }];
|
|
82
|
+
}
|
|
83
|
+
// Assistant: one entry per block, so a turn's text and its calls can be
|
|
84
|
+
// evicted independently.
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const block of message.content) {
|
|
87
|
+
if (block.type === 'text') {
|
|
88
|
+
if (block.text.trim().length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
out.push({ text: `ASSISTANT: ${clip(block.text, COMPACTION_LIMITS.assistantChars)}`, pinned: false });
|
|
91
|
+
}
|
|
92
|
+
else if (block.type === 'tool_call') {
|
|
93
|
+
const args = clip(JSON.stringify(block.args ?? {}), COMPACTION_LIMITS.toolArgChars);
|
|
94
|
+
out.push({ text: `CALL ${block.toolName}(${args})`, pinned: false });
|
|
95
|
+
}
|
|
96
|
+
// `thinking` blocks are DROPPED ENTIRELY (D-8): they are the model's private
|
|
97
|
+
// scratch, they are the single largest contributor at `thinkingLevel: high`,
|
|
98
|
+
// and nothing after the turn depends on them.
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The character budget for one digest.
|
|
104
|
+
*
|
|
105
|
+
* `min(digestMaxChars, (summarizerWindow - summarizerReserveTokens) * CHARS_PER_TOKEN)`.
|
|
106
|
+
* `CHARS_PER_TOKEN` comes from core (P2-3) rather than being re-spelled here: two
|
|
107
|
+
* chars-per-token constants that must agree and nothing checking that they do is
|
|
108
|
+
* how a digest budget and an occupancy estimate drift apart.
|
|
109
|
+
*/
|
|
110
|
+
export function digestBudgetChars(summarizerWindow) {
|
|
111
|
+
const ceiling = COMPACTION_LIMITS.digestMaxChars;
|
|
112
|
+
if (!summarizerWindow || summarizerWindow <= 0)
|
|
113
|
+
return ceiling;
|
|
114
|
+
const usableTokens = summarizerWindow - COMPACTION_LIMITS.summarizerReserveTokens;
|
|
115
|
+
if (usableTokens <= 0)
|
|
116
|
+
return ceiling;
|
|
117
|
+
return Math.min(ceiling, usableTokens * CHARS_PER_TOKEN);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render the head into a bounded transcript.
|
|
121
|
+
*
|
|
122
|
+
* OLDEST ENTRIES ARE DROPPED FIRST, except the pinned `PRIOR SUMMARY`, which is
|
|
123
|
+
* evicted last of all. A single omission marker is prepended so the summary can
|
|
124
|
+
* say that it is incomplete instead of pretending.
|
|
125
|
+
*/
|
|
126
|
+
export function buildDigest(input) {
|
|
127
|
+
const budgetChars = digestBudgetChars(input.summarizerWindow);
|
|
128
|
+
const entries = input.head.flatMap(renderMessage);
|
|
129
|
+
const sep = '\n';
|
|
130
|
+
let total = entries.reduce((acc, e) => acc + e.text.length + sep.length, 0);
|
|
131
|
+
if (total <= budgetChars) {
|
|
132
|
+
return { text: entries.map((e) => e.text).join(sep), omitted: 0, budgetChars };
|
|
133
|
+
}
|
|
134
|
+
// Evict oldest-first among the unpinned. The marker itself costs characters,
|
|
135
|
+
// so it is charged up front against a worst-case width.
|
|
136
|
+
const kept = [...entries];
|
|
137
|
+
let omitted = 0;
|
|
138
|
+
let reserve = OMISSION_MARKER(entries.length).length + sep.length;
|
|
139
|
+
for (let i = 0; i < kept.length && total + reserve > budgetChars; i += 1) {
|
|
140
|
+
const entry = kept[i];
|
|
141
|
+
if (entry.pinned)
|
|
142
|
+
continue;
|
|
143
|
+
total -= entry.text.length + sep.length;
|
|
144
|
+
kept[i] = { text: '', pinned: false };
|
|
145
|
+
omitted += 1;
|
|
146
|
+
}
|
|
147
|
+
// Still over budget with only pinned entries left: clip the pinned text rather
|
|
148
|
+
// than dropping it, so the densest record survives in some form.
|
|
149
|
+
if (total + reserve > budgetChars) {
|
|
150
|
+
const room = Math.max(0, budgetChars - reserve);
|
|
151
|
+
const joined = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
|
|
152
|
+
return {
|
|
153
|
+
text: `${OMISSION_MARKER(omitted)}${sep}${joined.slice(0, room)}`,
|
|
154
|
+
omitted,
|
|
155
|
+
budgetChars,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const body = kept.filter((e) => e.text.length > 0).map((e) => e.text).join(sep);
|
|
159
|
+
return {
|
|
160
|
+
text: omitted > 0 ? `${OMISSION_MARKER(omitted)}${sep}${body}` : body,
|
|
161
|
+
omitted,
|
|
162
|
+
budgetChars,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPACTION_LIMITS — the single authority on every bound context compaction
|
|
3
|
+
* enforces (context-auto-compaction §5.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
|
|
7
|
+
* byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
|
|
8
|
+
* and adding `compaction` to that list are the SAME change (C-7) - a scanner
|
|
9
|
+
* that silently stops scanning is worse than no scanner. `fast/limits.ts`
|
|
10
|
+
* records the identical trap one feature earlier.
|
|
11
|
+
*
|
|
12
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
13
|
+
* `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
|
|
14
|
+
*
|
|
15
|
+
* - The entries below are STRUCTURAL. They describe what a digest, a
|
|
16
|
+
* summarization call and a card can physically carry. A user has no business
|
|
17
|
+
* tuning them.
|
|
18
|
+
* - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
|
|
19
|
+
* how much to keep, which model, what to do on failure. Those are clamped,
|
|
20
|
+
* persisted and user-facing.
|
|
21
|
+
*/
|
|
22
|
+
export declare const COMPACTION_LIMITS: {
|
|
23
|
+
/** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
|
|
24
|
+
readonly digestMaxChars: 120000;
|
|
25
|
+
/** Output + system prompt + framing, kept out of the digest's share of the window. */
|
|
26
|
+
readonly summarizerReserveTokens: 8000;
|
|
27
|
+
readonly userChars: 2000;
|
|
28
|
+
readonly assistantChars: 2000;
|
|
29
|
+
readonly toolArgChars: 200;
|
|
30
|
+
readonly toolResultChars: 600;
|
|
31
|
+
/**
|
|
32
|
+
* Clip bound on the TAGGED text anchor.
|
|
33
|
+
*
|
|
34
|
+
* A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
|
|
35
|
+
* verbatim-standalone past this length and merged it into the block below it,
|
|
36
|
+
* which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
|
|
37
|
+
* the second compaction the "first user message" is a summary. A 4 000-character
|
|
38
|
+
* first message clipped with a visible marker keeps the rule "one leading
|
|
39
|
+
* anchor, always recognizable"; a 40 000-character first message is not a goal
|
|
40
|
+
* statement, and truncating it with a marker is the honest outcome.
|
|
41
|
+
*/
|
|
42
|
+
readonly anchorChars: 4000;
|
|
43
|
+
/** What the summarizer prompt asks for, and what the splice clips to. */
|
|
44
|
+
readonly summaryMaxChars: 6000;
|
|
45
|
+
readonly summaryOutputTokens: 2048;
|
|
46
|
+
/**
|
|
47
|
+
* Wall-clock budget for ONE summarization call.
|
|
48
|
+
*
|
|
49
|
+
* Longer than the fast reviewer's 20 s because the input is ~30x larger and the
|
|
50
|
+
* output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
|
|
51
|
+
* rather than a missing critique.
|
|
52
|
+
*
|
|
53
|
+
* COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
|
|
54
|
+
* host budget is two calls at this value back to back plus the digest render,
|
|
55
|
+
* so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
|
|
56
|
+
* this without raising that makes the §3.7 ladder unreachable and turns every
|
|
57
|
+
* slow-but-correct summarization into `manager_timeout`.
|
|
58
|
+
*/
|
|
59
|
+
readonly callTimeoutMs: 45000;
|
|
60
|
+
/** Turns between compactions. Never two in a row: a compaction that just ran
|
|
61
|
+
* needs a real turn before its effect is measurable, because `lastUsage` is
|
|
62
|
+
* still the pre-compaction number until the next `turn_end`. */
|
|
63
|
+
readonly minTurnsBetween: 2;
|
|
64
|
+
/** Compactions STARTED per run, both triggers, hard. */
|
|
65
|
+
readonly maxPerRun: 5;
|
|
66
|
+
/** A splice must project occupancy below `threshold - this`, or it bought two turns. */
|
|
67
|
+
readonly minReclaimRatio: 0.15;
|
|
68
|
+
/** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
|
|
69
|
+
readonly stuckLimit: 2;
|
|
70
|
+
/**
|
|
71
|
+
* Clip bound on an oversized `tool_result` body inside the RETAINED tail.
|
|
72
|
+
*
|
|
73
|
+
* 2 000 rather than `toolResultChars`' 600, and the two are not the same
|
|
74
|
+
* question. That one bounds a message in the DIGEST, which the summarizer
|
|
75
|
+
* reads once and which exists to be lossy. This one bounds a message the model
|
|
76
|
+
* keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
|
|
77
|
+
* continue on them. Relief only ever runs when the alternative is a request the
|
|
78
|
+
* provider cannot accept.
|
|
79
|
+
*/
|
|
80
|
+
readonly tailToolResultChars: 2000;
|
|
81
|
+
/**
|
|
82
|
+
* Room reserved for the `<compacted_context>` block in the tail-budget
|
|
83
|
+
* projection.
|
|
84
|
+
*
|
|
85
|
+
* THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
|
|
86
|
+
* relief measures a spliced history whose summary body is already in it, but
|
|
87
|
+
* the `plan === null` path measures one where nothing has been written yet, and
|
|
88
|
+
* a budget with no allowance would let the block push the result back over.
|
|
89
|
+
*/
|
|
90
|
+
readonly blockAllowanceTokens: 2000;
|
|
91
|
+
/**
|
|
92
|
+
* Files kept for ONE `runId`, newest first.
|
|
93
|
+
*
|
|
94
|
+
* PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
|
|
95
|
+
* `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
|
|
96
|
+
* processes as normal — so a global count-based prune would delete a live
|
|
97
|
+
* session's archives out from under it.
|
|
98
|
+
*/
|
|
99
|
+
readonly archiveMaxFiles: 20;
|
|
100
|
+
/**
|
|
101
|
+
* The cross-run sweep: 7 days.
|
|
102
|
+
*
|
|
103
|
+
* AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
|
|
104
|
+
* whose run finished a week ago; a count can delete the newest file another
|
|
105
|
+
* process wrote a second ago.
|
|
106
|
+
*/
|
|
107
|
+
readonly archiveMaxAgeMs: 604800000;
|
|
108
|
+
/**
|
|
109
|
+
* Per-file ceiling before dropped message bodies are clipped from the OLDEST
|
|
110
|
+
* end. The metadata and the summary are never clipped: a truncated archive
|
|
111
|
+
* that still says what happened beats none.
|
|
112
|
+
*/
|
|
113
|
+
readonly archiveMaxBytes: 8000000;
|
|
114
|
+
/** A child's `keepRecentTurns`, delivered through its config view. */
|
|
115
|
+
readonly childKeepRecentTurns: 2;
|
|
116
|
+
/**
|
|
117
|
+
* A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
|
|
118
|
+
*
|
|
119
|
+
* HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
|
|
120
|
+
* argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
|
|
121
|
+
* config path to it by design, and a child needs a tighter one. Both numbers
|
|
122
|
+
* therefore stay in the one file that owns structural bounds.
|
|
123
|
+
*/
|
|
124
|
+
readonly childMaxPerRun: 2;
|
|
125
|
+
/** Rows the collapsed transcript card renders; Ctrl+O expands. */
|
|
126
|
+
readonly cardTextRows: 6;
|
|
127
|
+
/**
|
|
128
|
+
* Below this many columns the status chip drops.
|
|
129
|
+
*
|
|
130
|
+
* 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
|
|
131
|
+
* `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
|
|
132
|
+
* that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
|
|
133
|
+
* The guaranteed reporting surface is `/compact status`, which is what makes
|
|
134
|
+
* dropping the chip acceptable at all.
|
|
135
|
+
*/
|
|
136
|
+
readonly statusCompactCols: 100;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Bumped whenever the wording of the `<compacted_context>` block or the
|
|
140
|
+
* summarizer system prompt changes, so a behaviour report can be tied to a block
|
|
141
|
+
* revision with one grep - exactly what `FAST_BLOCK_VERSION` and
|
|
142
|
+
* `TEAM_BLOCK_VERSION` are for.
|
|
143
|
+
*/
|
|
144
|
+
export declare const COMPACTION_BLOCK_VERSION = "v1-2026-08";
|
|
145
|
+
/** Opening tag of the verbatim goal anchor (§3.6.5). */
|
|
146
|
+
export declare const ANCHOR_TAG_OPEN = "<original_task>";
|
|
147
|
+
export declare const ANCHOR_TAG_CLOSE = "</original_task>";
|
|
148
|
+
/** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
|
|
149
|
+
export declare const BLOCK_TAG_OPEN = "<compacted_context";
|
|
150
|
+
export declare const BLOCK_TAG_CLOSE = "</compacted_context>";
|
|
151
|
+
/** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
|
|
152
|
+
export declare const ANCHOR_TRUNCATION_MARK = " [truncated]";
|
|
153
|
+
/**
|
|
154
|
+
* What tail relief writes where it removed text (hardening §3.3.2 rule 4).
|
|
155
|
+
*
|
|
156
|
+
* IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
|
|
157
|
+
* model that sees this knows the output is partial and can re-run the tool; a
|
|
158
|
+
* model handed a silently truncated file cannot tell the difference between "the
|
|
159
|
+
* file ends here" and "the file was cut here".
|
|
160
|
+
*
|
|
161
|
+
* THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
|
|
162
|
+
* must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
|
|
163
|
+
* file's header.
|
|
164
|
+
*/
|
|
165
|
+
export declare function tailClipMarker(removed: number): string;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPACTION_LIMITS — the single authority on every bound context compaction
|
|
3
|
+
* enforces (context-auto-compaction §5.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
|
|
6
|
+
* (`glyphs.test.ts::inScope`), so no literal in this tree may hold a non-ASCII
|
|
7
|
+
* byte. That regex is a HARDCODED DIRECTORY LIST, which is why adding this tree
|
|
8
|
+
* and adding `compaction` to that list are the SAME change (C-7) - a scanner
|
|
9
|
+
* that silently stops scanning is worse than no scanner. `fast/limits.ts`
|
|
10
|
+
* records the identical trap one feature earlier.
|
|
11
|
+
*
|
|
12
|
+
* TWO KINDS OF NUMBER, AND THEY ARE NOT INTERCHANGEABLE - the distinction
|
|
13
|
+
* `TEAM_LIMITS`, `TODO_LIMITS` and `FAST_LIMITS` all state:
|
|
14
|
+
*
|
|
15
|
+
* - The entries below are STRUCTURAL. They describe what a digest, a
|
|
16
|
+
* summarization call and a card can physically carry. A user has no business
|
|
17
|
+
* tuning them.
|
|
18
|
+
* - The six `compaction.*` keys in `config/schema.ts` are POLICY: when to fire,
|
|
19
|
+
* how much to keep, which model, what to do on failure. Those are clamped,
|
|
20
|
+
* persisted and user-facing.
|
|
21
|
+
*/
|
|
22
|
+
export const COMPACTION_LIMITS = {
|
|
23
|
+
/** Ceiling on the rendered digest, before the summarizer-window check. ~30k tokens. */
|
|
24
|
+
digestMaxChars: 120_000,
|
|
25
|
+
/** Output + system prompt + framing, kept out of the digest's share of the window. */
|
|
26
|
+
summarizerReserveTokens: 8_000,
|
|
27
|
+
// --- Per-message clips in the digest (§3.6.3). ---
|
|
28
|
+
userChars: 2_000,
|
|
29
|
+
assistantChars: 2_000,
|
|
30
|
+
toolArgChars: 200,
|
|
31
|
+
toolResultChars: 600,
|
|
32
|
+
/**
|
|
33
|
+
* Clip bound on the TAGGED text anchor.
|
|
34
|
+
*
|
|
35
|
+
* A CLIP, NOT A SHAPE SWITCH (D-21 / P1-9). v1 of the design kept the anchor
|
|
36
|
+
* verbatim-standalone past this length and merged it into the block below it,
|
|
37
|
+
* which made the anchor rule LENGTH-DEPENDENT and therefore non-idempotent: on
|
|
38
|
+
* the second compaction the "first user message" is a summary. A 4 000-character
|
|
39
|
+
* first message clipped with a visible marker keeps the rule "one leading
|
|
40
|
+
* anchor, always recognizable"; a 40 000-character first message is not a goal
|
|
41
|
+
* statement, and truncating it with a marker is the honest outcome.
|
|
42
|
+
*/
|
|
43
|
+
anchorChars: 4_000,
|
|
44
|
+
/** What the summarizer prompt asks for, and what the splice clips to. */
|
|
45
|
+
summaryMaxChars: 6_000,
|
|
46
|
+
summaryOutputTokens: 2_048,
|
|
47
|
+
/**
|
|
48
|
+
* Wall-clock budget for ONE summarization call.
|
|
49
|
+
*
|
|
50
|
+
* Longer than the fast reviewer's 20 s because the input is ~30x larger and the
|
|
51
|
+
* output ~4x larger, and because the cost of a timeout here is a DEGRADED RUN
|
|
52
|
+
* rather than a missing critique.
|
|
53
|
+
*
|
|
54
|
+
* COUPLED TO THE ENGINE'S `COMPACTION_HARD_TIMEOUT_MS` (120 s). The worst legal
|
|
55
|
+
* host budget is two calls at this value back to back plus the digest render,
|
|
56
|
+
* so `2 * callTimeoutMs` must stay strictly under the engine's ceiling. Raising
|
|
57
|
+
* this without raising that makes the §3.7 ladder unreachable and turns every
|
|
58
|
+
* slow-but-correct summarization into `manager_timeout`.
|
|
59
|
+
*/
|
|
60
|
+
callTimeoutMs: 45_000,
|
|
61
|
+
// --- Anti-loop guards (§3.8). ---
|
|
62
|
+
/** Turns between compactions. Never two in a row: a compaction that just ran
|
|
63
|
+
* needs a real turn before its effect is measurable, because `lastUsage` is
|
|
64
|
+
* still the pre-compaction number until the next `turn_end`. */
|
|
65
|
+
minTurnsBetween: 2,
|
|
66
|
+
/** Compactions STARTED per run, both triggers, hard. */
|
|
67
|
+
maxPerRun: 5,
|
|
68
|
+
/** A splice must project occupancy below `threshold - this`, or it bought two turns. */
|
|
69
|
+
minReclaimRatio: 0.15,
|
|
70
|
+
/** Consecutive no-progress PRESSURE compactions before the proactive trigger self-disables. */
|
|
71
|
+
stuckLimit: 2,
|
|
72
|
+
// --- Tail relief (hardening §3.3 / W2). ---
|
|
73
|
+
/**
|
|
74
|
+
* Clip bound on an oversized `tool_result` body inside the RETAINED tail.
|
|
75
|
+
*
|
|
76
|
+
* 2 000 rather than `toolResultChars`' 600, and the two are not the same
|
|
77
|
+
* question. That one bounds a message in the DIGEST, which the summarizer
|
|
78
|
+
* reads once and which exists to be lossy. This one bounds a message the model
|
|
79
|
+
* keeps WORKING FROM: it is the last two-to-four turns, and the run is about to
|
|
80
|
+
* continue on them. Relief only ever runs when the alternative is a request the
|
|
81
|
+
* provider cannot accept.
|
|
82
|
+
*/
|
|
83
|
+
tailToolResultChars: 2_000,
|
|
84
|
+
/**
|
|
85
|
+
* Room reserved for the `<compacted_context>` block in the tail-budget
|
|
86
|
+
* projection.
|
|
87
|
+
*
|
|
88
|
+
* THE BLOCK IS WRITTEN AFTER THE PROJECTION, which is why this exists at all:
|
|
89
|
+
* relief measures a spliced history whose summary body is already in it, but
|
|
90
|
+
* the `plan === null` path measures one where nothing has been written yet, and
|
|
91
|
+
* a budget with no allowance would let the block push the result back over.
|
|
92
|
+
*/
|
|
93
|
+
blockAllowanceTokens: 2_000,
|
|
94
|
+
// --- The archive (hardening §3.5 / W4). ---
|
|
95
|
+
/**
|
|
96
|
+
* Files kept for ONE `runId`, newest first.
|
|
97
|
+
*
|
|
98
|
+
* PER RUN, NOT PER DIRECTORY (RV-6). `<home>/compaction/` is shared by every
|
|
99
|
+
* `aragon` on the machine — `file-sink.ts:94-95` already treats concurrent
|
|
100
|
+
* processes as normal — so a global count-based prune would delete a live
|
|
101
|
+
* session's archives out from under it.
|
|
102
|
+
*/
|
|
103
|
+
archiveMaxFiles: 20,
|
|
104
|
+
/**
|
|
105
|
+
* The cross-run sweep: 7 days.
|
|
106
|
+
*
|
|
107
|
+
* AGE RATHER THAN COUNT, for the reason above. Age can only ever delete files
|
|
108
|
+
* whose run finished a week ago; a count can delete the newest file another
|
|
109
|
+
* process wrote a second ago.
|
|
110
|
+
*/
|
|
111
|
+
archiveMaxAgeMs: 604_800_000,
|
|
112
|
+
/**
|
|
113
|
+
* Per-file ceiling before dropped message bodies are clipped from the OLDEST
|
|
114
|
+
* end. The metadata and the summary are never clipped: a truncated archive
|
|
115
|
+
* that still says what happened beats none.
|
|
116
|
+
*/
|
|
117
|
+
archiveMaxBytes: 8_000_000,
|
|
118
|
+
// --- Sub-agent compaction (hardening §3.4 / W3). ---
|
|
119
|
+
/** A child's `keepRecentTurns`, delivered through its config view. */
|
|
120
|
+
childKeepRecentTurns: 2,
|
|
121
|
+
/**
|
|
122
|
+
* A child's guard-2 ceiling, delivered through `Compactor`'s instance bound.
|
|
123
|
+
*
|
|
124
|
+
* HERE RATHER THAN AS A `compaction.*` KEY, and the placement is the whole
|
|
125
|
+
* argument: `maxPerRun` is STRUCTURAL (see this file's header), there is no
|
|
126
|
+
* config path to it by design, and a child needs a tighter one. Both numbers
|
|
127
|
+
* therefore stay in the one file that owns structural bounds.
|
|
128
|
+
*/
|
|
129
|
+
childMaxPerRun: 2,
|
|
130
|
+
/** Rows the collapsed transcript card renders; Ctrl+O expands. */
|
|
131
|
+
cardTextRows: 6,
|
|
132
|
+
/**
|
|
133
|
+
* Below this many columns the status chip drops.
|
|
134
|
+
*
|
|
135
|
+
* 100, matching all four existing peers - `retry-view.ts`, `fast/limits.ts`,
|
|
136
|
+
* `team/limits.ts`, `todo/limits.ts` (P2-2). A status-bar degradation ladder
|
|
137
|
+
* that is nearly-but-not-quite uniform is a reading hazard bought for nothing.
|
|
138
|
+
* The guaranteed reporting surface is `/compact status`, which is what makes
|
|
139
|
+
* dropping the chip acceptable at all.
|
|
140
|
+
*/
|
|
141
|
+
statusCompactCols: 100,
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Bumped whenever the wording of the `<compacted_context>` block or the
|
|
145
|
+
* summarizer system prompt changes, so a behaviour report can be tied to a block
|
|
146
|
+
* revision with one grep - exactly what `FAST_BLOCK_VERSION` and
|
|
147
|
+
* `TEAM_BLOCK_VERSION` are for.
|
|
148
|
+
*/
|
|
149
|
+
export const COMPACTION_BLOCK_VERSION = 'v1-2026-08';
|
|
150
|
+
/** Opening tag of the verbatim goal anchor (§3.6.5). */
|
|
151
|
+
export const ANCHOR_TAG_OPEN = '<original_task>';
|
|
152
|
+
export const ANCHOR_TAG_CLOSE = '</original_task>';
|
|
153
|
+
/** Opening tag of the spliced summary block. Recognized on the NEXT compaction. */
|
|
154
|
+
export const BLOCK_TAG_OPEN = '<compacted_context';
|
|
155
|
+
export const BLOCK_TAG_CLOSE = '</compacted_context>';
|
|
156
|
+
/** Marker appended to an anchor clipped at `anchorChars`. ASCII (see the header). */
|
|
157
|
+
export const ANCHOR_TRUNCATION_MARK = ' [truncated]';
|
|
158
|
+
/**
|
|
159
|
+
* What tail relief writes where it removed text (hardening §3.3.2 rule 4).
|
|
160
|
+
*
|
|
161
|
+
* IN THE HISTORY THE MODEL READS, at the exact place the data went missing. A
|
|
162
|
+
* model that sees this knows the output is partial and can re-run the tool; a
|
|
163
|
+
* model handed a silently truncated file cannot tell the difference between "the
|
|
164
|
+
* file ends here" and "the file was cut here".
|
|
165
|
+
*
|
|
166
|
+
* THE HOST OWNS IT, not core: `engine/compaction.ts` has no glyph vocabulary and
|
|
167
|
+
* must not learn one, so `relieveTail` takes this as a callback. ASCII, per this
|
|
168
|
+
* file's header.
|
|
169
|
+
*/
|
|
170
|
+
export function tailClipMarker(removed) {
|
|
171
|
+
return `\n[... ${removed} characters removed by context compaction ...]`;
|
|
172
|
+
}
|