@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.
Files changed (25) hide show
  1. package/README.md +7 -4
  2. package/dist/src/agent/config.js +14 -2
  3. package/dist/src/index.js +16 -2
  4. package/dist/src/indexer/code-map.js +52 -5
  5. package/dist/src/indexer/focus-tracker.js +63 -0
  6. package/dist/src/indexer/project-index.js +2 -2
  7. package/dist/src/ui/cli-ink.js +22 -2
  8. package/dist/tests/agent.test.js +62 -0
  9. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +30 -1
  10. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  11. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +212 -8
  12. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  13. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +10 -0
  14. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  15. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +1 -1
  16. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  17. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  18. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts +51 -1
  19. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
  20. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +210 -2
  21. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
  22. package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js +75 -0
  23. package/node_modules/@minicode/agent-sdk/dist/tests/session.test.js.map +1 -1
  24. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  25. 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=60000
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
 
@@ -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 ?? 120_000),
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 ?? 15_000),
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
- function createSymbolRanker(edges) {
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
- const sortedFiles = [...symbolsByFile.keys()].sort();
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)
@@ -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 {
@@ -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;AA+BrE,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,sBAAsB,GAAG;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAClF,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAC1D,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AACF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AACF,MAAM,MAAM,QAAQ,GAChB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,qBAAqB,GACrB,mBAAmB,CAAC;AAExB,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgD;IAC3E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;gBAEzD,MAAM,EAAE;QAClB,MAAM,EAAE,WAAW,CAAC;QACpB,WAAW,EAAE,WAAW,CAAC;QACzB,YAAY,EAAE,YAAY,CAAC;QAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,aAAa,GAAG,SAAS,CAAC;QAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACvC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;KACxC;IAWD,UAAU,IAAI,OAAO;IAIf,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,CAAC;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CAkMH"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/agent/agent.ts"],"names":[],"mappings":"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"}