min-agent 0.1.3 → 0.1.5
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 +269 -164
- package/bin/min-agent.js +6 -0
- package/dist/agent.js +383 -16
- package/dist/cli.js +20 -1
- package/dist/clipboard.js +106 -0
- package/dist/code-mode.js +166 -0
- package/dist/compaction.js +243 -48
- package/dist/config.js +34 -7
- package/dist/context-window.js +185 -0
- package/dist/doom-loop.js +36 -0
- package/dist/instructions.js +42 -0
- package/dist/mcp.js +28 -16
- package/dist/output.js +15 -2
- package/dist/serve.js +351 -3
- package/dist/sessions.js +13 -4
- package/dist/structured-output.js +29 -0
- package/dist/title-gen.js +48 -0
- package/dist/tools/bash.js +81 -74
- package/dist/tools/code_search.js +91 -0
- package/dist/tools/explore.js +104 -0
- package/dist/tools/index.js +10 -1
- package/dist/tools/question.js +53 -0
- package/dist/tools/read.js +14 -3
- package/dist/tools/task.js +98 -0
- package/dist/tools/todo.js +88 -0
- package/docs/API.md +298 -111
- package/package.json +3 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export function scanProject() {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const isGitRepo = existsSync(path.join(cwd, ".git"));
|
|
7
|
+
let branch;
|
|
8
|
+
if (isGitRepo) {
|
|
9
|
+
try {
|
|
10
|
+
branch = execSync("git branch --show-current", { encoding: "utf-8", cwd }).trim();
|
|
11
|
+
}
|
|
12
|
+
catch { }
|
|
13
|
+
}
|
|
14
|
+
const languages = [];
|
|
15
|
+
const configFiles = [];
|
|
16
|
+
const entryFiles = [];
|
|
17
|
+
// Detect by config files
|
|
18
|
+
const checks = [
|
|
19
|
+
{ file: "package.json", lang: "TypeScript/JavaScript", pm: "npm" },
|
|
20
|
+
{ file: "bun.lock", lang: "TypeScript/JavaScript", pm: "bun" },
|
|
21
|
+
{ file: "yarn.lock", lang: "TypeScript/JavaScript", pm: "yarn" },
|
|
22
|
+
{ file: "pnpm-lock.yaml", lang: "TypeScript/JavaScript", pm: "pnpm" },
|
|
23
|
+
{ file: "tsconfig.json", lang: "TypeScript" },
|
|
24
|
+
{ file: "Cargo.toml", lang: "Rust", pm: "cargo" },
|
|
25
|
+
{ file: "go.mod", lang: "Go" },
|
|
26
|
+
{ file: "pyproject.toml", lang: "Python", pm: "pip/uv" },
|
|
27
|
+
{ file: "requirements.txt", lang: "Python", pm: "pip" },
|
|
28
|
+
{ file: "Gemfile", lang: "Ruby", pm: "bundler" },
|
|
29
|
+
{ file: "pom.xml", lang: "Java", pm: "maven" },
|
|
30
|
+
{ file: "build.gradle", lang: "Java/Kotlin", pm: "gradle" },
|
|
31
|
+
{ file: "composer.json", lang: "PHP", pm: "composer" },
|
|
32
|
+
{ file: "Makefile", lang: "" },
|
|
33
|
+
{ file: "Dockerfile", lang: "" },
|
|
34
|
+
];
|
|
35
|
+
let packageManager;
|
|
36
|
+
let framework;
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
if (existsSync(path.join(cwd, check.file))) {
|
|
39
|
+
configFiles.push(check.file);
|
|
40
|
+
if (check.lang && !languages.includes(check.lang))
|
|
41
|
+
languages.push(check.lang);
|
|
42
|
+
if (check.pm && !packageManager)
|
|
43
|
+
packageManager = check.pm;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Detect framework from package.json
|
|
47
|
+
if (existsSync(path.join(cwd, "package.json"))) {
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
50
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
51
|
+
if (allDeps["next"])
|
|
52
|
+
framework = "Next.js";
|
|
53
|
+
else if (allDeps["nuxt"])
|
|
54
|
+
framework = "Nuxt";
|
|
55
|
+
else if (allDeps["@angular/core"])
|
|
56
|
+
framework = "Angular";
|
|
57
|
+
else if (allDeps["vue"])
|
|
58
|
+
framework = "Vue";
|
|
59
|
+
else if (allDeps["react"])
|
|
60
|
+
framework = "React";
|
|
61
|
+
else if (allDeps["svelte"])
|
|
62
|
+
framework = "Svelte";
|
|
63
|
+
else if (allDeps["express"])
|
|
64
|
+
framework = "Express";
|
|
65
|
+
else if (allDeps["fastify"])
|
|
66
|
+
framework = "Fastify";
|
|
67
|
+
else if (allDeps["hono"])
|
|
68
|
+
framework = "Hono";
|
|
69
|
+
else if (allDeps["effect"])
|
|
70
|
+
framework = "Effect";
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
}
|
|
74
|
+
// Find entry files
|
|
75
|
+
const entryPatterns = [
|
|
76
|
+
"src/index.ts", "src/index.js", "src/main.ts", "src/main.js",
|
|
77
|
+
"src/app.ts", "src/app.js", "index.ts", "index.js",
|
|
78
|
+
"main.ts", "main.js", "app.ts", "app.js",
|
|
79
|
+
"src/lib.rs", "main.go", "main.py", "app.py",
|
|
80
|
+
];
|
|
81
|
+
for (const p of entryPatterns) {
|
|
82
|
+
if (existsSync(path.join(cwd, p)))
|
|
83
|
+
entryFiles.push(p);
|
|
84
|
+
}
|
|
85
|
+
// Build summary
|
|
86
|
+
const parts = [];
|
|
87
|
+
parts.push(`Directory: ${cwd}`);
|
|
88
|
+
if (isGitRepo)
|
|
89
|
+
parts.push(`Git: yes (branch: ${branch ?? "unknown"})`);
|
|
90
|
+
if (languages.length)
|
|
91
|
+
parts.push(`Languages: ${languages.join(", ")}`);
|
|
92
|
+
if (framework)
|
|
93
|
+
parts.push(`Framework: ${framework}`);
|
|
94
|
+
if (packageManager)
|
|
95
|
+
parts.push(`Package manager: ${packageManager}`);
|
|
96
|
+
if (configFiles.length)
|
|
97
|
+
parts.push(`Config files: ${configFiles.join(", ")}`);
|
|
98
|
+
if (entryFiles.length)
|
|
99
|
+
parts.push(`Entry points: ${entryFiles.join(", ")}`);
|
|
100
|
+
// List top-level directory structure
|
|
101
|
+
try {
|
|
102
|
+
const items = readdirSync(cwd)
|
|
103
|
+
.filter((f) => !f.startsWith(".") || f === ".env.example")
|
|
104
|
+
.filter((f) => f !== "node_modules" && f !== ".git")
|
|
105
|
+
.slice(0, 30)
|
|
106
|
+
.map((f) => {
|
|
107
|
+
const stat = statSync(path.join(cwd, f));
|
|
108
|
+
return stat.isDirectory() ? `${f}/` : f;
|
|
109
|
+
});
|
|
110
|
+
parts.push(`Structure: ${items.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
return {
|
|
114
|
+
directory: cwd,
|
|
115
|
+
isGitRepo,
|
|
116
|
+
branch,
|
|
117
|
+
languages,
|
|
118
|
+
framework,
|
|
119
|
+
packageManager,
|
|
120
|
+
entryFiles,
|
|
121
|
+
configFiles,
|
|
122
|
+
summary: parts.join("\n"),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function buildCodeSystemPrompt(project, instructions) {
|
|
126
|
+
const parts = [
|
|
127
|
+
"You are an expert AI coding assistant. You help users with software engineering tasks including writing code, debugging, refactoring, and architecture decisions.",
|
|
128
|
+
"",
|
|
129
|
+
"# Principles",
|
|
130
|
+
"- Be concise and direct. Minimize output tokens while maintaining quality.",
|
|
131
|
+
"- NEVER add comments to code unless asked. Write self-documenting code.",
|
|
132
|
+
"- Follow existing code conventions in the project. Mimic style, libraries, and patterns.",
|
|
133
|
+
"- NEVER assume a library is available — check package.json/Cargo.toml/etc first.",
|
|
134
|
+
"- Prefer editing existing files over creating new ones.",
|
|
135
|
+
"- After making changes, run lint/typecheck/build if available.",
|
|
136
|
+
"- NEVER commit unless explicitly asked.",
|
|
137
|
+
"",
|
|
138
|
+
"# Workflow",
|
|
139
|
+
"- Use search tools (grep, glob) to understand the codebase before making changes.",
|
|
140
|
+
"- Use the edit tool for precise changes instead of rewriting entire files.",
|
|
141
|
+
"- When fixing bugs: read the relevant code, understand the issue, fix it, verify.",
|
|
142
|
+
"- When adding features: explore existing patterns first, then implement consistently.",
|
|
143
|
+
"- Run tests after changes when a test command is available.",
|
|
144
|
+
"",
|
|
145
|
+
"# Environment",
|
|
146
|
+
`<env>`,
|
|
147
|
+
` Working directory: ${project.directory}`,
|
|
148
|
+
` Git repo: ${project.isGitRepo ? `yes (branch: ${project.branch ?? "unknown"})` : "no"}`,
|
|
149
|
+
` Platform: ${process.platform}`,
|
|
150
|
+
` Date: ${new Date().toDateString()}`,
|
|
151
|
+
project.languages.length ? ` Languages: ${project.languages.join(", ")}` : "",
|
|
152
|
+
project.framework ? ` Framework: ${project.framework}` : "",
|
|
153
|
+
project.packageManager ? ` Package manager: ${project.packageManager}` : "",
|
|
154
|
+
`</env>`,
|
|
155
|
+
"",
|
|
156
|
+
"# Project Structure",
|
|
157
|
+
"```",
|
|
158
|
+
project.summary,
|
|
159
|
+
"```",
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
if (instructions.length > 0) {
|
|
162
|
+
parts.push("", "# User Instructions", "");
|
|
163
|
+
parts.push(...instructions);
|
|
164
|
+
}
|
|
165
|
+
return parts.join("\n");
|
|
166
|
+
}
|
package/dist/compaction.js
CHANGED
|
@@ -1,29 +1,94 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
2
|
/**
|
|
3
|
-
* Context compaction system.
|
|
3
|
+
* Context compaction system — modeled after opencode's SessionCompaction.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 4. Keep recent N turns verbatim for continuity
|
|
5
|
+
* Features:
|
|
6
|
+
* 1. Real token tracking from API responses
|
|
7
|
+
* 2. Structured summary template (Goal/Progress/Decisions/Files)
|
|
8
|
+
* 3. Incremental summaries (update previous summary instead of rewriting)
|
|
9
|
+
* 4. Tool output pruning (trim old tool results to save space)
|
|
10
|
+
* 5. Auto-continue after compaction
|
|
11
|
+
* 6. Token-budget-aware tail preservation
|
|
13
12
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
13
|
+
// ─── Structured Summary Template (from opencode) ───────────────────────────
|
|
14
|
+
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown below. Keep the section order unchanged.
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
- [single-sentence task summary]
|
|
18
|
+
|
|
19
|
+
## Constraints & Preferences
|
|
20
|
+
- [user constraints, preferences, specs, or "(none)"]
|
|
21
|
+
|
|
22
|
+
## Progress
|
|
23
|
+
### Done
|
|
24
|
+
- [completed work or "(none)"]
|
|
25
|
+
|
|
26
|
+
### In Progress
|
|
27
|
+
- [current work or "(none)"]
|
|
28
|
+
|
|
29
|
+
### Blocked
|
|
30
|
+
- [blockers or "(none)"]
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
## Key Decisions
|
|
33
|
+
- [decision and why, or "(none)"]
|
|
34
|
+
|
|
35
|
+
## Next Steps
|
|
36
|
+
- [ordered next actions or "(none)"]
|
|
37
|
+
|
|
38
|
+
## Critical Context
|
|
39
|
+
- [important technical facts, errors, open questions, or "(none)"]
|
|
40
|
+
|
|
41
|
+
## Relevant Files
|
|
42
|
+
- [file or directory path: why it matters, or "(none)"]
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- Keep every section, even when empty.
|
|
46
|
+
- Use terse bullets, not prose paragraphs.
|
|
47
|
+
- Preserve exact file paths, commands, error strings, and identifiers when known.
|
|
48
|
+
- Do not mention the summary process or that context was compacted.`;
|
|
49
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
23
50
|
const DEFAULT_MAX_TOKENS = 128000;
|
|
24
51
|
const COMPACTION_RATIO = 0.75;
|
|
25
|
-
const
|
|
26
|
-
|
|
52
|
+
const DEFAULT_TAIL_TURNS = 2;
|
|
53
|
+
const TAIL_TOKEN_BUDGET_RATIO = 0.25;
|
|
54
|
+
const MIN_TAIL_BUDGET = 2000;
|
|
55
|
+
const MAX_TAIL_BUDGET = 8000;
|
|
56
|
+
const PRUNE_PROTECT_TOKENS = 40000;
|
|
57
|
+
const PRUNE_MIN_SAVINGS = 20000;
|
|
58
|
+
const TOOL_OUTPUT_MAX_CHARS = 2000;
|
|
59
|
+
// ─── Token Tracker ─────────────────────────────────────────────────────────
|
|
60
|
+
export class TokenTracker {
|
|
61
|
+
_lastInputTokens = 0;
|
|
62
|
+
_totalOutputTokens = 0;
|
|
63
|
+
_totalInputTokens = 0;
|
|
64
|
+
_totalCacheRead = 0;
|
|
65
|
+
update(usage) {
|
|
66
|
+
const input = usage.inputTokens ?? 0;
|
|
67
|
+
const output = usage.outputTokens ?? 0;
|
|
68
|
+
const cacheRead = usage.cachedInputTokens ?? 0;
|
|
69
|
+
this._lastInputTokens = input;
|
|
70
|
+
this._totalInputTokens += input;
|
|
71
|
+
this._totalOutputTokens += output;
|
|
72
|
+
this._totalCacheRead += cacheRead;
|
|
73
|
+
}
|
|
74
|
+
get lastInputTokens() {
|
|
75
|
+
return this._lastInputTokens;
|
|
76
|
+
}
|
|
77
|
+
get totalInputTokens() {
|
|
78
|
+
return this._totalInputTokens;
|
|
79
|
+
}
|
|
80
|
+
get totalOutputTokens() {
|
|
81
|
+
return this._totalOutputTokens;
|
|
82
|
+
}
|
|
83
|
+
summary() {
|
|
84
|
+
return `context: ${this._lastInputTokens} | total in: ${this._totalInputTokens} out: ${this._totalOutputTokens}`;
|
|
85
|
+
}
|
|
86
|
+
resetContext() {
|
|
87
|
+
this._lastInputTokens = 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ─── Token Estimation ──────────────────────────────────────────────────────
|
|
91
|
+
/** Rough token estimation (fallback) */
|
|
27
92
|
export function estimateTokens(messages) {
|
|
28
93
|
let chars = 0;
|
|
29
94
|
for (const msg of messages) {
|
|
@@ -38,51 +103,180 @@ export function estimateTokens(messages) {
|
|
|
38
103
|
}
|
|
39
104
|
}
|
|
40
105
|
}
|
|
41
|
-
// Rough estimate: mix of English (~4 chars/token) and CJK (~2 chars/token)
|
|
42
106
|
return Math.ceil(chars / 3);
|
|
43
107
|
}
|
|
44
|
-
|
|
45
|
-
|
|
108
|
+
function estimateMessageTokens(msg) {
|
|
109
|
+
if (typeof msg.content === "string")
|
|
110
|
+
return Math.ceil(msg.content.length / 3);
|
|
111
|
+
if (Array.isArray(msg.content)) {
|
|
112
|
+
let chars = 0;
|
|
113
|
+
for (const part of msg.content) {
|
|
114
|
+
if ("text" in part && typeof part.text === "string")
|
|
115
|
+
chars += part.text.length;
|
|
116
|
+
}
|
|
117
|
+
return Math.ceil(chars / 3);
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
// ─── Compaction Check ──────────────────────────────────────────────────────
|
|
122
|
+
export function needsCompaction(messages, tracker, config) {
|
|
46
123
|
const maxTokens = config?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
47
124
|
const threshold = maxTokens * COMPACTION_RATIO;
|
|
125
|
+
if (tracker && tracker.lastInputTokens > 0) {
|
|
126
|
+
return tracker.lastInputTokens > threshold;
|
|
127
|
+
}
|
|
48
128
|
return estimateTokens(messages) > threshold;
|
|
49
129
|
}
|
|
50
|
-
|
|
130
|
+
// ─── Tool Output Pruning ───────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Prune old tool outputs in-place to free context space.
|
|
133
|
+
* Keeps recent tool outputs intact, trims older ones to a short summary.
|
|
134
|
+
* Returns the estimated tokens saved.
|
|
135
|
+
*/
|
|
136
|
+
export function pruneToolOutputs(messages) {
|
|
137
|
+
let totalTokens = 0;
|
|
138
|
+
let saved = 0;
|
|
139
|
+
let turns = 0;
|
|
140
|
+
// Walk backwards, skip recent 2 turns
|
|
141
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
142
|
+
const msg = messages[i];
|
|
143
|
+
if (msg.role === "user")
|
|
144
|
+
turns++;
|
|
145
|
+
if (turns < 2)
|
|
146
|
+
continue;
|
|
147
|
+
// Prune tool results in older messages
|
|
148
|
+
if (msg.role === "tool" || (Array.isArray(msg.content) && msg.content.some((p) => p.type === "tool-result"))) {
|
|
149
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
150
|
+
const estimate = Math.ceil(content.length / 3);
|
|
151
|
+
totalTokens += estimate;
|
|
152
|
+
if (totalTokens > PRUNE_PROTECT_TOKENS && content.length > TOOL_OUTPUT_MAX_CHARS) {
|
|
153
|
+
const truncated = content.slice(0, TOOL_OUTPUT_MAX_CHARS) + "\n\n[... output truncated during compaction ...]";
|
|
154
|
+
msg.content = truncated;
|
|
155
|
+
saved += estimate - Math.ceil(truncated.length / 3);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return saved;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Select how many recent turns to keep verbatim based on token budget.
|
|
163
|
+
* Similar to opencode's select() function.
|
|
164
|
+
*/
|
|
165
|
+
function selectTail(messages, config) {
|
|
166
|
+
const maxTokens = config?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
167
|
+
const tailTurns = config?.keepRecentTurns ?? DEFAULT_TAIL_TURNS;
|
|
168
|
+
const budget = Math.min(MAX_TAIL_BUDGET, Math.max(MIN_TAIL_BUDGET, Math.floor(maxTokens * TAIL_TOKEN_BUDGET_RATIO)));
|
|
169
|
+
// Find user message boundaries (turns)
|
|
170
|
+
const turnStarts = [];
|
|
171
|
+
for (let i = 0; i < messages.length; i++) {
|
|
172
|
+
if (messages[i].role === "user")
|
|
173
|
+
turnStarts.push(i);
|
|
174
|
+
}
|
|
175
|
+
if (turnStarts.length <= 1) {
|
|
176
|
+
return { headEnd: 0, tailStart: 0 };
|
|
177
|
+
}
|
|
178
|
+
// Try to keep the last N turns within budget
|
|
179
|
+
let tokensUsed = 0;
|
|
180
|
+
let tailStart = messages.length;
|
|
181
|
+
const recentTurns = turnStarts.slice(-tailTurns);
|
|
182
|
+
for (let i = recentTurns.length - 1; i >= 0; i--) {
|
|
183
|
+
const turnStart = recentTurns[i];
|
|
184
|
+
const turnEnd = i < recentTurns.length - 1 ? recentTurns[i + 1] : messages.length;
|
|
185
|
+
let turnTokens = 0;
|
|
186
|
+
for (let j = turnStart; j < turnEnd; j++) {
|
|
187
|
+
turnTokens += estimateMessageTokens(messages[j]);
|
|
188
|
+
}
|
|
189
|
+
if (tokensUsed + turnTokens > budget && tokensUsed > 0)
|
|
190
|
+
break;
|
|
191
|
+
tokensUsed += turnTokens;
|
|
192
|
+
tailStart = turnStart;
|
|
193
|
+
}
|
|
194
|
+
if (tailStart >= messages.length)
|
|
195
|
+
tailStart = messages.length - 2;
|
|
196
|
+
if (tailStart < 0)
|
|
197
|
+
tailStart = 0;
|
|
198
|
+
return { headEnd: tailStart, tailStart };
|
|
199
|
+
}
|
|
200
|
+
// ─── Compaction Agent Prompt ────────────────────────────────────────────────
|
|
201
|
+
const COMPACTION_AGENT_SYSTEM = `You are an anchored context summarization assistant for coding sessions.
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
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.
|
|
206
|
+
|
|
207
|
+
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
|
|
208
|
+
|
|
209
|
+
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.`;
|
|
210
|
+
/** Previous summary stored in the first system message if present */
|
|
211
|
+
function extractPreviousSummary(messages) {
|
|
212
|
+
const first = messages[0];
|
|
213
|
+
if (first?.role === "system" && typeof first.content === "string" && first.content.includes("[Context Summary")) {
|
|
214
|
+
// Extract just the summary content after the header
|
|
215
|
+
const match = first.content.match(/\[Context Summary[^\]]*\]\n\n([\s\S]*)/);
|
|
216
|
+
return match?.[1];
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
function buildCompactionPrompt(previousSummary) {
|
|
221
|
+
const anchor = previousSummary
|
|
222
|
+
? [
|
|
223
|
+
"Update the anchored summary below using the conversation history above.",
|
|
224
|
+
"Preserve still-true details, remove stale details, and merge in the new facts.",
|
|
225
|
+
"",
|
|
226
|
+
"<previous-summary>",
|
|
227
|
+
previousSummary,
|
|
228
|
+
"</previous-summary>",
|
|
229
|
+
].join("\n")
|
|
230
|
+
: "Create a new anchored summary from the conversation history above.";
|
|
231
|
+
return [anchor, "", SUMMARY_TEMPLATE].join("\n");
|
|
232
|
+
}
|
|
233
|
+
function messageToText(msg) {
|
|
234
|
+
if (typeof msg.content === "string")
|
|
235
|
+
return msg.content;
|
|
236
|
+
if (Array.isArray(msg.content)) {
|
|
237
|
+
return msg.content
|
|
238
|
+
.filter((p) => "text" in p && typeof p.text === "string")
|
|
239
|
+
.map((p) => p.text)
|
|
240
|
+
.join("\n");
|
|
241
|
+
}
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Compact messages by summarizing older history with structured template.
|
|
246
|
+
* Supports incremental summaries and token-budget tail preservation.
|
|
247
|
+
*/
|
|
51
248
|
export async function compactMessages(messages, model, config) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const toSummarize = messages.slice(0,
|
|
60
|
-
const toKeep = messages.slice(
|
|
61
|
-
//
|
|
249
|
+
// Step 1: Prune old tool outputs first
|
|
250
|
+
pruneToolOutputs(messages);
|
|
251
|
+
// Step 2: Select tail (recent turns to keep verbatim)
|
|
252
|
+
const { headEnd, tailStart } = selectTail(messages, config);
|
|
253
|
+
if (headEnd <= 1) {
|
|
254
|
+
return { messages, compacted: false, shouldContinue: false };
|
|
255
|
+
}
|
|
256
|
+
const toSummarize = messages.slice(0, headEnd);
|
|
257
|
+
const toKeep = messages.slice(tailStart);
|
|
258
|
+
// Step 3: Check for previous summary (incremental)
|
|
259
|
+
const previousSummary = extractPreviousSummary(toSummarize);
|
|
260
|
+
// Step 4: Build conversation text for summarization
|
|
62
261
|
const conversationText = toSummarize
|
|
63
262
|
.map((msg) => {
|
|
64
263
|
const role = msg.role;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
? msg.content
|
|
69
|
-
.filter((p) => "text" in p)
|
|
70
|
-
.map((p) => p.text)
|
|
71
|
-
.join("\n")
|
|
72
|
-
: "";
|
|
73
|
-
return `[${role}]: ${content.slice(0, 2000)}`;
|
|
264
|
+
const text = messageToText(msg);
|
|
265
|
+
// Limit each message to avoid overwhelming the summarizer
|
|
266
|
+
return `[${role}]: ${text.slice(0, 3000)}`;
|
|
74
267
|
})
|
|
75
268
|
.join("\n\n");
|
|
269
|
+
// Step 5: Generate structured summary using dedicated compaction agent
|
|
76
270
|
try {
|
|
271
|
+
const prompt = buildCompactionPrompt(previousSummary);
|
|
77
272
|
const result = await generateText({
|
|
78
273
|
model,
|
|
274
|
+
system: COMPACTION_AGENT_SYSTEM,
|
|
79
275
|
messages: [
|
|
80
|
-
{ role: "
|
|
81
|
-
{ role: "user", content: `Summarize this conversation:\n\n${conversationText}` },
|
|
276
|
+
{ role: "user", content: conversationText + "\n\n" + prompt },
|
|
82
277
|
],
|
|
83
278
|
});
|
|
84
279
|
const summary = result.text;
|
|
85
|
-
// Build compacted message list
|
|
86
280
|
const compactedMessages = [
|
|
87
281
|
{
|
|
88
282
|
role: "system",
|
|
@@ -90,10 +284,11 @@ export async function compactMessages(messages, model, config) {
|
|
|
90
284
|
},
|
|
91
285
|
...toKeep,
|
|
92
286
|
];
|
|
93
|
-
|
|
287
|
+
const shouldContinue = config?.autoContinue !== false;
|
|
288
|
+
return { messages: compactedMessages, compacted: true, shouldContinue };
|
|
94
289
|
}
|
|
95
290
|
catch {
|
|
96
|
-
//
|
|
97
|
-
return { messages: toKeep, compacted: true };
|
|
291
|
+
// Fallback: just keep the tail
|
|
292
|
+
return { messages: toKeep, compacted: true, shouldContinue: false };
|
|
98
293
|
}
|
|
99
294
|
}
|
package/dist/config.js
CHANGED
|
@@ -56,20 +56,47 @@ function ask(rl, question, defaultValue) {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
const MODELS_CACHE_FILE = path.join(CONFIG_DIR, "models-cache.json");
|
|
60
|
+
function loadModelsCache() {
|
|
61
|
+
if (!existsSync(MODELS_CACHE_FILE))
|
|
62
|
+
return [];
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(readFileSync(MODELS_CACHE_FILE, "utf-8"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function saveModelsCache(models) {
|
|
71
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
72
|
+
writeFileSync(MODELS_CACHE_FILE, JSON.stringify(models), "utf-8");
|
|
73
|
+
}
|
|
59
74
|
export async function fetchModels(baseURL, apiKey) {
|
|
60
75
|
try {
|
|
61
76
|
const trimmed = baseURL.replace(/\/$/, "");
|
|
62
|
-
|
|
63
|
-
if (primary.length > 0)
|
|
64
|
-
return primary;
|
|
77
|
+
let models = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
|
|
65
78
|
// Ollama users often provide host without /v1; auto-retry that variant.
|
|
66
|
-
if (!trimmed.endsWith("/v1")) {
|
|
67
|
-
|
|
79
|
+
if (models.length === 0 && !trimmed.endsWith("/v1")) {
|
|
80
|
+
models = await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
|
|
68
81
|
}
|
|
69
|
-
|
|
82
|
+
if (models.length > 0) {
|
|
83
|
+
saveModelsCache(models);
|
|
84
|
+
return models;
|
|
85
|
+
}
|
|
86
|
+
// Fallback to cache if live fetch returned nothing
|
|
87
|
+
const cached = loadModelsCache();
|
|
88
|
+
if (cached.length > 0) {
|
|
89
|
+
console.log("\x1b[90m (using cached model list)\x1b[0m");
|
|
90
|
+
}
|
|
91
|
+
return cached;
|
|
70
92
|
}
|
|
71
93
|
catch {
|
|
72
|
-
|
|
94
|
+
// Network error — fallback to cache
|
|
95
|
+
const cached = loadModelsCache();
|
|
96
|
+
if (cached.length > 0) {
|
|
97
|
+
console.log("\x1b[90m (using cached model list — network unavailable)\x1b[0m");
|
|
98
|
+
}
|
|
99
|
+
return cached;
|
|
73
100
|
}
|
|
74
101
|
}
|
|
75
102
|
export async function runSetup() {
|