@aragon-agent/cli 0.6.2 → 0.6.4
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 +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- 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 +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -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 +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -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 +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- 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/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- 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 +44 -9
- package/dist/ui/layout/budget.js +42 -10
- 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 +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -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 +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The summarizer's system prompt and the `<compacted_context>` splice
|
|
3
|
+
* (context-auto-compaction §3.6.4 / §3.6.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* PURE. Every input arrives as an argument, so the block a session produced can
|
|
8
|
+
* be reconstructed from a bug report with one call.
|
|
9
|
+
*/
|
|
10
|
+
import { ANCHOR_TAG_CLOSE, ANCHOR_TAG_OPEN, ANCHOR_TRUNCATION_MARK, BLOCK_TAG_CLOSE, BLOCK_TAG_OPEN, COMPACTION_BLOCK_VERSION, COMPACTION_LIMITS, } from './limits.js';
|
|
11
|
+
import { isPriorSummaryBlock } from './digest.js';
|
|
12
|
+
/**
|
|
13
|
+
* The section skeleton the summary must fill.
|
|
14
|
+
*
|
|
15
|
+
* THE HEADINGS ARE PART OF THE CONTRACT, because the next model to read them is
|
|
16
|
+
* an LLM: a summary with predictable sections can be scanned for "what is still
|
|
17
|
+
* open" without re-reading the whole thing.
|
|
18
|
+
*/
|
|
19
|
+
export function buildSummarySystemPrompt(opts) {
|
|
20
|
+
const lines = [
|
|
21
|
+
'You are compacting a software-engineering conversation so the agent can keep working',
|
|
22
|
+
'after older messages are dropped from its context window.',
|
|
23
|
+
'',
|
|
24
|
+
'Write a record of the conversation below using EXACTLY these sections, in this order:',
|
|
25
|
+
'',
|
|
26
|
+
'## Task',
|
|
27
|
+
"The user's original goal, and any revision to it.",
|
|
28
|
+
'## Decisions',
|
|
29
|
+
'What was decided and why. One line each.',
|
|
30
|
+
'## Files',
|
|
31
|
+
'path -> what changed in it, or what is in it that matters.',
|
|
32
|
+
'## Facts',
|
|
33
|
+
'Commands, paths, APIs, identifiers and values discovered. Verbatim where exactness matters.',
|
|
34
|
+
'## Open',
|
|
35
|
+
'What remains to be done, in the order it should be done.',
|
|
36
|
+
'## Pitfalls',
|
|
37
|
+
'What was tried and did not work, so it is not tried again.',
|
|
38
|
+
'',
|
|
39
|
+
'Rules:',
|
|
40
|
+
`- Stay under ${opts.maxChars} characters in total.`,
|
|
41
|
+
'- Prefer verbatim identifiers, paths and commands over prose describing them.',
|
|
42
|
+
'- NEVER invent a fact the transcript does not contain. An empty section is correct',
|
|
43
|
+
' when the transcript says nothing about it; write "(none)" under it.',
|
|
44
|
+
'- Write in the language of the conversation.',
|
|
45
|
+
'- Output the sections and nothing else. No preamble, no sign-off.',
|
|
46
|
+
];
|
|
47
|
+
if (opts.hasPriorSummary) {
|
|
48
|
+
// WITHOUT THIS CLAUSE the second compaction is a model deciding, with no
|
|
49
|
+
// instruction either way, how much of a summary a summary needs - which is
|
|
50
|
+
// the mechanism by which flattened summaries decay into nothing over a long
|
|
51
|
+
// run (D-22 / R-14).
|
|
52
|
+
lines.push('', 'The transcript below OPENS WITH A `PRIOR SUMMARY` SECTION. That section is an', 'authoritative record of everything that came before it, produced by an earlier', 'compaction of this same conversation. MERGE IT AND CARRY IT FORWARD - do not', 're-condense it. Facts, file paths and pitfalls already recorded there are to be', 'reproduced, not paraphrased away, and anything it marks as incomplete stays marked.');
|
|
53
|
+
}
|
|
54
|
+
if (opts.instructions && opts.instructions.trim().length > 0) {
|
|
55
|
+
lines.push('', '## Focus', 'The user asked you to pay particular attention to the following. Keep everything', 'the sections above require, and be more detailed where it is relevant:', opts.instructions.trim());
|
|
56
|
+
}
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
/** The user message the summarization call sends: the digest, and nothing else. */
|
|
60
|
+
export function buildSummaryUserMessage(digest, now) {
|
|
61
|
+
return { role: 'user', content: digest, timestamp: now };
|
|
62
|
+
}
|
|
63
|
+
function contentIsParts(content) {
|
|
64
|
+
return Array.isArray(content);
|
|
65
|
+
}
|
|
66
|
+
function firstText(content) {
|
|
67
|
+
if (typeof content === 'string')
|
|
68
|
+
return content;
|
|
69
|
+
return content.map((p) => (p.type === 'text' ? p.text : '[image]')).join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Recognize an anchor this feature produced on a previous pass.
|
|
73
|
+
*
|
|
74
|
+
* STRUCTURAL, NOT LENGTH-DEPENDENT (D-21 / P1-9). v1 of the design identified
|
|
75
|
+
* the anchor as "the first `user` message of the conversation" and chose between
|
|
76
|
+
* two shapes by length. Neither rule composes across repeated compactions,
|
|
77
|
+
* because after the first pass the first user message IS the compacted block:
|
|
78
|
+
* the merged shape promotes a SUMMARY to "the user's original task", and the
|
|
79
|
+
* standalone shape keeps the block as an anchor AND re-digests it. Neither
|
|
80
|
+
* failure raises anything, and both get worse every pass.
|
|
81
|
+
*/
|
|
82
|
+
export function isTaggedAnchor(message) {
|
|
83
|
+
if (message.role !== 'user')
|
|
84
|
+
return false;
|
|
85
|
+
return firstText(message.content).trimStart().startsWith(ANCHOR_TAG_OPEN);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build (or carry through) the leading anchor.
|
|
89
|
+
*
|
|
90
|
+
* ALWAYS ITS OWN LEADING MESSAGE, AND ALWAYS STRUCTURALLY IDENTIFIABLE. A
|
|
91
|
+
* summarizer that is 90 % faithful is fine for the middle of a conversation and
|
|
92
|
+
* catastrophic for the sentence that says what the user wanted (D-6).
|
|
93
|
+
*/
|
|
94
|
+
export function buildAnchor(messages) {
|
|
95
|
+
const first = messages.find((m) => m.role === 'user');
|
|
96
|
+
if (!first)
|
|
97
|
+
return null;
|
|
98
|
+
// Already ours: carry it through byte-for-byte. This is what makes the rule
|
|
99
|
+
// idempotent across an unbounded number of compactions.
|
|
100
|
+
if (isTaggedAnchor(first))
|
|
101
|
+
return { message: first, shape: 'tagged' };
|
|
102
|
+
// A message carrying `ContentPart[]` may depend on an IMAGE, which no tag can
|
|
103
|
+
// wrap and no clip can preserve. It is kept byte-for-byte verbatim, and the
|
|
104
|
+
// block records `anchor="verbatim"` so the next pass recognizes it by position
|
|
105
|
+
// rather than by a tag it cannot carry.
|
|
106
|
+
if (contentIsParts(first.content)) {
|
|
107
|
+
return { message: first, shape: 'verbatim' };
|
|
108
|
+
}
|
|
109
|
+
const raw = first.content;
|
|
110
|
+
const clipped = raw.length > COMPACTION_LIMITS.anchorChars
|
|
111
|
+
? `${raw.slice(0, COMPACTION_LIMITS.anchorChars)}${ANCHOR_TRUNCATION_MARK}`
|
|
112
|
+
: raw;
|
|
113
|
+
const message = {
|
|
114
|
+
role: 'user',
|
|
115
|
+
content: `${ANCHOR_TAG_OPEN}\n${clipped}\n${ANCHOR_TAG_CLOSE}`,
|
|
116
|
+
...(first.timestamp !== undefined ? { timestamp: first.timestamp } : {}),
|
|
117
|
+
};
|
|
118
|
+
return { message, shape: 'tagged' };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* How many leading messages are ALREADY structurally preserved, and must be
|
|
122
|
+
* carried through untouched rather than digested.
|
|
123
|
+
*
|
|
124
|
+
* This is `planCompaction`'s `protectedPrefix` (§3.6.2). The HOST computes it
|
|
125
|
+
* because it is the host that knows what an `<original_task>` message is; core
|
|
126
|
+
* stays free of any knowledge of the block format.
|
|
127
|
+
*/
|
|
128
|
+
export function countProtectedPrefix(messages) {
|
|
129
|
+
const first = messages[0];
|
|
130
|
+
if (!first)
|
|
131
|
+
return 0;
|
|
132
|
+
// Case 1 — a TAGGED anchor identifies itself. The block, if any, is next.
|
|
133
|
+
if (isTaggedAnchor(first)) {
|
|
134
|
+
return messages[1] && isPriorSummaryBlock(messages[1]) ? 2 : 1;
|
|
135
|
+
}
|
|
136
|
+
// Case 2 — a VERBATIM anchor is not self-identifying (no tag can wrap the image
|
|
137
|
+
// it exists to preserve), so it is recognized BY POSITION: a `user` message at
|
|
138
|
+
// index 0 followed IMMEDIATELY by our block. `messages[1]`, never `messages[n]`
|
|
139
|
+
// — reading the slot the anchor itself occupies is an off-by-one whose only
|
|
140
|
+
// symptom is that a verbatim-anchor session re-digests its own anchor and block
|
|
141
|
+
// on every subsequent compaction, which is the exact P1-9 decay this function
|
|
142
|
+
// exists to prevent, arriving through the back door.
|
|
143
|
+
if (first.role === 'user' && messages[1] && isPriorSummaryBlock(messages[1]))
|
|
144
|
+
return 2;
|
|
145
|
+
// Case 3 — no anchor of ours, but a block at the head anyway (a history hand-
|
|
146
|
+
// edited, or restored from a file this feature did not write).
|
|
147
|
+
if (isPriorSummaryBlock(first))
|
|
148
|
+
return 1;
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
/** Read `generation="N"` off a prior block, so the next one can say `N + 1`. */
|
|
152
|
+
export function readGeneration(messages) {
|
|
153
|
+
for (const message of messages) {
|
|
154
|
+
if (!isPriorSummaryBlock(message))
|
|
155
|
+
continue;
|
|
156
|
+
const text = firstText(message.content);
|
|
157
|
+
const match = /generation="(\d+)"/.exec(text);
|
|
158
|
+
if (match)
|
|
159
|
+
return Number.parseInt(match[1], 10);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
/** The body used when the ladder ran out and there is no summary (§3.7 rung 3). */
|
|
165
|
+
export function truncationBody(replaced) {
|
|
166
|
+
return (`${replaced} earlier ${replaced === 1 ? 'message was' : 'messages were'} dropped to free ` +
|
|
167
|
+
'context. Their content is not available. Do not assume anything about what they ' +
|
|
168
|
+
'contained; ask the user if you need it.');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The spliced block.
|
|
172
|
+
*
|
|
173
|
+
* A `user` MESSAGE, NEVER A FABRICATED ASSISTANT ONE (D-5). An assistant message
|
|
174
|
+
* claiming the model said something it did not is a lie the model will then act
|
|
175
|
+
* on, and it corrupts every subsequent turn. The XML tag is the same disclosure
|
|
176
|
+
* device `<fast_review>` and `<skill>` already use in this codebase, and the
|
|
177
|
+
* instruction line is what stops the model reading the block as the user ASKING
|
|
178
|
+
* for a summary.
|
|
179
|
+
*/
|
|
180
|
+
export function buildCompactedBlock(input) {
|
|
181
|
+
const body = input.summary.slice(0, COMPACTION_LIMITS.summaryMaxChars);
|
|
182
|
+
const header = `${BLOCK_TAG_OPEN} version="${COMPACTION_BLOCK_VERSION}" replaced="${input.replaced}" ` +
|
|
183
|
+
`turns="${input.turns}" anchor="${input.anchor}" generation="${input.generation}">`;
|
|
184
|
+
const intro = [
|
|
185
|
+
'The conversation above this point was compacted to free context. The following',
|
|
186
|
+
'is an accurate record of it. Continue the work from here; do not re-derive this',
|
|
187
|
+
'history, do not ask the user to repeat it, and do not apologise for the gap.',
|
|
188
|
+
];
|
|
189
|
+
if (input.truncated) {
|
|
190
|
+
intro.push('', 'THE RECORD IS INCOMPLETE: summarization failed, so the dropped messages were not', 'summarized at all. Treat anything not stated below as unknown rather than absent.');
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
role: 'user',
|
|
194
|
+
content: `${header}\n${intro.join('\n')}\n\n${body}\n${BLOCK_TAG_CLOSE}`,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tail-relief projection - what a retained tail may occupy, and whether it
|
|
3
|
+
* has to be clipped (context-auto-compaction-hardening §3.3.3 / W2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* ITS OWN MODULE BY A PRE-MADE DECISION, not an improvised split. The design's
|
|
8
|
+
* §7 records it in advance: "If `compactor.ts` approaches the guideline during
|
|
9
|
+
* implementation, the tail-relief projection block moves to
|
|
10
|
+
* `compaction/tail-budget.ts` - noted here so the decision is not improvised."
|
|
11
|
+
* It did (`CLAUDE.md` caps a source file at 1 000 lines), so it did.
|
|
12
|
+
*
|
|
13
|
+
* PURE APART FROM THE LOG SINK. Everything it needs arrives as an argument,
|
|
14
|
+
* which is what lets the projection be tested without standing up a `Compactor`.
|
|
15
|
+
*/
|
|
16
|
+
import { type Message, type TailReliefResult } from '@aragon-agent/core';
|
|
17
|
+
/**
|
|
18
|
+
* What the whole history may occupy and still leave room for one reply.
|
|
19
|
+
*
|
|
20
|
+
* THE BLOCK ALLOWANCE IS NOT PADDING. On the `plan === null` path the
|
|
21
|
+
* `<compacted_context>` block has not been written when this is measured, so a
|
|
22
|
+
* budget with no room reserved for it would let the block push the relieved
|
|
23
|
+
* history straight back over.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tailBudgetTokens(input: {
|
|
26
|
+
contextWindow: number;
|
|
27
|
+
maxOutputTokens: number;
|
|
28
|
+
}): number;
|
|
29
|
+
export interface TailReliefAttempt {
|
|
30
|
+
messages: readonly Message[];
|
|
31
|
+
/** Index the tail starts at; nothing before it is eligible. */
|
|
32
|
+
from: number;
|
|
33
|
+
systemPrompt: string;
|
|
34
|
+
budget: number;
|
|
35
|
+
/** W2 fires rarely; when it does it is the most interesting event in the session. */
|
|
36
|
+
log: (event: string, fields: Record<string, unknown>) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Relieve the tail when - and ONLY when - the history is over budget.
|
|
40
|
+
* `null` means "it fits, or nothing was eligible".
|
|
41
|
+
*
|
|
42
|
+
* THE PROJECTION IS THE GATE. Relief is a real, bounded, announced data loss
|
|
43
|
+
* inside the turns the run is about to continue from, so it must never become a
|
|
44
|
+
* routine tidy-up: it runs only when the alternative is a request the provider
|
|
45
|
+
* cannot accept.
|
|
46
|
+
*/
|
|
47
|
+
export declare function attemptTailRelief(attempt: TailReliefAttempt): TailReliefResult | null;
|
|
48
|
+
/** The shape `CompactionRecord.tailRelief` and the archive both carry. */
|
|
49
|
+
export declare function reliefSummary(relief: TailReliefResult): {
|
|
50
|
+
messages: number;
|
|
51
|
+
charsRemoved: number;
|
|
52
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tail-relief projection - what a retained tail may occupy, and whether it
|
|
3
|
+
* has to be clipped (context-auto-compaction-hardening §3.3.3 / W2).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* ITS OWN MODULE BY A PRE-MADE DECISION, not an improvised split. The design's
|
|
8
|
+
* §7 records it in advance: "If `compactor.ts` approaches the guideline during
|
|
9
|
+
* implementation, the tail-relief projection block moves to
|
|
10
|
+
* `compaction/tail-budget.ts` - noted here so the decision is not improvised."
|
|
11
|
+
* It did (`CLAUDE.md` caps a source file at 1 000 lines), so it did.
|
|
12
|
+
*
|
|
13
|
+
* PURE APART FROM THE LOG SINK. Everything it needs arrives as an argument,
|
|
14
|
+
* which is what lets the projection be tested without standing up a `Compactor`.
|
|
15
|
+
*/
|
|
16
|
+
import { estimatePromptTokens, relieveTail, } from '@aragon-agent/core';
|
|
17
|
+
import { COMPACTION_LIMITS, tailClipMarker } from './limits.js';
|
|
18
|
+
import { requiredHeadroom } from './pressure.js';
|
|
19
|
+
/**
|
|
20
|
+
* What the whole history may occupy and still leave room for one reply.
|
|
21
|
+
*
|
|
22
|
+
* THE BLOCK ALLOWANCE IS NOT PADDING. On the `plan === null` path the
|
|
23
|
+
* `<compacted_context>` block has not been written when this is measured, so a
|
|
24
|
+
* budget with no room reserved for it would let the block push the relieved
|
|
25
|
+
* history straight back over.
|
|
26
|
+
*/
|
|
27
|
+
export function tailBudgetTokens(input) {
|
|
28
|
+
return Math.max(0, input.contextWindow -
|
|
29
|
+
requiredHeadroom({ maxOutputTokens: input.maxOutputTokens }) -
|
|
30
|
+
COMPACTION_LIMITS.blockAllowanceTokens);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Relieve the tail when - and ONLY when - the history is over budget.
|
|
34
|
+
* `null` means "it fits, or nothing was eligible".
|
|
35
|
+
*
|
|
36
|
+
* THE PROJECTION IS THE GATE. Relief is a real, bounded, announced data loss
|
|
37
|
+
* inside the turns the run is about to continue from, so it must never become a
|
|
38
|
+
* routine tidy-up: it runs only when the alternative is a request the provider
|
|
39
|
+
* cannot accept.
|
|
40
|
+
*/
|
|
41
|
+
export function attemptTailRelief(attempt) {
|
|
42
|
+
if (attempt.budget <= 0)
|
|
43
|
+
return null;
|
|
44
|
+
const projected = estimatePromptTokens(attempt.messages, attempt.systemPrompt);
|
|
45
|
+
if (projected <= attempt.budget)
|
|
46
|
+
return null;
|
|
47
|
+
const relief = relieveTail(attempt.messages, {
|
|
48
|
+
from: attempt.from,
|
|
49
|
+
clipChars: COMPACTION_LIMITS.tailToolResultChars,
|
|
50
|
+
targetTokens: attempt.budget,
|
|
51
|
+
systemPrompt: attempt.systemPrompt,
|
|
52
|
+
marker: tailClipMarker,
|
|
53
|
+
});
|
|
54
|
+
if (relief.clippedMessages === 0)
|
|
55
|
+
return null;
|
|
56
|
+
attempt.log('compaction_tail_relief', {
|
|
57
|
+
clippedMessages: relief.clippedMessages,
|
|
58
|
+
charsRemoved: relief.charsRemoved,
|
|
59
|
+
projectedBefore: projected,
|
|
60
|
+
budget: attempt.budget,
|
|
61
|
+
});
|
|
62
|
+
return relief;
|
|
63
|
+
}
|
|
64
|
+
/** The shape `CompactionRecord.tailRelief` and the archive both carry. */
|
|
65
|
+
export function reliefSummary(relief) {
|
|
66
|
+
return { messages: relief.clippedMessages, charsRemoved: relief.charsRemoved };
|
|
67
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-local compaction shapes (context-auto-compaction §5.2 / §3.4.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* A CLI-LOCAL EVENT STREAM, like `FastEvent` / `TeamEvent` / `TodoEvent`, for
|
|
7
|
+
* the reason each of those records: core's `AgentEvent` union is a public
|
|
8
|
+
* contract and must not learn host shapes.
|
|
9
|
+
*
|
|
10
|
+
* NOTE THE ASYMMETRY WITH CORE. Core emits the two ENGINE events because the
|
|
11
|
+
* engine is what performs the splice and decides `applied`; the CLI emits the
|
|
12
|
+
* richer ones because only the CLI knows the model name, the price and the
|
|
13
|
+
* session totals.
|
|
14
|
+
*
|
|
15
|
+
* THIS STREAM IS THE SOLE SOURCE OF REDUCER ACTIONS (D-20 / P1-6), and
|
|
16
|
+
* `reduceEvent` gains NO cases for `compaction_start` / `compaction_end`. One
|
|
17
|
+
* compaction arriving through two channels is two transcript cards and a doubled
|
|
18
|
+
* `tokensReclaimed`; `reduceEvent`'s `default: return []` is already exactly
|
|
19
|
+
* right for the two core events, which makes "one authority" also the smallest
|
|
20
|
+
* possible change.
|
|
21
|
+
*/
|
|
22
|
+
import type { TokenUsage } from '@aragon-agent/core';
|
|
23
|
+
/**
|
|
24
|
+
* `relieved` MEANS THE TAIL WAS CLIPPED AND NOTHING WAS DROPPED (hardening
|
|
25
|
+
* RV-5) — the `plan === null` recovery of §3.3.3.
|
|
26
|
+
*
|
|
27
|
+
* IT EXISTS ONLY HERE. The port still reports `'truncated'` for it (DH-5),
|
|
28
|
+
* because the engine's question is narrower: "was this history reduced without a
|
|
29
|
+
* summary", and the answer is yes.
|
|
30
|
+
*
|
|
31
|
+
* WITHOUT IT THE CARD LIES. `detailLine` renders
|
|
32
|
+
* `summarized ${before - after} messages with ${model}` for `'summarized'`, and
|
|
33
|
+
* relief drops NO messages — so a compaction in which no model was called would
|
|
34
|
+
* render "summarized 0 messages with claude-haiku-4-5", in the rare and alarming
|
|
35
|
+
* case the user most needs to read correctly.
|
|
36
|
+
*
|
|
37
|
+
* ANY NEW `switch` ON THIS UNION MUST PAIR ITS BRANCHES. TypeScript finds the
|
|
38
|
+
* exhaustive switches; it does not find an `else` that meant `'summarized'`.
|
|
39
|
+
*/
|
|
40
|
+
export type CompactionMode = 'summarized' | 'truncated' | 'relieved' | 'none';
|
|
41
|
+
/**
|
|
42
|
+
* `manual` exists ONLY here, never in core.
|
|
43
|
+
*
|
|
44
|
+
* The engine's `CompactionTrigger` is `pressure | overflow`, because those are
|
|
45
|
+
* the two the LOOP can originate. A `/compact` while running is queued and then
|
|
46
|
+
* arrives at the loop as an ordinary `pressure` checkpoint, so the distinction
|
|
47
|
+
* is a host-side fact about WHY the flag was set - which is exactly the kind of
|
|
48
|
+
* thing a CLI-local type is for.
|
|
49
|
+
*/
|
|
50
|
+
export type CompactionUiTrigger = 'pressure' | 'overflow' | 'manual';
|
|
51
|
+
/** Whether a figure was measured by the provider or estimated by us. */
|
|
52
|
+
export type PressureSource = 'usage' | 'estimate';
|
|
53
|
+
export interface Pressure {
|
|
54
|
+
occupied: number;
|
|
55
|
+
contextWindow: number;
|
|
56
|
+
/** `occupied / contextWindow`, clamped to [0, 1]. */
|
|
57
|
+
ratio: number;
|
|
58
|
+
/** `contextWindow - occupied`, floored at 0. */
|
|
59
|
+
headroom: number;
|
|
60
|
+
source: PressureSource;
|
|
61
|
+
/** False when the window came from `buildRuntimeModel`'s 128k placeholder. */
|
|
62
|
+
windowKnown: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
|
|
65
|
+
* clamped to `>= 0`, or `undefined` before the first `turn_end`.
|
|
66
|
+
*
|
|
67
|
+
* THE SYSTEMATIC PART OF THE ESTIMATOR'S ERROR (D-23 / P1-11), chiefly the
|
|
68
|
+
* TOOL SCHEMAS. `estimatePromptTokens` counts the system prompt and the message
|
|
69
|
+
* bodies and NOTHING ELSE, while the loop also sends `tools: toolDefs` and the
|
|
70
|
+
* provider's `input_tokens` includes them - several thousand tokens for this
|
|
71
|
+
* CLI's toolset. So the estimate is biased LOW, always, in the one direction
|
|
72
|
+
* that hurts: the resumed 180k-token session is exactly where the estimate is
|
|
73
|
+
* used, and it reads lower than the truth.
|
|
74
|
+
*
|
|
75
|
+
* One shared FUNCTION is not the same as one shared UNIT. Everywhere an
|
|
76
|
+
* estimated figure is compared against a threshold or shown to a user it is
|
|
77
|
+
* `estimate + (estimateOffset ?? 0)`.
|
|
78
|
+
*/
|
|
79
|
+
estimateOffset?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Tokens estimated for messages APPENDED AFTER the measurement in `lastUsage`
|
|
82
|
+
* (hardening §3.2 / W1). `0` on the estimate branch, and `0` whenever the
|
|
83
|
+
* recorded prefix is unusable.
|
|
84
|
+
*
|
|
85
|
+
* ALWAYS PRESENT, never optional, so `isApproximate` is a total function and a
|
|
86
|
+
* consumer cannot forget the `?? 0`. `> 0` is what makes the measured branch
|
|
87
|
+
* approximate: the base is the provider's own number for a request that really
|
|
88
|
+
* happened, and this is a guess about messages no request has yet carried.
|
|
89
|
+
*/
|
|
90
|
+
deltaTokens: number;
|
|
91
|
+
}
|
|
92
|
+
/** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
|
|
93
|
+
export interface CompactionRecord {
|
|
94
|
+
/** 1-based within the session, so the card can name itself. */
|
|
95
|
+
index: number;
|
|
96
|
+
trigger: CompactionUiTrigger;
|
|
97
|
+
mode: CompactionMode;
|
|
98
|
+
applied: boolean;
|
|
99
|
+
reason?: string;
|
|
100
|
+
messagesBefore: number;
|
|
101
|
+
messagesAfter: number;
|
|
102
|
+
tokensBefore: number;
|
|
103
|
+
tokensAfter: number;
|
|
104
|
+
summary?: string;
|
|
105
|
+
model: string;
|
|
106
|
+
durationMs: number;
|
|
107
|
+
/** What the summarization call itself spent. */
|
|
108
|
+
usage?: TokenUsage;
|
|
109
|
+
/** Set when an overflow attempt halved `keepRecentTurns` for itself (D-24). */
|
|
110
|
+
keepRecentTurnsUsed?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Present ONLY when tail relief fired (hardening §3.3.4 / W2).
|
|
113
|
+
*
|
|
114
|
+
* A bounded, ANNOUNCED data loss inside the turns the run is about to continue
|
|
115
|
+
* from, so it is disclosed on the card, in `/compact status`, on the JSON
|
|
116
|
+
* stream and in the archive — the honesty rule `truncated` already follows.
|
|
117
|
+
*/
|
|
118
|
+
tailRelief?: {
|
|
119
|
+
messages: number;
|
|
120
|
+
charsRemoved: number;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* The archive file this compaction was written to, for `/compact show`
|
|
124
|
+
* (hardening §3.5 / W4). Absent when `compaction.archive` is off, and absent
|
|
125
|
+
* when the write failed — a best-effort artefact never claims to exist.
|
|
126
|
+
*/
|
|
127
|
+
archivePath?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* WHAT DELIBERATELY DOES NOT LIVE ON `CompactionRecord` (hardening RV-3): the
|
|
131
|
+
* dropped `Message[]`.
|
|
132
|
+
*
|
|
133
|
+
* This record is dispatched into the reducer (`App.tsx`'s `compaction_end` case)
|
|
134
|
+
* and retained in view state for the transcript card's lifetime. Hanging a
|
|
135
|
+
* megabyte-scale array off it would park the compacted history in React state
|
|
136
|
+
* and drag it through every subsequent render — an archive feature that creates
|
|
137
|
+
* the leak it audits. The messages travel on `Compactor.takeLastDropped()`
|
|
138
|
+
* instead, which never enters the event stream.
|
|
139
|
+
*/
|
|
140
|
+
/** Everything `/compact status`, the chip and the settings row read. */
|
|
141
|
+
export interface CompactionSnapshot {
|
|
142
|
+
/** Registered AND enabled AND a summarizer model resolves. */
|
|
143
|
+
live: boolean;
|
|
144
|
+
/** The summarizer's model id, or `''`. */
|
|
145
|
+
model: string;
|
|
146
|
+
/**
|
|
147
|
+
* How many compactions ANNOUNCED themselves (quiet-noop §6.2 / D-5).
|
|
148
|
+
*
|
|
149
|
+
* COMPACTIONS THAT RAN, NOT CHECKPOINTS THAT FIRED. A `pressure` compaction
|
|
150
|
+
* that declines before calling a model or changing the history never advances
|
|
151
|
+
* this, which is what keeps the card's `#N` contiguous. `declined` below is
|
|
152
|
+
* the other half of the same total.
|
|
153
|
+
*/
|
|
154
|
+
compactions: number;
|
|
155
|
+
/**
|
|
156
|
+
* How many `pressure` checkpoints declined BEFORE committing to any work
|
|
157
|
+
* (quiet-noop D-9 / P1-2).
|
|
158
|
+
*
|
|
159
|
+
* OPTIONAL SO EVERY EXISTING CONSTRUCTION SITE STAYS UNTOUCHED, and rendered
|
|
160
|
+
* by `/compact status` only when non-zero — a healthy session must not read
|
|
161
|
+
* `(0 checkpoints declined)`. This is the one human surface that still speaks
|
|
162
|
+
* about a decline after the transcript went quiet, which is what keeps
|
|
163
|
+
* `context-auto-compaction` §6.5's "guaranteed reporting surface" promise true.
|
|
164
|
+
*/
|
|
165
|
+
declined?: number;
|
|
166
|
+
/** Sum of `(tokensBefore - tokensAfter)` over applied compactions. */
|
|
167
|
+
tokensReclaimed: number;
|
|
168
|
+
/** What compaction itself has spent this session. */
|
|
169
|
+
usage: TokenUsage;
|
|
170
|
+
/**
|
|
171
|
+
* CARRIED, NEVER RE-DERIVED (the C-11 / RV-4 lesson from `fast-model-tier`).
|
|
172
|
+
* An unknown model priced at `$0.00` would make the feature look free while it
|
|
173
|
+
* spends money - and here the DEFAULT summarizer is the session's own model.
|
|
174
|
+
*/
|
|
175
|
+
pricingUnknown: boolean;
|
|
176
|
+
inFlight: boolean;
|
|
177
|
+
selfDisabled: boolean;
|
|
178
|
+
selfDisabledReason?: string;
|
|
179
|
+
/** How many times this history has been compacted, from the block's `generation`. */
|
|
180
|
+
generation: number;
|
|
181
|
+
pressure: Pressure;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* THE COMPACTION EVENT STREAM.
|
|
185
|
+
*
|
|
186
|
+
* `compaction_start` and `compaction_end` are emitted ONLY for compactions that
|
|
187
|
+
* COMMITTED TO WORK (quiet-noop §3.1): a `pressure` compaction that declines
|
|
188
|
+
* before calling a model or changing the history emits NEITHER, and is
|
|
189
|
+
* deliberately indistinguishable from a turn at which `shouldCompact` returned
|
|
190
|
+
* false. `manual` and `overflow` always emit both.
|
|
191
|
+
*
|
|
192
|
+
* The pair is still BALANCED: every `compaction_start` is followed by exactly
|
|
193
|
+
* one `compaction_end` (a `finally` in the engine guarantees the verdict, and
|
|
194
|
+
* `settlePending` synthesizes a skeleton record when the compactor's own has not
|
|
195
|
+
* arrived). Consumers that count PAIRS are unaffected; consumers that counted
|
|
196
|
+
* ATTEMPTS must read `/compact status` or the JSONL diagnostic log instead -
|
|
197
|
+
* CORE's own `compaction_start` / `compaction_end` pair still fires for every
|
|
198
|
+
* attempt, declined or not, and that is the stream the log subscribes to.
|
|
199
|
+
*/
|
|
200
|
+
export type CompactionEvent = {
|
|
201
|
+
type: 'compaction_start';
|
|
202
|
+
index: number;
|
|
203
|
+
trigger: CompactionUiTrigger;
|
|
204
|
+
model: string;
|
|
205
|
+
} | {
|
|
206
|
+
type: 'compaction_end';
|
|
207
|
+
record: CompactionRecord;
|
|
208
|
+
} | {
|
|
209
|
+
type: 'usage';
|
|
210
|
+
usage: TokenUsage;
|
|
211
|
+
} | {
|
|
212
|
+
type: 'snapshot';
|
|
213
|
+
snapshot: CompactionSnapshot;
|
|
214
|
+
};
|
|
215
|
+
export type CompactionEventListener = (event: CompactionEvent) => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-local compaction shapes (context-auto-compaction §5.2 / §3.4.2).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* A CLI-LOCAL EVENT STREAM, like `FastEvent` / `TeamEvent` / `TodoEvent`, for
|
|
7
|
+
* the reason each of those records: core's `AgentEvent` union is a public
|
|
8
|
+
* contract and must not learn host shapes.
|
|
9
|
+
*
|
|
10
|
+
* NOTE THE ASYMMETRY WITH CORE. Core emits the two ENGINE events because the
|
|
11
|
+
* engine is what performs the splice and decides `applied`; the CLI emits the
|
|
12
|
+
* richer ones because only the CLI knows the model name, the price and the
|
|
13
|
+
* session totals.
|
|
14
|
+
*
|
|
15
|
+
* THIS STREAM IS THE SOLE SOURCE OF REDUCER ACTIONS (D-20 / P1-6), and
|
|
16
|
+
* `reduceEvent` gains NO cases for `compaction_start` / `compaction_end`. One
|
|
17
|
+
* compaction arriving through two channels is two transcript cards and a doubled
|
|
18
|
+
* `tokensReclaimed`; `reduceEvent`'s `default: return []` is already exactly
|
|
19
|
+
* right for the two core events, which makes "one authority" also the smallest
|
|
20
|
+
* possible change.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|