@sean.holung/minicode 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/src/agent/config.js +14 -2
- package/dist/src/index.js +16 -2
- package/dist/src/indexer/code-map.js +52 -5
- package/dist/src/indexer/focus-tracker.js +63 -0
- package/dist/src/indexer/project-index.js +2 -2
- package/dist/src/ui/cli-ink.js +22 -2
- package/dist/tests/agent.test.js +62 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +30 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +212 -8
- package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +10 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts +51 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +210 -2
- package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js +75 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight CLI coding agent optimized for **local models** by providing AST-based intelligent context for smaller models running on consumer hardware.
|
|
4
4
|
|
|
5
|
-
> minicode gives local models a dependency-aware map of your codebase, so agents read less, reason better, and ship changes faster.
|
|
6
|
-
|
|
7
5
|
Read operations dominate token usage in typical agent sessions; minicode addresses this by optimizing for **specific languages** — indexing your project at startup with language plugins (TypeScript/JavaScript built-in) and injecting a compact **code map** (signatures only) into the system prompt, plus symbol-level tools (`read_symbol`, `find_references`, `get_dependencies`) so the model reads only what it needs instead of entire files. This keeps prompts lean enough for smaller models in the 20B range, with faster inference and better attention over the relevant code.
|
|
8
6
|
|
|
9
7
|
## Quick Start (LM Studio)
|
|
@@ -23,7 +21,7 @@ OPENAI_BASE_URL=http://localhost:1234/v1
|
|
|
23
21
|
OPENAI_API_KEY=
|
|
24
22
|
MAX_STEPS=50
|
|
25
23
|
MAX_TOKENS=4096
|
|
26
|
-
MAX_CONTEXT_TOKENS=
|
|
24
|
+
MAX_CONTEXT_TOKENS=24000
|
|
27
25
|
WORKSPACE_ROOT=.
|
|
28
26
|
COMMAND_TIMEOUT_MS=30000
|
|
29
27
|
MAX_FILE_SIZE_BYTES=1000000
|
|
@@ -194,6 +192,8 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
|
|
|
194
192
|
| `CONFIRM_DESTRUCTIVE` | No | `true` | If `true`, blocks destructive shell commands unless confirmed |
|
|
195
193
|
| `KEEP_RECENT_MESSAGES` | No | `12` | Minimum number of latest messages kept during trimming |
|
|
196
194
|
| `LOOP_DETECTION_WINDOW` | No | `6` | Window for repeated tool-call loop detection |
|
|
195
|
+
| `COMPACTION_THRESHOLD` | No | `0.8` | Context fullness ratio (0–1) at which auto-compaction triggers |
|
|
196
|
+
| `COMPACTION_MODEL` | No | none | Model for LLM-based compaction summaries. When set, `/compact` and auto-compaction use this model instead of mechanical truncation. Use a small, fast model (e.g. your local model). |
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
### `agent.config.json`
|
|
@@ -215,7 +215,8 @@ Create `agent.config.json` in `~/.minicode/` for user-level defaults, or in the
|
|
|
215
215
|
"keepRecentMessages": 12,
|
|
216
216
|
"loopDetectionWindow": 6,
|
|
217
217
|
"openAiBaseUrl": "http://localhost:1234/v1",
|
|
218
|
-
"openAiApiKey": ""
|
|
218
|
+
"openAiApiKey": "",
|
|
219
|
+
"compactionModel": ""
|
|
219
220
|
}
|
|
220
221
|
```
|
|
221
222
|
|
|
@@ -235,6 +236,8 @@ Field mapping:
|
|
|
235
236
|
- `loopDetectionWindow` ↔ `LOOP_DETECTION_WINDOW`
|
|
236
237
|
- `openAiBaseUrl` ↔ `OPENAI_BASE_URL`
|
|
237
238
|
- `openAiApiKey` ↔ `OPENAI_API_KEY`
|
|
239
|
+
- `compactionThreshold` ↔ `COMPACTION_THRESHOLD`
|
|
240
|
+
- `compactionModel` ↔ `COMPACTION_MODEL`
|
|
238
241
|
|
|
239
242
|
## Usage
|
|
240
243
|
|
package/dist/src/agent/config.js
CHANGED
|
@@ -27,6 +27,11 @@ export function formatConfigForDisplay(config) {
|
|
|
27
27
|
"commandDenylist: " + config.commandDenylist.length + " patterns",
|
|
28
28
|
"openAiBaseUrl: " + config.openAiBaseUrl,
|
|
29
29
|
"openAiApiKey: " + (config.openAiApiKey ? "***" : "(unset)"),
|
|
30
|
+
"enableFileReadDedup: " + (config.enableFileReadDedup ?? false),
|
|
31
|
+
"enableAdaptiveKeepRecent: " + (config.enableAdaptiveKeepRecent ?? false),
|
|
32
|
+
"enableToolOutputTruncation: " + (config.enableToolOutputTruncation ?? false),
|
|
33
|
+
"compactionThreshold: " + (config.compactionThreshold ?? "(disabled)"),
|
|
34
|
+
"compactionModel: " + (config.compactionModel ?? "(disabled — using mechanical compaction)"),
|
|
30
35
|
];
|
|
31
36
|
return lines.join("\n");
|
|
32
37
|
}
|
|
@@ -136,7 +141,7 @@ export async function loadAgentConfig(cwd = process.cwd()) {
|
|
|
136
141
|
"zai-org/glm-4.7-flash",
|
|
137
142
|
maxSteps: parseNumber(process.env.MAX_STEPS, fileConfig.maxSteps ?? 50),
|
|
138
143
|
maxTokens: parseNumber(process.env.MAX_TOKENS, fileConfig.maxTokens ?? 4096),
|
|
139
|
-
maxContextTokens: parseNumber(process.env.MAX_CONTEXT_TOKENS, fileConfig.maxContextTokens ??
|
|
144
|
+
maxContextTokens: parseNumber(process.env.MAX_CONTEXT_TOKENS, fileConfig.maxContextTokens ?? 40_000),
|
|
140
145
|
workspaceRoot,
|
|
141
146
|
commandTimeoutMs: parseNumber(process.env.COMMAND_TIMEOUT_MS, fileConfig.commandTimeout ?? 30_000),
|
|
142
147
|
maxFileSizeBytes: parseNumber(process.env.MAX_FILE_SIZE_BYTES, fileConfig.maxFileSizeBytes ?? 1_000_000),
|
|
@@ -144,8 +149,15 @@ export async function loadAgentConfig(cwd = process.cwd()) {
|
|
|
144
149
|
confirmDestructive: parseBoolean(process.env.CONFIRM_DESTRUCTIVE, fileConfig.confirmDestructive ?? true),
|
|
145
150
|
keepRecentMessages: parseNumber(process.env.KEEP_RECENT_MESSAGES, fileConfig.keepRecentMessages ?? 12),
|
|
146
151
|
loopDetectionWindow: parseNumber(process.env.LOOP_DETECTION_WINDOW, fileConfig.loopDetectionWindow ?? 6),
|
|
147
|
-
maxToolOutputChars: parseNumber(process.env.MAX_TOOL_OUTPUT_CHARS, fileConfig.maxToolOutputChars ??
|
|
152
|
+
maxToolOutputChars: parseNumber(process.env.MAX_TOOL_OUTPUT_CHARS, fileConfig.maxToolOutputChars ?? 8_000),
|
|
148
153
|
openAiBaseUrl: rawBaseUrl,
|
|
149
154
|
...(openAiApiKey !== undefined ? { openAiApiKey } : {}),
|
|
155
|
+
enableFileReadDedup: parseBoolean(process.env.ENABLE_FILE_READ_DEDUP, fileConfig.enableFileReadDedup ?? true),
|
|
156
|
+
enableAdaptiveKeepRecent: parseBoolean(process.env.ENABLE_ADAPTIVE_KEEP_RECENT, fileConfig.enableAdaptiveKeepRecent ?? true),
|
|
157
|
+
enableToolOutputTruncation: parseBoolean(process.env.ENABLE_TOOL_OUTPUT_TRUNCATION, fileConfig.enableToolOutputTruncation ?? true),
|
|
158
|
+
compactionThreshold: parseNumber(process.env.COMPACTION_THRESHOLD, fileConfig.compactionThreshold ?? 0.8),
|
|
159
|
+
...(process.env.COMPACTION_MODEL ?? fileConfig.compactionModel
|
|
160
|
+
? { compactionModel: process.env.COMPACTION_MODEL ?? fileConfig.compactionModel }
|
|
161
|
+
: {}),
|
|
150
162
|
};
|
|
151
163
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -44,7 +44,7 @@ async function createAgentRuntime(verbose, onProgress) {
|
|
|
44
44
|
verbose,
|
|
45
45
|
...(session ? { session } : {}),
|
|
46
46
|
...(projectIndex !== undefined
|
|
47
|
-
? { getCodeMap: () => projectIndex.getCodeMap() }
|
|
47
|
+
? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
|
|
48
48
|
: {}),
|
|
49
49
|
...(onProgress ? { onProgress } : {}),
|
|
50
50
|
});
|
|
@@ -98,7 +98,7 @@ async function runInteractive(verbose, initialTask) {
|
|
|
98
98
|
break;
|
|
99
99
|
}
|
|
100
100
|
if (trimmed === "/help") {
|
|
101
|
-
console.log('Commands: "/help", "/config", "/save [label]", "/load [label]", "/sessions", "/exit"');
|
|
101
|
+
console.log('Commands: "/help", "/config", "/compact", "/save [label]", "/load [label]", "/sessions", "/exit"');
|
|
102
102
|
console.log("Start with --verbose or -v to log prompts, responses, and tool calls.");
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
@@ -106,6 +106,20 @@ async function runInteractive(verbose, initialTask) {
|
|
|
106
106
|
console.log("\n" + formatConfigForDisplay(config) + "\n");
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
+
if (trimmed === "/compact") {
|
|
110
|
+
const session = agent.getSession();
|
|
111
|
+
const tokensBefore = session.getTokenEstimate();
|
|
112
|
+
const result = session.compact(config.keepRecentMessages);
|
|
113
|
+
if (result) {
|
|
114
|
+
console.log(`Compacted: ${result.removedMessages} messages summarized, ` +
|
|
115
|
+
`${result.previousTokens} → ${result.newTokens} tokens ` +
|
|
116
|
+
`(saved ${result.previousTokens - result.newTokens} tokens)`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(`Nothing to compact (${tokensBefore} tokens, ${session.getMessages().length} messages).`);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
109
123
|
if (trimmed === "/save" || trimmed.startsWith("/save ")) {
|
|
110
124
|
const label = trimmed.slice("/save".length).trim() || undefined;
|
|
111
125
|
try {
|
|
@@ -12,12 +12,42 @@ function formatSymbol(symbol, indent, isMethod) {
|
|
|
12
12
|
function isEntryPointFile(filePath) {
|
|
13
13
|
return filePath === "src/index.ts" || filePath.endsWith("/index.ts");
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Build the set of symbols related to focus symbols via dependency edges.
|
|
17
|
+
* Expands 1 hop outbound (what focus symbols depend on) and 1 hop inbound
|
|
18
|
+
* (what depends on focus symbols).
|
|
19
|
+
*/
|
|
20
|
+
function expandFocusSet(focusSymbols, edges) {
|
|
21
|
+
const expanded = new Set(focusSymbols);
|
|
22
|
+
for (const edge of edges) {
|
|
23
|
+
// Outbound: focus symbol depends on something
|
|
24
|
+
if (focusSymbols.has(edge.from)) {
|
|
25
|
+
expanded.add(edge.to);
|
|
26
|
+
}
|
|
27
|
+
// Inbound: something depends on focus symbol
|
|
28
|
+
if (focusSymbols.has(edge.to)) {
|
|
29
|
+
expanded.add(edge.from);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return expanded;
|
|
33
|
+
}
|
|
34
|
+
function createSymbolRanker(edges, focusSymbols) {
|
|
16
35
|
const refCount = new Map();
|
|
17
36
|
for (const e of edges) {
|
|
18
37
|
refCount.set(e.to, (refCount.get(e.to) ?? 0) + 1);
|
|
19
38
|
}
|
|
39
|
+
// Expand focus set to include 1-hop neighbors in the dependency graph
|
|
40
|
+
const boosted = focusSymbols?.size
|
|
41
|
+
? expandFocusSet(focusSymbols, edges)
|
|
42
|
+
: undefined;
|
|
20
43
|
return (a, b) => {
|
|
44
|
+
// Focus-boosted symbols always sort first
|
|
45
|
+
if (boosted) {
|
|
46
|
+
const aFocused = boosted.has(a.qualifiedName);
|
|
47
|
+
const bFocused = boosted.has(b.qualifiedName);
|
|
48
|
+
if (aFocused !== bFocused)
|
|
49
|
+
return aFocused ? -1 : 1;
|
|
50
|
+
}
|
|
21
51
|
if (a.exported !== b.exported)
|
|
22
52
|
return a.exported ? -1 : 1;
|
|
23
53
|
const refA = refCount.get(a.qualifiedName) ?? 0;
|
|
@@ -31,20 +61,37 @@ function createSymbolRanker(edges) {
|
|
|
31
61
|
}
|
|
32
62
|
/**
|
|
33
63
|
* Generate a compact code map from symbols grouped by file.
|
|
34
|
-
* Ranks symbols by: exported > high reference count > entry points.
|
|
64
|
+
* Ranks symbols by: focus-boosted > exported > high reference count > entry points.
|
|
35
65
|
* When over budget, truncates with a footer.
|
|
66
|
+
*
|
|
67
|
+
* @param focusSymbols Optional set of symbol qualified names to boost to the top.
|
|
68
|
+
* These symbols (and their 1-hop dependency neighbors) will be ranked above all
|
|
69
|
+
* others, ensuring they survive truncation within the token budget.
|
|
36
70
|
*/
|
|
37
|
-
export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGET, dependencyEdges) {
|
|
71
|
+
export function generateCodeMap(symbolsByFile, tokenBudget = DEFAULT_TOKEN_BUDGET, dependencyEdges, focusSymbols) {
|
|
38
72
|
const totalCount = [...symbolsByFile.values()].reduce((sum, syms) => sum + syms.length, 0);
|
|
39
73
|
const lines = ["# Project Code Map", ""];
|
|
40
74
|
const rank = dependencyEdges
|
|
41
|
-
? createSymbolRanker(dependencyEdges)
|
|
75
|
+
? createSymbolRanker(dependencyEdges, focusSymbols)
|
|
42
76
|
: (a, b) => (a.exported === b.exported ? 0 : a.exported ? -1 : 1);
|
|
43
77
|
let totalTokens = estimateTokens(lines.join("\n"));
|
|
44
78
|
let truncatedSymbols = 0;
|
|
45
79
|
let shownCount = 0;
|
|
46
80
|
const filesWithTruncation = new Set();
|
|
47
|
-
|
|
81
|
+
// When we have focus symbols, sort files so that files containing
|
|
82
|
+
// focused symbols come first in the code map.
|
|
83
|
+
const boosted = focusSymbols?.size && dependencyEdges
|
|
84
|
+
? expandFocusSet(focusSymbols, dependencyEdges)
|
|
85
|
+
: undefined;
|
|
86
|
+
const sortedFiles = [...symbolsByFile.keys()].sort((a, b) => {
|
|
87
|
+
if (boosted) {
|
|
88
|
+
const aHasFocus = symbolsByFile.get(a)?.some((s) => boosted.has(s.qualifiedName)) ?? false;
|
|
89
|
+
const bHasFocus = symbolsByFile.get(b)?.some((s) => boosted.has(s.qualifiedName)) ?? false;
|
|
90
|
+
if (aHasFocus !== bHasFocus)
|
|
91
|
+
return aHasFocus ? -1 : 1;
|
|
92
|
+
}
|
|
93
|
+
return a.localeCompare(b);
|
|
94
|
+
});
|
|
48
95
|
for (const filePath of sortedFiles) {
|
|
49
96
|
const symbols = symbolsByFile.get(filePath);
|
|
50
97
|
if (!symbols?.length)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks which symbols the user/agent is actively working with.
|
|
3
|
+
* Used to dynamically re-rank the code map so that relevant symbols
|
|
4
|
+
* survive truncation within the fixed token budget.
|
|
5
|
+
*
|
|
6
|
+
* Focus is derived from:
|
|
7
|
+
* - Symbol names in tool calls (read_symbol, find_references, get_dependencies)
|
|
8
|
+
* - Symbol names mentioned in user messages (fuzzy match against index)
|
|
9
|
+
*/
|
|
10
|
+
const MAX_FOCUS_SYMBOLS = 30;
|
|
11
|
+
export class FocusTracker {
|
|
12
|
+
focused = new Map();
|
|
13
|
+
generation = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Record a symbol as being actively focused on.
|
|
16
|
+
* More recent additions have higher priority.
|
|
17
|
+
*/
|
|
18
|
+
addSymbol(qualifiedName) {
|
|
19
|
+
this.generation += 1;
|
|
20
|
+
this.focused.set(qualifiedName, this.generation);
|
|
21
|
+
// Evict oldest entries if we exceed the limit
|
|
22
|
+
if (this.focused.size > MAX_FOCUS_SYMBOLS) {
|
|
23
|
+
let oldestKey = null;
|
|
24
|
+
let oldestGen = Infinity;
|
|
25
|
+
for (const [key, gen] of this.focused) {
|
|
26
|
+
if (gen < oldestGen) {
|
|
27
|
+
oldestGen = gen;
|
|
28
|
+
oldestKey = key;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (oldestKey) {
|
|
32
|
+
this.focused.delete(oldestKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Record multiple symbols at once (e.g. from dependency expansion).
|
|
38
|
+
*/
|
|
39
|
+
addSymbols(qualifiedNames) {
|
|
40
|
+
for (const name of qualifiedNames) {
|
|
41
|
+
this.addSymbol(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the current set of focused symbol qualified names.
|
|
46
|
+
*/
|
|
47
|
+
getFocusedSymbols() {
|
|
48
|
+
return new Set(this.focused.keys());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a symbol is currently focused.
|
|
52
|
+
*/
|
|
53
|
+
hasFocus(qualifiedName) {
|
|
54
|
+
return this.focused.has(qualifiedName);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clear all focus tracking.
|
|
58
|
+
*/
|
|
59
|
+
clear() {
|
|
60
|
+
this.focused.clear();
|
|
61
|
+
this.generation = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -66,8 +66,8 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
|
|
|
66
66
|
}
|
|
67
67
|
return [...result.values()];
|
|
68
68
|
},
|
|
69
|
-
getCodeMap(tokenBudget) {
|
|
70
|
-
return generateCodeMap(files, tokenBudget, dependencyEdges);
|
|
69
|
+
getCodeMap(tokenBudget, focusSymbols) {
|
|
70
|
+
return generateCodeMap(files, tokenBudget, dependencyEdges, focusSymbols);
|
|
71
71
|
},
|
|
72
72
|
reindexFile(filePath, content) {
|
|
73
73
|
const relPath = path.isAbsolute(filePath)
|
package/dist/src/ui/cli-ink.js
CHANGED
|
@@ -85,7 +85,7 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
85
85
|
verbose,
|
|
86
86
|
...(session ? { session } : {}),
|
|
87
87
|
...(projectIndex !== undefined
|
|
88
|
-
? { getCodeMap: () => projectIndex.getCodeMap() }
|
|
88
|
+
? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
|
|
89
89
|
: {}),
|
|
90
90
|
...(verbose
|
|
91
91
|
? {
|
|
@@ -121,7 +121,7 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
121
121
|
if (trimmed === "/help") {
|
|
122
122
|
store.addItem({
|
|
123
123
|
type: "system",
|
|
124
|
-
content: 'Commands: "/help", "/config", "/save [label]", "/load [label]", "/sessions", "/exit".',
|
|
124
|
+
content: 'Commands: "/help", "/config", "/compact", "/save [label]", "/load [label]", "/sessions", "/exit".',
|
|
125
125
|
});
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
@@ -132,6 +132,26 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
132
132
|
});
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
|
+
if (trimmed === "/compact") {
|
|
136
|
+
const session = agent.getSession();
|
|
137
|
+
const result = await agent.compactContext();
|
|
138
|
+
if (result) {
|
|
139
|
+
const method = config.compactionModel ? "LLM" : "mechanical";
|
|
140
|
+
store.addItem({
|
|
141
|
+
type: "system",
|
|
142
|
+
content: `Compacted (${method}): ${result.removedMessages} messages summarized, ` +
|
|
143
|
+
`${result.previousTokens} → ${result.newTokens} tokens ` +
|
|
144
|
+
`(saved ${result.previousTokens - result.newTokens} tokens)`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
store.addItem({
|
|
149
|
+
type: "system",
|
|
150
|
+
content: `Nothing to compact (${session.getTokenEstimate()} tokens, ${session.getMessages().length} messages).`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
135
155
|
if (trimmed === "/save" || trimmed.startsWith("/save ")) {
|
|
136
156
|
const label = trimmed.slice("/save".length).trim() || undefined;
|
|
137
157
|
try {
|
package/dist/tests/agent.test.js
CHANGED
|
@@ -102,6 +102,68 @@ test("agent omits code map when projectIndex is not provided", async () => {
|
|
|
102
102
|
await agent.runTurn("Hello");
|
|
103
103
|
assert.ok(!capturedSystem.includes("[Project Code Map]"));
|
|
104
104
|
});
|
|
105
|
+
test("agent caps thinking text in session but preserves final response", async () => {
|
|
106
|
+
const longThinking = "I need to analyze this carefully. ".repeat(20); // ~660 chars, well over 200
|
|
107
|
+
const finalResponse = "Here is the complete and detailed answer that should not be truncated at all. ".repeat(10); // ~780 chars
|
|
108
|
+
const responses = [
|
|
109
|
+
{
|
|
110
|
+
text: longThinking,
|
|
111
|
+
toolCalls: [{ id: "tool-1", name: "echo_tool", input: { value: "ok" } }],
|
|
112
|
+
stopReason: "tool_use",
|
|
113
|
+
usage: { inputTokens: 10, outputTokens: 8 },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
text: finalResponse,
|
|
117
|
+
toolCalls: [],
|
|
118
|
+
stopReason: "end_turn",
|
|
119
|
+
usage: { inputTokens: 12, outputTokens: 6 },
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
const agent = new CodingAgent({
|
|
123
|
+
config: createTestAgentConfig("/tmp"),
|
|
124
|
+
modelClient: new SequenceModelClient(responses),
|
|
125
|
+
toolRegistry: new ToolRegistry([createEchoTool()]),
|
|
126
|
+
});
|
|
127
|
+
const { text } = await agent.runTurn("Do something complex");
|
|
128
|
+
const messages = agent.getSession().getMessages();
|
|
129
|
+
// Thinking message (assistant with toolCalls) should be capped at ~200 chars + "..."
|
|
130
|
+
const thinkingMsg = messages[1];
|
|
131
|
+
assert.equal(thinkingMsg?.role, "assistant");
|
|
132
|
+
assert.ok(thinkingMsg?.role === "assistant" && thinkingMsg.content.length <= 204, `Thinking should be capped but was ${thinkingMsg?.role === "assistant" ? thinkingMsg.content.length : "?"} chars`);
|
|
133
|
+
assert.ok(thinkingMsg?.role === "assistant" && thinkingMsg.content.endsWith("..."), "Capped thinking should end with ellipsis");
|
|
134
|
+
// Final response (assistant without toolCalls) should be preserved in full
|
|
135
|
+
const finalMsg = messages[3];
|
|
136
|
+
assert.equal(finalMsg?.role, "assistant");
|
|
137
|
+
assert.equal(text, finalResponse);
|
|
138
|
+
assert.ok(finalMsg?.role === "assistant" && finalMsg.content === finalResponse, "Final response should not be truncated");
|
|
139
|
+
});
|
|
140
|
+
test("agent preserves short thinking text without capping", async () => {
|
|
141
|
+
const shortThinking = "Let me check.";
|
|
142
|
+
const responses = [
|
|
143
|
+
{
|
|
144
|
+
text: shortThinking,
|
|
145
|
+
toolCalls: [{ id: "tool-1", name: "echo_tool", input: { value: "ok" } }],
|
|
146
|
+
stopReason: "tool_use",
|
|
147
|
+
usage: { inputTokens: 10, outputTokens: 8 },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
text: "Done.",
|
|
151
|
+
toolCalls: [],
|
|
152
|
+
stopReason: "end_turn",
|
|
153
|
+
usage: { inputTokens: 12, outputTokens: 6 },
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const agent = new CodingAgent({
|
|
157
|
+
config: createTestAgentConfig("/tmp"),
|
|
158
|
+
modelClient: new SequenceModelClient(responses),
|
|
159
|
+
toolRegistry: new ToolRegistry([createEchoTool()]),
|
|
160
|
+
});
|
|
161
|
+
await agent.runTurn("Quick task");
|
|
162
|
+
const messages = agent.getSession().getMessages();
|
|
163
|
+
const thinkingMsg = messages[1];
|
|
164
|
+
assert.equal(thinkingMsg?.role, "assistant");
|
|
165
|
+
assert.ok(thinkingMsg?.role === "assistant" && thinkingMsg.content === shortThinking, "Short thinking should be preserved verbatim");
|
|
166
|
+
});
|
|
105
167
|
test("agent includes code map in system prompt when projectIndex is provided", async () => {
|
|
106
168
|
const root = path.resolve(import.meta.dirname, "..");
|
|
107
169
|
const projectIndex = await buildProjectIndex(root);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CodeMapResult } from "../prompt/system-prompt.js";
|
|
2
2
|
import { Session } from "../session/session.js";
|
|
3
|
+
import type { CompactionResult } from "../session/session.js";
|
|
3
4
|
import { ToolRegistry } from "../tools/registry.js";
|
|
4
5
|
import type { AgentConfig, ModelClient } from "./types.js";
|
|
5
6
|
export type UiUpdateThinking = {
|
|
@@ -36,17 +37,45 @@ export declare class CodingAgent {
|
|
|
36
37
|
private readonly verbose;
|
|
37
38
|
private readonly onProgress;
|
|
38
39
|
private readonly onUiUpdate;
|
|
40
|
+
/**
|
|
41
|
+
* Tracks symbol names the user/agent has been working with.
|
|
42
|
+
* Persists across turns so the code map stays focused on the
|
|
43
|
+
* current area of interest.
|
|
44
|
+
*/
|
|
45
|
+
private readonly focusedSymbols;
|
|
46
|
+
private focusGeneration;
|
|
47
|
+
/**
|
|
48
|
+
* Cache of recently read file paths (key: "path:offset:limit") to avoid
|
|
49
|
+
* sending duplicate full file contents through the context window.
|
|
50
|
+
* Maps to the step number when the file was last read.
|
|
51
|
+
*/
|
|
52
|
+
private readonly fileReadCache;
|
|
39
53
|
constructor(params: {
|
|
40
54
|
config: AgentConfig;
|
|
41
55
|
modelClient: ModelClient;
|
|
42
56
|
toolRegistry: ToolRegistry;
|
|
43
57
|
session?: Session;
|
|
44
|
-
getCodeMap?: () => CodeMapResult | undefined;
|
|
58
|
+
getCodeMap?: (focusSymbols?: Set<string>) => CodeMapResult | undefined;
|
|
45
59
|
verbose?: boolean;
|
|
46
60
|
onProgress?: (message: string) => void;
|
|
47
61
|
onUiUpdate?: (event: UiUpdate) => void;
|
|
48
62
|
});
|
|
49
63
|
getSession(): Session;
|
|
64
|
+
/**
|
|
65
|
+
* Manually compact the conversation context.
|
|
66
|
+
* Uses LLM-based summarization when compactionModel is configured,
|
|
67
|
+
* otherwise falls back to mechanical compaction.
|
|
68
|
+
*/
|
|
69
|
+
compactContext(): Promise<CompactionResult | null>;
|
|
70
|
+
private addFocusSymbol;
|
|
71
|
+
private getFocusSet;
|
|
72
|
+
/**
|
|
73
|
+
* Check whether a previously-read file's content is still present in the
|
|
74
|
+
* session context (i.e. hasn't been trimmed/compacted away). We look for
|
|
75
|
+
* a tool message from "read_file" whose content still contains the file
|
|
76
|
+
* path and hasn't been replaced with a summary stub.
|
|
77
|
+
*/
|
|
78
|
+
private isFileReadStillInContext;
|
|
50
79
|
runTurn(userMessage: string, options?: {
|
|
51
80
|
signal?: AbortSignal;
|
|
52
81
|
}): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAY,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}
|