min-agent 0.5.0 → 0.6.0
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/README.md +32 -18
- package/dist/cli.js +538 -12
- package/docs/API.md +25 -8
- package/package.json +6 -6
- package/skills/self-config/SKILL.md +7 -3
- package/skills/self-config/reference.md +8 -4
- package/dist/agent.js +0 -1342
- package/dist/assistant-stream.js +0 -131
- package/dist/cli/commands/chat.js +0 -13
- package/dist/cli/commands/exec.js +0 -35
- package/dist/cli/commands/history.js +0 -58
- package/dist/cli/commands/index.js +0 -241
- package/dist/cli/commands/init.js +0 -18
- package/dist/cli/commands/mcp.js +0 -173
- package/dist/cli/commands/memory.js +0 -87
- package/dist/cli/commands/models.js +0 -21
- package/dist/cli/commands/permission.js +0 -12
- package/dist/cli/commands/rules.js +0 -33
- package/dist/cli/commands/sandbox.js +0 -13
- package/dist/cli/commands/serve.js +0 -9
- package/dist/cli/commands/setup.js +0 -4
- package/dist/cli/commands/shared.js +0 -16
- package/dist/cli/commands/skills.js +0 -119
- package/dist/cli/commands/think.js +0 -12
- package/dist/cli/commands/update.js +0 -7
- package/dist/cli/commands/write-config.js +0 -52
- package/dist/cli/errors.js +0 -36
- package/dist/cli/exec-prompt.js +0 -26
- package/dist/cli/option-helpers.js +0 -65
- package/dist/cli/program.js +0 -217
- package/dist/clipboard.js +0 -142
- package/dist/code-mode.js +0 -184
- package/dist/compaction.js +0 -644
- package/dist/config.js +0 -470
- package/dist/confirm.js +0 -172
- package/dist/context-window.js +0 -246
- package/dist/doom-loop.js +0 -278
- package/dist/fetch-timeout.js +0 -152
- package/dist/http-approvals.js +0 -60
- package/dist/http.js +0 -119
- package/dist/instructions.js +0 -196
- package/dist/logger.js +0 -95
- package/dist/markdown.js +0 -259
- package/dist/mcp.js +0 -1036
- package/dist/memory-cli.js +0 -33
- package/dist/memory.js +0 -295
- package/dist/model-catalog.js +0 -285
- package/dist/output.js +0 -76
- package/dist/paste-handler.js +0 -41
- package/dist/permission-cli.js +0 -40
- package/dist/plugins.js +0 -131
- package/dist/pricing.js +0 -119
- package/dist/provider.js +0 -63
- package/dist/question-format.js +0 -60
- package/dist/reasoning-stream.js +0 -158
- package/dist/sandbox-cli.js +0 -79
- package/dist/sandbox.js +0 -403
- package/dist/save-throttle.js +0 -45
- package/dist/scope.js +0 -23
- package/dist/serve/common.js +0 -425
- package/dist/serve/routes-chat.js +0 -367
- package/dist/serve/routes-mcp.js +0 -212
- package/dist/serve/routes-memory.js +0 -95
- package/dist/serve/routes-meta.js +0 -233
- package/dist/serve/routes-sessions.js +0 -61
- package/dist/serve/routes-skills.js +0 -70
- package/dist/serve.js +0 -138
- package/dist/sessions.js +0 -265
- package/dist/skills.js +0 -599
- package/dist/structured-output.js +0 -29
- package/dist/synthetic.js +0 -7
- package/dist/think-cli.js +0 -36
- package/dist/thinking-wire.js +0 -228
- package/dist/thinking.js +0 -142
- package/dist/title-gen.js +0 -55
- package/dist/token-display.js +0 -39
- package/dist/tool-display.js +0 -178
- package/dist/tool-output.js +0 -126
- package/dist/tools/apply_patch.js +0 -265
- package/dist/tools/atomic-file.js +0 -35
- package/dist/tools/backend.js +0 -61
- package/dist/tools/bash.js +0 -231
- package/dist/tools/code_search.js +0 -98
- package/dist/tools/edit.js +0 -68
- package/dist/tools/explore.js +0 -232
- package/dist/tools/glob.js +0 -40
- package/dist/tools/grep.js +0 -174
- package/dist/tools/index.js +0 -34
- package/dist/tools/question.js +0 -54
- package/dist/tools/read.js +0 -109
- package/dist/tools/search-searxng.js +0 -223
- package/dist/tools/search-serper.js +0 -189
- package/dist/tools/task.js +0 -165
- package/dist/tools/todo.js +0 -213
- package/dist/tools/web_fetch.js +0 -195
- package/dist/tools/web_search.js +0 -228
- package/dist/tools/write.js +0 -55
- package/dist/tui/App.js +0 -126
- package/dist/tui/ConfirmBar.js +0 -66
- package/dist/tui/InputBar.js +0 -616
- package/dist/tui/MessageList.js +0 -718
- package/dist/tui/ModelPicker.js +0 -113
- package/dist/tui/QuestionBar.js +0 -136
- package/dist/tui/SessionPicker.js +0 -79
- package/dist/tui/Spinner.js +0 -6
- package/dist/tui/StatusBar.js +0 -22
- package/dist/tui/ThinkPicker.js +0 -77
- package/dist/tui/agent-runner.js +0 -223
- package/dist/tui/bracketed-paste.js +0 -37
- package/dist/tui/caret-pos.js +0 -179
- package/dist/tui/caret.js +0 -69
- package/dist/tui/click-count.js +0 -13
- package/dist/tui/diff-view.js +0 -61
- package/dist/tui/drag-state.js +0 -49
- package/dist/tui/hydrate.js +0 -129
- package/dist/tui/index.js +0 -258
- package/dist/tui/input-history.js +0 -125
- package/dist/tui/layout.js +0 -105
- package/dist/tui/mouse.js +0 -46
- package/dist/tui/overlay-input.js +0 -12
- package/dist/tui/paste-draft.js +0 -173
- package/dist/tui/prompt-queue.js +0 -24
- package/dist/tui/selection.js +0 -232
- package/dist/tui/session-switch.js +0 -28
- package/dist/tui/slash-commands.js +0 -123
- package/dist/tui/slash-handler.js +0 -589
- package/dist/tui/text-width.js +0 -113
- package/dist/tui/theme.js +0 -12
- package/dist/tui/token-info.js +0 -7
- package/dist/tui/tool-children.js +0 -19
- package/dist/tui/types.js +0 -1
- package/dist/tui/undo-stack.js +0 -14
- package/dist/tui/use-sgr-mouse.js +0 -29
- package/dist/tui-chat.js +0 -456
- package/dist/updater.js +0 -116
- package/dist/xml-search.js +0 -194
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
- package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
- package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
- package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
- package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
- package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
package/dist/compaction.js
DELETED
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
import { generateText } from "ai";
|
|
2
|
-
import { getEffectiveConfig } from "./config.js";
|
|
3
|
-
import { resolveModel } from "./provider.js";
|
|
4
|
-
import { getContextWindowInfo, DEFAULT_CONTEXT_WINDOW } from "./context-window.js";
|
|
5
|
-
import { collectLoadedSkillNames, buildSkillReloadNote } from "./skills.js";
|
|
6
|
-
/**
|
|
7
|
-
* Context compaction system — modeled after opencode's SessionCompaction.
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* 1. Real token tracking from API responses
|
|
11
|
-
* 2. Structured summary template (Goal/Progress/Decisions/Files)
|
|
12
|
-
* 3. Incremental summaries (update previous summary instead of rewriting)
|
|
13
|
-
* 4. Tool output pruning with skill protection
|
|
14
|
-
* 5. Auto-continue after compaction with overflow replay
|
|
15
|
-
* 6. Token-budget-aware tail preservation
|
|
16
|
-
* 7. Model-aware thresholds (uses actual context window)
|
|
17
|
-
* 8. Configurable compaction model
|
|
18
|
-
*/
|
|
19
|
-
// ─── Structured Summary Template ───────────────────────────────────────────
|
|
20
|
-
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown below. Keep the section order unchanged.
|
|
21
|
-
|
|
22
|
-
## Goal
|
|
23
|
-
- [single-sentence task summary]
|
|
24
|
-
|
|
25
|
-
## Constraints & Preferences
|
|
26
|
-
- [user constraints, preferences, specs, or "(none)"]
|
|
27
|
-
|
|
28
|
-
## Progress
|
|
29
|
-
### Done
|
|
30
|
-
- [completed work or "(none)"]
|
|
31
|
-
|
|
32
|
-
### In Progress
|
|
33
|
-
- [current work or "(none)"]
|
|
34
|
-
|
|
35
|
-
### Blocked
|
|
36
|
-
- [blockers or "(none)"]
|
|
37
|
-
|
|
38
|
-
## Key Decisions
|
|
39
|
-
- [decision and why, or "(none)"]
|
|
40
|
-
|
|
41
|
-
## Next Steps
|
|
42
|
-
- [ordered next actions or "(none)"]
|
|
43
|
-
|
|
44
|
-
## Critical Context
|
|
45
|
-
- [important technical facts, numbers, dates, scores, URLs, errors, open questions, or "(none)"]
|
|
46
|
-
|
|
47
|
-
## Relevant Files
|
|
48
|
-
- [file or directory path: why it matters, or "(none)"]
|
|
49
|
-
|
|
50
|
-
Rules:
|
|
51
|
-
- Keep every section, even when empty.
|
|
52
|
-
- Use terse bullets, not prose paragraphs.
|
|
53
|
-
- Preserve exact file paths, commands, error strings, and identifiers when known.
|
|
54
|
-
- Do not mention the summary process or that context was compacted.`;
|
|
55
|
-
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
56
|
-
export const COMPACTION_RATIO = 0.75;
|
|
57
|
-
/** Start pruning old tool payloads only once the context is this full. */
|
|
58
|
-
export const PRUNE_PRESSURE_RATIO = 0.6;
|
|
59
|
-
/** Prune (largest first) until the estimate is back under this. */
|
|
60
|
-
export const PRUNE_TARGET_RATIO = 0.45;
|
|
61
|
-
const DEFAULT_TAIL_TURNS = 2;
|
|
62
|
-
const TAIL_TOKEN_BUDGET_RATIO = 0.04;
|
|
63
|
-
const MIN_TAIL_BUDGET = 2000;
|
|
64
|
-
const MAX_TAIL_BUDGET = 48000;
|
|
65
|
-
const KEEP_RECENT_ASSISTANT_UNITS = 4;
|
|
66
|
-
const TOOL_RESULT_STUB_CHARS = 400;
|
|
67
|
-
const TOOL_CALL_STRING_MAX = 500;
|
|
68
|
-
const SUMMARIZE_TOOL_CHARS = 400;
|
|
69
|
-
const SUMMARIZE_RESEARCH_CHARS = 2500;
|
|
70
|
-
const MAX_SUMMARIZE_CHARS = 100_000;
|
|
71
|
-
const OMITTED_TOOL_MARKER = "old tool output omitted";
|
|
72
|
-
const RESEARCH_SUMMARY_TOOLS = new Set(["search_web", "web_fetch"]);
|
|
73
|
-
/** Tools whose output should never be pruned during compaction */
|
|
74
|
-
const PRUNE_PROTECTED_TOOLS = new Set(["skill"]);
|
|
75
|
-
// ─── Token Tracker ─────────────────────────────────────────────────────────
|
|
76
|
-
export class TokenTracker {
|
|
77
|
-
_lastInputTokens = 0;
|
|
78
|
-
_totalOutputTokens = 0;
|
|
79
|
-
_totalInputTokens = 0;
|
|
80
|
-
listeners = new Set();
|
|
81
|
-
subscribe(listener) {
|
|
82
|
-
this.listeners.add(listener);
|
|
83
|
-
return () => {
|
|
84
|
-
this.listeners.delete(listener);
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
emit() {
|
|
88
|
-
for (const listener of this.listeners)
|
|
89
|
-
listener();
|
|
90
|
-
}
|
|
91
|
-
update(usage) {
|
|
92
|
-
this._totalInputTokens += usage.inputTokens ?? 0;
|
|
93
|
-
this._totalOutputTokens += usage.outputTokens ?? 0;
|
|
94
|
-
this._lastInputTokens = usage.inputTokens ?? 0;
|
|
95
|
-
this.emit();
|
|
96
|
-
}
|
|
97
|
-
/** Add to running totals only (e.g. compaction / sub-agent calls), without
|
|
98
|
-
* touching the "last step input" used for context-window display. */
|
|
99
|
-
add(usage) {
|
|
100
|
-
this._totalInputTokens += usage.inputTokens ?? 0;
|
|
101
|
-
this._totalOutputTokens += usage.outputTokens ?? 0;
|
|
102
|
-
this.emit();
|
|
103
|
-
}
|
|
104
|
-
get lastInputTokens() {
|
|
105
|
-
return this._lastInputTokens;
|
|
106
|
-
}
|
|
107
|
-
get totalInputTokens() {
|
|
108
|
-
return this._totalInputTokens;
|
|
109
|
-
}
|
|
110
|
-
get totalOutputTokens() {
|
|
111
|
-
return this._totalOutputTokens;
|
|
112
|
-
}
|
|
113
|
-
summary() {
|
|
114
|
-
return `context: ${this._lastInputTokens} | total in: ${this._totalInputTokens} out: ${this._totalOutputTokens}`;
|
|
115
|
-
}
|
|
116
|
-
reset() {
|
|
117
|
-
this._lastInputTokens = 0;
|
|
118
|
-
this._totalInputTokens = 0;
|
|
119
|
-
this._totalOutputTokens = 0;
|
|
120
|
-
this.emit();
|
|
121
|
-
}
|
|
122
|
-
resetContext() {
|
|
123
|
-
this._lastInputTokens = 0;
|
|
124
|
-
this.emit();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// ─── Token Estimation ──────────────────────────────────────────────────────
|
|
128
|
-
function isCjk(code) {
|
|
129
|
-
return ((code >= 0x2e80 && code <= 0x9fff) ||
|
|
130
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
131
|
-
(code >= 0xff00 && code <= 0xffef) ||
|
|
132
|
-
(code >= 0x20000 && code <= 0x3fffd) ||
|
|
133
|
-
(code >= 0x3040 && code <= 0x30ff) ||
|
|
134
|
-
(code >= 0xac00 && code <= 0xd7af));
|
|
135
|
-
}
|
|
136
|
-
/** ~4 ASCII chars per token, CJK chars weighted separately (much denser in tokens). */
|
|
137
|
-
function estimateTextTokens(text) {
|
|
138
|
-
let ascii = 0;
|
|
139
|
-
let other = 0;
|
|
140
|
-
for (const ch of text) {
|
|
141
|
-
if (isCjk(ch.codePointAt(0) ?? 0))
|
|
142
|
-
other++;
|
|
143
|
-
else
|
|
144
|
-
ascii++;
|
|
145
|
-
}
|
|
146
|
-
return ascii / 4 + other * 0.7;
|
|
147
|
-
}
|
|
148
|
-
const IMAGE_TOKEN_ESTIMATE = 800;
|
|
149
|
-
function estimatePartTokens(part) {
|
|
150
|
-
if (typeof part.text === "string")
|
|
151
|
-
return estimateTextTokens(part.text);
|
|
152
|
-
if (part.type === "image" || part.type === "file")
|
|
153
|
-
return IMAGE_TOKEN_ESTIMATE;
|
|
154
|
-
if (part.type === "tool-call" && part.input !== undefined) {
|
|
155
|
-
try {
|
|
156
|
-
return estimateTextTokens(JSON.stringify(part.input));
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
return 0;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
function estimateMessageTextTokens(msg) {
|
|
165
|
-
const content = msg.content;
|
|
166
|
-
if (typeof content === "string")
|
|
167
|
-
return estimateTextTokens(content);
|
|
168
|
-
if (Array.isArray(content)) {
|
|
169
|
-
let tokens = 0;
|
|
170
|
-
for (const part of content) {
|
|
171
|
-
if (isToolResultPart(part)) {
|
|
172
|
-
const text = toolResultText(part);
|
|
173
|
-
tokens += text ? estimateTextTokens(text) : 0;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
tokens += estimatePartTokens(part);
|
|
177
|
-
}
|
|
178
|
-
return tokens;
|
|
179
|
-
}
|
|
180
|
-
return 0;
|
|
181
|
-
}
|
|
182
|
-
/** Token estimation: ASCII ~4 chars/token, CJK ~0.7 token/char (aligned with opencode) */
|
|
183
|
-
export function estimateTokens(messages) {
|
|
184
|
-
return Math.ceil(messages.reduce((sum, msg) => sum + estimateMessageTextTokens(msg), 0));
|
|
185
|
-
}
|
|
186
|
-
/** Same estimator for a plain string (assistant text, tool-call JSON, …). */
|
|
187
|
-
export function estimateStringTokens(text) {
|
|
188
|
-
return Math.ceil(estimateTextTokens(text));
|
|
189
|
-
}
|
|
190
|
-
function estimateMessageTokens(msg) {
|
|
191
|
-
return Math.ceil(estimateMessageTextTokens(msg));
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Tokens spent on everything that is not in `messages`: the system prompt and
|
|
195
|
-
* the JSON schema of every tool. This is routinely 10k+ tokens with skills and
|
|
196
|
-
* MCP servers attached, so leaving it out makes every threshold too generous.
|
|
197
|
-
*/
|
|
198
|
-
export function estimateOverheadTokens(systemPrompt, tools) {
|
|
199
|
-
let tokens = estimateTextTokens(systemPrompt);
|
|
200
|
-
for (const [name, tool] of Object.entries(tools ?? {})) {
|
|
201
|
-
tokens += estimateTextTokens(name);
|
|
202
|
-
const t = tool;
|
|
203
|
-
if (typeof t?.description === "string")
|
|
204
|
-
tokens += estimateTextTokens(t.description);
|
|
205
|
-
const schema = t?.inputSchema ?? t?.parameters;
|
|
206
|
-
if (schema != null) {
|
|
207
|
-
try {
|
|
208
|
-
tokens += estimateTextTokens(JSON.stringify(schema));
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
/* non-serializable schema — skip */
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return Math.ceil(tokens);
|
|
216
|
-
}
|
|
217
|
-
// ─── Compaction Check ──────────────────────────────────────────────────────
|
|
218
|
-
/**
|
|
219
|
-
* Resolve context window: explicit override, then model-aware detection.
|
|
220
|
-
* Undetected models use 512k.
|
|
221
|
-
*/
|
|
222
|
-
async function resolveWindow(config) {
|
|
223
|
-
if (config?.maxTokens)
|
|
224
|
-
return { tokens: config.maxTokens, source: "config" };
|
|
225
|
-
try {
|
|
226
|
-
return await getContextWindowInfo(config?.modelId);
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Check if LLM summarization is needed.
|
|
234
|
-
* Uses configured or detected window; otherwise 512k.
|
|
235
|
-
*/
|
|
236
|
-
export async function needsCompaction(messages, tracker, config) {
|
|
237
|
-
if (config?.force)
|
|
238
|
-
return true;
|
|
239
|
-
const window = await resolveWindow(config);
|
|
240
|
-
const threshold = window.tokens * COMPACTION_RATIO;
|
|
241
|
-
const overhead = config?.overheadTokens ?? 0;
|
|
242
|
-
if (tracker && tracker.lastInputTokens > 0) {
|
|
243
|
-
// Reported input tokens already include the system prompt and tool schemas.
|
|
244
|
-
return tracker.lastInputTokens > threshold;
|
|
245
|
-
}
|
|
246
|
-
return estimateTokens(messages) + overhead > threshold;
|
|
247
|
-
}
|
|
248
|
-
function toolResultText(part) {
|
|
249
|
-
const out = part.output;
|
|
250
|
-
if (typeof out === "string")
|
|
251
|
-
return out;
|
|
252
|
-
if (out && typeof out === "object" && "value" in out) {
|
|
253
|
-
const v = out.value;
|
|
254
|
-
if (typeof v === "string")
|
|
255
|
-
return v;
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
function isOmittedToolOutput(text) {
|
|
260
|
-
return text.includes(OMITTED_TOOL_MARKER);
|
|
261
|
-
}
|
|
262
|
-
/** Keep the "full output saved to <path>" hint alive so the model can re-read it. */
|
|
263
|
-
function savedPathHint(text) {
|
|
264
|
-
const match = /Full output saved to:\s*(\S+)/.exec(text);
|
|
265
|
-
return match ? `, full output: ${match[1]}` : "";
|
|
266
|
-
}
|
|
267
|
-
function stubToolOutput(toolName, text) {
|
|
268
|
-
return `[${OMITTED_TOOL_MARKER}: ${toolName}, ${text.length} chars${savedPathHint(text)}]`;
|
|
269
|
-
}
|
|
270
|
-
function truncateToolCallValue(value) {
|
|
271
|
-
if (typeof value === "string") {
|
|
272
|
-
if (value.length <= TOOL_CALL_STRING_MAX)
|
|
273
|
-
return value;
|
|
274
|
-
return `${value.slice(0, TOOL_CALL_STRING_MAX)}\n\n[... ${value.length - TOOL_CALL_STRING_MAX} chars omitted from old tool call ...]`;
|
|
275
|
-
}
|
|
276
|
-
if (Array.isArray(value))
|
|
277
|
-
return value.map(truncateToolCallValue);
|
|
278
|
-
if (value && typeof value === "object") {
|
|
279
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, truncateToolCallValue(v)]));
|
|
280
|
-
}
|
|
281
|
-
return value;
|
|
282
|
-
}
|
|
283
|
-
function isProtectedToolMessage(msg) {
|
|
284
|
-
if (typeof msg.content === "string") {
|
|
285
|
-
return msg.content.includes("<skill_content");
|
|
286
|
-
}
|
|
287
|
-
if (Array.isArray(msg.content)) {
|
|
288
|
-
return msg.content.some((p) => (p.type === "tool-result" || p.type === "tool-call") &&
|
|
289
|
-
"toolName" in p &&
|
|
290
|
-
PRUNE_PROTECTED_TOOLS.has(p.toolName));
|
|
291
|
-
}
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
function unitStarts(messages) {
|
|
295
|
-
const starts = [];
|
|
296
|
-
for (let i = 0; i < messages.length; i++) {
|
|
297
|
-
const role = messages[i].role;
|
|
298
|
-
if (role === "user" || role === "assistant")
|
|
299
|
-
starts.push(i);
|
|
300
|
-
}
|
|
301
|
-
return starts;
|
|
302
|
-
}
|
|
303
|
-
function assistantStarts(messages) {
|
|
304
|
-
const starts = [];
|
|
305
|
-
for (let i = 0; i < messages.length; i++) {
|
|
306
|
-
if (messages[i].role === "assistant")
|
|
307
|
-
starts.push(i);
|
|
308
|
-
}
|
|
309
|
-
return starts;
|
|
310
|
-
}
|
|
311
|
-
/** Keep the last few assistant+tool steps intact so the model can see recent work. */
|
|
312
|
-
function protectFromIndex(messages) {
|
|
313
|
-
const starts = assistantStarts(messages);
|
|
314
|
-
if (starts.length <= KEEP_RECENT_ASSISTANT_UNITS)
|
|
315
|
-
return 0;
|
|
316
|
-
return starts[starts.length - KEEP_RECENT_ASSISTANT_UNITS];
|
|
317
|
-
}
|
|
318
|
-
function pruneToolResultMessage(msg) {
|
|
319
|
-
if (msg.role !== "tool" || !Array.isArray(msg.content) || isProtectedToolMessage(msg))
|
|
320
|
-
return null;
|
|
321
|
-
const parts = [...msg.content];
|
|
322
|
-
let changed = false;
|
|
323
|
-
for (let j = 0; j < parts.length; j++) {
|
|
324
|
-
const p = parts[j];
|
|
325
|
-
if (p.type !== "tool-result")
|
|
326
|
-
continue;
|
|
327
|
-
const text = toolResultText(p);
|
|
328
|
-
if (!text || isOmittedToolOutput(text) || text.length <= TOOL_RESULT_STUB_CHARS)
|
|
329
|
-
continue;
|
|
330
|
-
parts[j] = { ...p, output: { type: "text", value: stubToolOutput(p.toolName, text) } };
|
|
331
|
-
changed = true;
|
|
332
|
-
}
|
|
333
|
-
return changed ? { ...msg, content: parts } : null;
|
|
334
|
-
}
|
|
335
|
-
function pruneAssistantToolCalls(msg) {
|
|
336
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content) || isProtectedToolMessage(msg))
|
|
337
|
-
return null;
|
|
338
|
-
const parts = [...msg.content];
|
|
339
|
-
let changed = false;
|
|
340
|
-
for (let j = 0; j < parts.length; j++) {
|
|
341
|
-
const p = parts[j];
|
|
342
|
-
if (p.type !== "tool-call" || p.input === undefined)
|
|
343
|
-
continue;
|
|
344
|
-
const next = truncateToolCallValue(p.input);
|
|
345
|
-
let unchanged = next === p.input;
|
|
346
|
-
if (!unchanged) {
|
|
347
|
-
try {
|
|
348
|
-
unchanged = JSON.stringify(next) === JSON.stringify(p.input);
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
unchanged = false;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (unchanged)
|
|
355
|
-
continue;
|
|
356
|
-
parts[j] = { ...p, input: next };
|
|
357
|
-
changed = true;
|
|
358
|
-
}
|
|
359
|
-
return changed ? { ...msg, content: parts } : null;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Return a copy of messages with old tool payloads stubbed.
|
|
363
|
-
* Recent assistant steps stay intact. Skill results are never pruned.
|
|
364
|
-
* Does not mutate the input.
|
|
365
|
-
*
|
|
366
|
-
* Without a token budget (or with `force`) every prunable payload outside the
|
|
367
|
-
* recent window is stubbed. With a budget, pruning is pressure driven: nothing
|
|
368
|
-
* happens below `pressureTokens`, and above it the biggest payloads go first
|
|
369
|
-
* until the estimate is back under `targetTokens` — so a long session with a
|
|
370
|
-
* roomy context window keeps its tool output.
|
|
371
|
-
*/
|
|
372
|
-
export function pruneToolOutputs(messages, options = {}) {
|
|
373
|
-
const protectFrom = protectFromIndex(messages);
|
|
374
|
-
const candidates = [];
|
|
375
|
-
for (let i = 0; i < protectFrom; i++) {
|
|
376
|
-
const msg = messages[i];
|
|
377
|
-
const pruned = msg.role === "tool" ? pruneToolResultMessage(msg) : msg.role === "assistant" ? pruneAssistantToolCalls(msg) : null;
|
|
378
|
-
if (!pruned)
|
|
379
|
-
continue;
|
|
380
|
-
candidates.push({
|
|
381
|
-
index: i,
|
|
382
|
-
pruned,
|
|
383
|
-
tokensSaved: Math.max(0, estimateMessageTokens(msg) - estimateMessageTokens(pruned)),
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (candidates.length === 0)
|
|
387
|
-
return messages;
|
|
388
|
-
const pressure = options.pressureTokens ?? options.targetTokens;
|
|
389
|
-
const replaced = new Map();
|
|
390
|
-
if (options.force || pressure == null) {
|
|
391
|
-
for (const c of candidates)
|
|
392
|
-
replaced.set(c.index, c.pruned);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
let estimate = estimateTokens(messages) + (options.overheadTokens ?? 0);
|
|
396
|
-
if (estimate <= pressure)
|
|
397
|
-
return messages;
|
|
398
|
-
const goal = options.targetTokens ?? pressure;
|
|
399
|
-
for (const c of [...candidates].sort((a, b) => b.tokensSaved - a.tokensSaved)) {
|
|
400
|
-
if (estimate <= goal)
|
|
401
|
-
break;
|
|
402
|
-
if (c.tokensSaved <= 0)
|
|
403
|
-
continue;
|
|
404
|
-
replaced.set(c.index, c.pruned);
|
|
405
|
-
estimate -= c.tokensSaved;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (replaced.size === 0)
|
|
409
|
-
return messages;
|
|
410
|
-
return messages.map((msg, i) => replaced.get(i) ?? msg);
|
|
411
|
-
}
|
|
412
|
-
/** Replace `messages` in place when old tool payloads can be dropped. */
|
|
413
|
-
export function applyToolPrune(messages, options = {}) {
|
|
414
|
-
const pruned = pruneToolOutputs(messages, options);
|
|
415
|
-
if (pruned === messages)
|
|
416
|
-
return false;
|
|
417
|
-
messages.length = 0;
|
|
418
|
-
messages.push(...pruned);
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Select how many recent turns to keep verbatim based on token budget.
|
|
423
|
-
*/
|
|
424
|
-
async function selectTail(messages, config) {
|
|
425
|
-
const window = await resolveWindow(config);
|
|
426
|
-
const budget = Math.min(MAX_TAIL_BUDGET, Math.max(MIN_TAIL_BUDGET, Math.floor(window.tokens * TAIL_TOKEN_BUDGET_RATIO)));
|
|
427
|
-
const starts = unitStarts(messages);
|
|
428
|
-
if (starts.length === 0) {
|
|
429
|
-
return { headEnd: 0, tailStart: 0 };
|
|
430
|
-
}
|
|
431
|
-
let tokensUsed = 0;
|
|
432
|
-
let tailStart = messages.length;
|
|
433
|
-
for (let i = starts.length - 1; i >= 0; i--) {
|
|
434
|
-
const unitStart = starts[i];
|
|
435
|
-
const unitEnd = i < starts.length - 1 ? starts[i + 1] : messages.length;
|
|
436
|
-
let unitTokens = 0;
|
|
437
|
-
for (let j = unitStart; j < unitEnd; j++) {
|
|
438
|
-
unitTokens += estimateMessageTokens(messages[j]);
|
|
439
|
-
}
|
|
440
|
-
if (tokensUsed + unitTokens > budget && tokensUsed > 0)
|
|
441
|
-
break;
|
|
442
|
-
tokensUsed += unitTokens;
|
|
443
|
-
tailStart = unitStart;
|
|
444
|
-
}
|
|
445
|
-
if (tailStart >= messages.length)
|
|
446
|
-
tailStart = starts[starts.length - 1];
|
|
447
|
-
const keepUsers = config?.keepRecentTurns ?? DEFAULT_TAIL_TURNS;
|
|
448
|
-
const userStarts = [];
|
|
449
|
-
for (let i = 0; i < messages.length; i++) {
|
|
450
|
-
if (messages[i].role === "user")
|
|
451
|
-
userStarts.push(i);
|
|
452
|
-
}
|
|
453
|
-
if (tailStart === 0 && userStarts.length > keepUsers) {
|
|
454
|
-
tailStart = userStarts[userStarts.length - keepUsers];
|
|
455
|
-
}
|
|
456
|
-
return { headEnd: tailStart, tailStart };
|
|
457
|
-
}
|
|
458
|
-
// ─── Compaction Agent ──────────────────────────────────────────────────────
|
|
459
|
-
const COMPACTION_AGENT_SYSTEM = `You are an anchored context summarization assistant for coding sessions.
|
|
460
|
-
|
|
461
|
-
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
|
|
462
|
-
|
|
463
|
-
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
|
|
464
|
-
|
|
465
|
-
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths, identifiers, numeric scores, dates, and URLs when known, and prefer terse bullets over paragraphs.
|
|
466
|
-
|
|
467
|
-
When the history includes web search or fetched pages, copy concrete facts (names, scores, dates, URLs) into Critical Context. Do not drop numbers.
|
|
468
|
-
|
|
469
|
-
Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.`;
|
|
470
|
-
/**
|
|
471
|
-
* Resolve the model to use for compaction.
|
|
472
|
-
* If config.compaction.model is set, use that (allows cheap/fast model for summaries).
|
|
473
|
-
* Otherwise falls back to the main model.
|
|
474
|
-
*/
|
|
475
|
-
function resolveCompactionModel(mainModel) {
|
|
476
|
-
const cfg = getEffectiveConfig();
|
|
477
|
-
const compactionModelId = cfg.compaction?.model;
|
|
478
|
-
if (compactionModelId) {
|
|
479
|
-
return resolveModel(compactionModelId);
|
|
480
|
-
}
|
|
481
|
-
return mainModel;
|
|
482
|
-
}
|
|
483
|
-
const SKILL_NOTE_HEADER = "## Skills Previously Loaded";
|
|
484
|
-
function extractPreviousSummary(messages) {
|
|
485
|
-
const first = messages[0];
|
|
486
|
-
if (first?.role === "system" && typeof first.content === "string" && first.content.includes("[Context Summary")) {
|
|
487
|
-
const match = first.content.match(/\[Context Summary[^\]]*\]\n\n([\s\S]*)/);
|
|
488
|
-
return match?.[1]?.split(SKILL_NOTE_HEADER)[0]?.trimEnd();
|
|
489
|
-
}
|
|
490
|
-
return undefined;
|
|
491
|
-
}
|
|
492
|
-
/** Skills listed in a previous compaction note, so repeated compactions don't forget them. */
|
|
493
|
-
function extractNotedSkills(messages) {
|
|
494
|
-
const first = messages[0];
|
|
495
|
-
if (first?.role !== "system" || typeof first.content !== "string")
|
|
496
|
-
return [];
|
|
497
|
-
const section = first.content.split(SKILL_NOTE_HEADER)[1];
|
|
498
|
-
if (!section)
|
|
499
|
-
return [];
|
|
500
|
-
return section
|
|
501
|
-
.split("\n")
|
|
502
|
-
.filter((line) => line.startsWith("- "))
|
|
503
|
-
.map((line) => line
|
|
504
|
-
.slice(2)
|
|
505
|
-
.replace(/\s*\(base dir:.*$/, "")
|
|
506
|
-
.trim())
|
|
507
|
-
.filter(Boolean);
|
|
508
|
-
}
|
|
509
|
-
function buildCompactionPrompt(previousSummary, taskGoal) {
|
|
510
|
-
const goal = taskGoal?.trim()
|
|
511
|
-
? ["The original user goal that must be preserved in ## Goal:", taskGoal.trim(), ""].join("\n")
|
|
512
|
-
: "";
|
|
513
|
-
const anchor = previousSummary
|
|
514
|
-
? [
|
|
515
|
-
"Update the anchored summary below using the conversation history above.",
|
|
516
|
-
"Preserve still-true details, remove stale details, and merge in the new facts.",
|
|
517
|
-
"",
|
|
518
|
-
"<previous-summary>",
|
|
519
|
-
previousSummary,
|
|
520
|
-
"</previous-summary>",
|
|
521
|
-
].join("\n")
|
|
522
|
-
: "Create a new anchored summary from the conversation history above.";
|
|
523
|
-
return [goal, anchor, "", SUMMARY_TEMPLATE].filter(Boolean).join("\n");
|
|
524
|
-
}
|
|
525
|
-
function isToolResultPart(p) {
|
|
526
|
-
return typeof p === "object" && p !== null && "type" in p && p.type === "tool-result";
|
|
527
|
-
}
|
|
528
|
-
function clipForSummary(text, max) {
|
|
529
|
-
if (text.length <= max)
|
|
530
|
-
return text;
|
|
531
|
-
return `${text.slice(0, max)}\n[... ${text.length - max} chars omitted ...]`;
|
|
532
|
-
}
|
|
533
|
-
function clipLimitForTool(name) {
|
|
534
|
-
return RESEARCH_SUMMARY_TOOLS.has(name) ? SUMMARIZE_RESEARCH_CHARS : SUMMARIZE_TOOL_CHARS;
|
|
535
|
-
}
|
|
536
|
-
function messageToText(msg) {
|
|
537
|
-
if (typeof msg.content === "string")
|
|
538
|
-
return msg.content;
|
|
539
|
-
if (Array.isArray(msg.content)) {
|
|
540
|
-
const parts = [];
|
|
541
|
-
for (const p of msg.content) {
|
|
542
|
-
if ("text" in p && typeof p.text === "string") {
|
|
543
|
-
parts.push(p.text);
|
|
544
|
-
}
|
|
545
|
-
else if (p.type === "tool-call") {
|
|
546
|
-
const name = "toolName" in p ? String(p.toolName) : "tool";
|
|
547
|
-
let input = "";
|
|
548
|
-
try {
|
|
549
|
-
input = JSON.stringify("input" in p ? p.input : undefined);
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
input = "";
|
|
553
|
-
}
|
|
554
|
-
parts.push(`[tool-call ${name}] ${clipForSummary(input, clipLimitForTool(name))}`);
|
|
555
|
-
}
|
|
556
|
-
else if (isToolResultPart(p)) {
|
|
557
|
-
const name = p.toolName || "tool";
|
|
558
|
-
const out = p.output;
|
|
559
|
-
const raw = typeof out === "string"
|
|
560
|
-
? out
|
|
561
|
-
: out && typeof out === "object" && "value" in out && out.value != null
|
|
562
|
-
? String(out.value)
|
|
563
|
-
: JSON.stringify(out);
|
|
564
|
-
parts.push(`[tool-result ${name}] ${clipForSummary(raw, clipLimitForTool(name))}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return parts.join("\n");
|
|
568
|
-
}
|
|
569
|
-
return "";
|
|
570
|
-
}
|
|
571
|
-
function conversationForSummary(messages) {
|
|
572
|
-
const text = messages.map((msg) => `[${msg.role}]: ${clipForSummary(messageToText(msg), 1500)}`).join("\n\n");
|
|
573
|
-
if (text.length <= MAX_SUMMARIZE_CHARS)
|
|
574
|
-
return text;
|
|
575
|
-
const keep = Math.floor((MAX_SUMMARIZE_CHARS - 40) / 2);
|
|
576
|
-
return `${text.slice(0, keep)}\n\n[... middle omitted ...]\n\n${text.slice(-keep)}`;
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Summarize with one retry: losing the summary means falling back to plain
|
|
580
|
-
* pruning, which throws away far more context than a second attempt costs.
|
|
581
|
-
*/
|
|
582
|
-
async function summarizeWithRetry(model, prompt, config) {
|
|
583
|
-
const attempt = () => generateText({
|
|
584
|
-
model,
|
|
585
|
-
system: COMPACTION_AGENT_SYSTEM,
|
|
586
|
-
messages: [{ role: "user", content: prompt }],
|
|
587
|
-
abortSignal: config?.abortSignal,
|
|
588
|
-
});
|
|
589
|
-
try {
|
|
590
|
-
const first = await attempt();
|
|
591
|
-
if (first.text.trim())
|
|
592
|
-
return first;
|
|
593
|
-
console.error("[compaction] empty summary, retrying once");
|
|
594
|
-
}
|
|
595
|
-
catch (err) {
|
|
596
|
-
if (config?.abortSignal?.aborted)
|
|
597
|
-
throw err;
|
|
598
|
-
console.error("[compaction] summary attempt failed, retrying once:", err);
|
|
599
|
-
}
|
|
600
|
-
return attempt();
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Compact messages by summarizing older history with structured template.
|
|
604
|
-
* Supports incremental summaries, skill protection, configurable model,
|
|
605
|
-
* and overflow replay.
|
|
606
|
-
*/
|
|
607
|
-
export async function compactMessages(messages, model, config) {
|
|
608
|
-
const pruned = pruneToolOutputs(messages, { force: true });
|
|
609
|
-
const pruneOnly = () => pruned === messages ? { messages, compacted: false } : { messages: pruned, compacted: true };
|
|
610
|
-
const { headEnd, tailStart } = await selectTail(pruned, config);
|
|
611
|
-
if (headEnd <= 0)
|
|
612
|
-
return pruneOnly();
|
|
613
|
-
const toSummarize = pruned.slice(0, headEnd);
|
|
614
|
-
const toKeep = pruned.slice(tailStart);
|
|
615
|
-
const previousSummary = extractPreviousSummary(toSummarize);
|
|
616
|
-
const conversationText = conversationForSummary(toSummarize);
|
|
617
|
-
const compactionModel = resolveCompactionModel(model);
|
|
618
|
-
try {
|
|
619
|
-
const prompt = buildCompactionPrompt(previousSummary, config?.taskGoal);
|
|
620
|
-
const result = await summarizeWithRetry(compactionModel, conversationText + "\n\n" + prompt, config);
|
|
621
|
-
const summary = result.text;
|
|
622
|
-
const stillPresent = collectLoadedSkillNames(toKeep);
|
|
623
|
-
const droppedSkills = [
|
|
624
|
-
...new Set([...collectLoadedSkillNames(toSummarize), ...extractNotedSkills(toSummarize)]),
|
|
625
|
-
].filter((name) => !stillPresent.has(name));
|
|
626
|
-
const reloadNote = buildSkillReloadNote(droppedSkills);
|
|
627
|
-
const compactedMessages = [
|
|
628
|
-
{
|
|
629
|
-
role: "system",
|
|
630
|
-
content: `[Context Summary - Previous conversation was compacted]\n\n${summary}${reloadNote ? `\n\n${reloadNote}` : ""}`,
|
|
631
|
-
},
|
|
632
|
-
...toKeep,
|
|
633
|
-
];
|
|
634
|
-
return {
|
|
635
|
-
messages: compactedMessages,
|
|
636
|
-
compacted: true,
|
|
637
|
-
usage: result.usage,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
catch (error) {
|
|
641
|
-
console.error("[compaction] summary generation failed, keeping pruned messages:", error);
|
|
642
|
-
return pruneOnly();
|
|
643
|
-
}
|
|
644
|
-
}
|