micro-models-agent 0.46.2 → 0.47.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.
Files changed (218) hide show
  1. package/README.md +312 -312
  2. package/dist/main.js +397 -353
  3. package/dist/modules/browser/bridge-server.mjs +202 -202
  4. package/dist/skills/builtin/git.md +36 -36
  5. package/dist/skills/builtin/typescript.md +35 -35
  6. package/package.json +48 -48
  7. package/dist/cli/commands.js +0 -323
  8. package/dist/cli/completer.js +0 -167
  9. package/dist/cli/index.js +0 -2
  10. package/dist/cli/main.js +0 -165
  11. package/dist/cli/plugin-commands.js +0 -36
  12. package/dist/cli/repl-commands.js +0 -661
  13. package/dist/cli/repl.js +0 -616
  14. package/dist/cli/run-result.js +0 -22
  15. package/dist/cli/security-commands.js +0 -164
  16. package/dist/cli/setup.js +0 -231
  17. package/dist/config/config.js +0 -249
  18. package/dist/config/defaults.js +0 -124
  19. package/dist/config/experts.js +0 -15
  20. package/dist/config/index.js +0 -3
  21. package/dist/config/security.js +0 -193
  22. package/dist/config/types.js +0 -1
  23. package/dist/core/agent-moe.js +0 -102
  24. package/dist/core/agent.js +0 -886
  25. package/dist/core/bootstrap.js +0 -404
  26. package/dist/core/index.js +0 -2
  27. package/dist/core/prompt-builder.js +0 -76
  28. package/dist/core/session-logger.js +0 -197
  29. package/dist/core/types.js +0 -1
  30. package/dist/core/version.js +0 -24
  31. package/dist/core/workspace.js +0 -76
  32. package/dist/i18n/en.json +0 -598
  33. package/dist/i18n/index.js +0 -46
  34. package/dist/i18n/ru.json +0 -598
  35. package/dist/index.js +0 -22
  36. package/dist/llm/image-utils.js +0 -143
  37. package/dist/llm/index.js +0 -4
  38. package/dist/llm/model-loader.js +0 -78
  39. package/dist/llm/openai-compat.js +0 -359
  40. package/dist/llm/orchestrator.js +0 -198
  41. package/dist/llm/provider.js +0 -10
  42. package/dist/llm/response.js +0 -39
  43. package/dist/llm/token-counter.js +0 -39
  44. package/dist/llm/types.js +0 -1
  45. package/dist/logger/app-logger.js +0 -143
  46. package/dist/logger/file-log.js +0 -151
  47. package/dist/logger/index.js +0 -1
  48. package/dist/migration/backup.js +0 -45
  49. package/dist/migration/detect.js +0 -50
  50. package/dist/migration/index.js +0 -2
  51. package/dist/modules/artifacts/store.js +0 -61
  52. package/dist/modules/browser/actions.js +0 -76
  53. package/dist/modules/browser/bridge-client.js +0 -199
  54. package/dist/modules/browser/bridge-path.js +0 -10
  55. package/dist/modules/browser/cookie-store.js +0 -24
  56. package/dist/modules/browser/driver.js +0 -136
  57. package/dist/modules/browser/index.js +0 -7
  58. package/dist/modules/browser/module.js +0 -29
  59. package/dist/modules/browser/session.js +0 -338
  60. package/dist/modules/browser/snapshot.js +0 -148
  61. package/dist/modules/browser/types.js +0 -12
  62. package/dist/modules/certification/cli.js +0 -174
  63. package/dist/modules/certification/fact-checker.js +0 -82
  64. package/dist/modules/certification/loader.js +0 -105
  65. package/dist/modules/certification/manifest.js +0 -50
  66. package/dist/modules/certification/runner.js +0 -159
  67. package/dist/modules/certification/scenarios.js +0 -124
  68. package/dist/modules/certification/types.js +0 -1
  69. package/dist/modules/context/chunk-query.js +0 -100
  70. package/dist/modules/context/fact-extractor.js +0 -162
  71. package/dist/modules/context/history.js +0 -15
  72. package/dist/modules/context/index.js +0 -1
  73. package/dist/modules/context/manager.js +0 -423
  74. package/dist/modules/execution/audit-runners.js +0 -152
  75. package/dist/modules/execution/auditor.js +0 -218
  76. package/dist/modules/execution/execution-plugin.js +0 -272
  77. package/dist/modules/execution/index.js +0 -8
  78. package/dist/modules/execution/module.js +0 -436
  79. package/dist/modules/execution/moe-executor.js +0 -291
  80. package/dist/modules/execution/plan-coverage.js +0 -68
  81. package/dist/modules/execution/plan-persister.js +0 -46
  82. package/dist/modules/execution/plan-store.js +0 -157
  83. package/dist/modules/execution/plan-tool.js +0 -508
  84. package/dist/modules/execution/plan-validator.js +0 -153
  85. package/dist/modules/execution/planner.js +0 -90
  86. package/dist/modules/execution/stuck-detector.js +0 -510
  87. package/dist/modules/execution/tracker.js +0 -67
  88. package/dist/modules/execution/types.js +0 -1
  89. package/dist/modules/execution/verifier.js +0 -222
  90. package/dist/modules/execution/windows-commands.js +0 -41
  91. package/dist/modules/hallucination/confidence.js +0 -66
  92. package/dist/modules/hallucination/consistency.js +0 -26
  93. package/dist/modules/hallucination/detector.js +0 -43
  94. package/dist/modules/hallucination/factual.js +0 -129
  95. package/dist/modules/hallucination/index.js +0 -5
  96. package/dist/modules/hallucination/js-identifiers.js +0 -262
  97. package/dist/modules/hallucination/llm-judge.js +0 -101
  98. package/dist/modules/index.js +0 -5
  99. package/dist/modules/indexer/cache.js +0 -40
  100. package/dist/modules/indexer/index.js +0 -3
  101. package/dist/modules/indexer/module.js +0 -245
  102. package/dist/modules/indexer/project-profile.js +0 -183
  103. package/dist/modules/indexer/walker.js +0 -101
  104. package/dist/modules/lsp/check-tool.js +0 -58
  105. package/dist/modules/lsp/client.js +0 -278
  106. package/dist/modules/lsp/command.js +0 -60
  107. package/dist/modules/lsp/config.js +0 -135
  108. package/dist/modules/lsp/index.js +0 -3
  109. package/dist/modules/lsp/module.js +0 -232
  110. package/dist/modules/lsp/probe.js +0 -76
  111. package/dist/modules/lsp/project-root.js +0 -32
  112. package/dist/modules/lsp/startup-check.js +0 -141
  113. package/dist/modules/lsp/types.js +0 -1
  114. package/dist/modules/mcp/client.js +0 -399
  115. package/dist/modules/mcp/index.js +0 -3
  116. package/dist/modules/mcp/module.js +0 -142
  117. package/dist/modules/mcp/registry.js +0 -15
  118. package/dist/modules/memory/index.js +0 -1
  119. package/dist/modules/memory/module.js +0 -96
  120. package/dist/modules/memory/search.js +0 -42
  121. package/dist/modules/memory/store.js +0 -69
  122. package/dist/modules/pipelines/engine.js +0 -60
  123. package/dist/modules/pipelines/index.js +0 -3
  124. package/dist/modules/pipelines/parser.js +0 -56
  125. package/dist/modules/pipelines/template.js +0 -14
  126. package/dist/modules/plugins/builtin/lint-on-write.js +0 -231
  127. package/dist/modules/plugins/builtin/notify.js +0 -9
  128. package/dist/modules/plugins/index.js +0 -1
  129. package/dist/modules/plugins/loader.js +0 -70
  130. package/dist/modules/plugins/manager.js +0 -217
  131. package/dist/modules/plugins/types.js +0 -1
  132. package/dist/modules/processes/detect.js +0 -34
  133. package/dist/modules/processes/index.js +0 -2
  134. package/dist/modules/processes/registry.js +0 -327
  135. package/dist/modules/processes/runner.js +0 -23
  136. package/dist/modules/registry.js +0 -47
  137. package/dist/modules/security/audit-log.js +0 -136
  138. package/dist/modules/security/audit-notifier.js +0 -292
  139. package/dist/modules/security/command-validator.js +0 -205
  140. package/dist/modules/security/content-scanner.js +0 -53
  141. package/dist/modules/security/data-sanitizer.js +0 -89
  142. package/dist/modules/security/encryption.js +0 -242
  143. package/dist/modules/security/index.js +0 -14
  144. package/dist/modules/security/network-validator.js +0 -71
  145. package/dist/modules/security/path-validator.js +0 -207
  146. package/dist/modules/security/rate-limiter.js +0 -119
  147. package/dist/modules/security/security-policies.js +0 -531
  148. package/dist/modules/security/session-encryption.js +0 -210
  149. package/dist/modules/security/session-isolation.js +0 -95
  150. package/dist/modules/session/index.js +0 -3
  151. package/dist/modules/session/manager.js +0 -172
  152. package/dist/modules/session/module.js +0 -24
  153. package/dist/modules/session/store.js +0 -222
  154. package/dist/modules/session/types.js +0 -1
  155. package/dist/modules/skills/index.js +0 -2
  156. package/dist/modules/skills/loader.js +0 -72
  157. package/dist/modules/skills/matcher.js +0 -27
  158. package/dist/modules/skills/module.js +0 -129
  159. package/dist/modules/types.js +0 -1
  160. package/dist/modules/updater/checker.js +0 -96
  161. package/dist/modules/updater/index.js +0 -2
  162. package/dist/modules/updater/module.js +0 -116
  163. package/dist/modules/user-profile/compressor.js +0 -16
  164. package/dist/modules/user-profile/index.js +0 -1
  165. package/dist/modules/user-profile/profile.js +0 -68
  166. package/dist/tools/approve.js +0 -32
  167. package/dist/tools/attach-image.js +0 -89
  168. package/dist/tools/bash.js +0 -496
  169. package/dist/tools/browser.js +0 -114
  170. package/dist/tools/chunk-query.js +0 -99
  171. package/dist/tools/create-dir.js +0 -55
  172. package/dist/tools/delete-file.js +0 -62
  173. package/dist/tools/download-file.js +0 -116
  174. package/dist/tools/edit-file.js +0 -79
  175. package/dist/tools/enable-tools.js +0 -58
  176. package/dist/tools/executor.js +0 -144
  177. package/dist/tools/file-info.js +0 -46
  178. package/dist/tools/filter-tools.js +0 -17
  179. package/dist/tools/glob-tool.js +0 -26
  180. package/dist/tools/grep-tool.js +0 -84
  181. package/dist/tools/hidden-tools-block.js +0 -37
  182. package/dist/tools/index.js +0 -78
  183. package/dist/tools/list-dir.js +0 -48
  184. package/dist/tools/load-skill.js +0 -42
  185. package/dist/tools/mcp-call.js +0 -68
  186. package/dist/tools/move-file.js +0 -85
  187. package/dist/tools/path-utils.js +0 -51
  188. package/dist/tools/pipeline-run.js +0 -144
  189. package/dist/tools/preview.js +0 -2
  190. package/dist/tools/process-kill.js +0 -29
  191. package/dist/tools/process-list.js +0 -36
  192. package/dist/tools/process-log.js +0 -45
  193. package/dist/tools/question.js +0 -140
  194. package/dist/tools/read-file.js +0 -91
  195. package/dist/tools/recall.js +0 -117
  196. package/dist/tools/registry.js +0 -47
  197. package/dist/tools/remember.js +0 -67
  198. package/dist/tools/scope-check.js +0 -30
  199. package/dist/tools/search-history.js +0 -84
  200. package/dist/tools/subagent.js +0 -196
  201. package/dist/tools/types.js +0 -1
  202. package/dist/tools/user-input.js +0 -123
  203. package/dist/tools/web-browse.js +0 -86
  204. package/dist/tools/web-fetch.js +0 -98
  205. package/dist/tools/web-search.js +0 -78
  206. package/dist/tools/write-file.js +0 -81
  207. package/dist/ui/box.js +0 -77
  208. package/dist/ui/colors.js +0 -4
  209. package/dist/ui/diff.js +0 -178
  210. package/dist/ui/index.js +0 -6
  211. package/dist/ui/line-editor.js +0 -703
  212. package/dist/ui/line-math.js +0 -69
  213. package/dist/ui/md-formatter.js +0 -212
  214. package/dist/ui/output.js +0 -13
  215. package/dist/ui/plan-view.js +0 -103
  216. package/dist/ui/renderer.js +0 -209
  217. package/dist/ui/spinner.js +0 -70
  218. package/dist/ui/table.js +0 -144
@@ -1,886 +0,0 @@
1
- import { t } from "../i18n/index";
2
- import { pc } from "../ui/colors";
3
- import { PromptBuilder } from "./prompt-builder";
4
- import { processRegistry } from "../modules/processes";
5
- import { SessionLogger } from "./session-logger";
6
- import { runWithMoE } from "./agent-moe";
7
- import { StepVerifier } from "../modules/execution/verifier";
8
- const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
9
- const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
10
- const QUALITY_TRIGGER_THRESHOLD = 40;
11
- /** Minimum iterations between quality-triggered forced compactions. Without
12
- * this, a low-quality context re-triggers compaction on EVERY iteration
13
- * (observed: 56 compactions in ~28 min) and the compaction itself can't
14
- * restore quality, so the agent burns the whole budget compacting. */
15
- const FORCED_COMPACTION_COOLDOWN = 3;
16
- /** True when the text looks like a raw JSON tool payload (garbage to display). */
17
- function isToolCallJson(text) {
18
- const trimmed = text.trim();
19
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
20
- try {
21
- JSON.parse(trimmed);
22
- return true;
23
- }
24
- catch {
25
- return false;
26
- }
27
- }
28
- return false;
29
- }
30
- /**
31
- * Max chars a tool result may occupy, given remaining budget and whether
32
- * the tool bounds its own output. Tools that declare `boundedOutput` (e.g.
33
- * read_file with its line limit) are never truncated by the budget — a
34
- * near-full context used to cut them to ~2K chars, making the model believe
35
- * files were truncated and re-read them forever.
36
- */
37
- export function toolOutputCharLimit(remainingBudget, historyBudget, bounded) {
38
- if (bounded)
39
- return Number.MAX_SAFE_INTEGER;
40
- const maxCharsByRatio = Math.floor(historyBudget * TOOL_RESULT_MAX_TOKENS_RATIO * 2);
41
- return Math.min(remainingBudget, maxCharsByRatio, TOOL_RESULT_ABSOLUTE_MAX_CHARS);
42
- }
43
- export class Agent {
44
- deps;
45
- systemPromptAdded = false;
46
- shutdownRequested = false;
47
- abortController = null;
48
- lastCompactionShown = 0;
49
- constructor(deps) {
50
- this.deps = deps;
51
- }
52
- /** Expose context manager for REPL image attachment and other direct access. */
53
- get contextManager() {
54
- return this.deps.contextManager;
55
- }
56
- setScope() {
57
- if (this.deps.scope) {
58
- this.deps.toolExecutor.setScope(this.deps.scope);
59
- }
60
- }
61
- buildSystemPrompt() {
62
- const systemBudget = Math.floor(this.deps.config.contextWindow * this.deps.config.contextBudget.systemPrompt);
63
- const builder = new PromptBuilder(systemBudget);
64
- builder.addBlocks(this.deps.promptBlocks);
65
- const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
66
- if (dynamic.length > 0) {
67
- builder.addBlocks(dynamic);
68
- }
69
- const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? []).flatMap((content) => content && content.trim() !== ""
70
- ? [
71
- {
72
- content,
73
- priority: "low",
74
- essential: false,
75
- estimatedTokens: this.deps.llmProvider.countTokens(content),
76
- },
77
- ]
78
- : []);
79
- if (pluginBlocks.length > 0) {
80
- builder.addBlocks(pluginBlocks);
81
- }
82
- const result = builder.build();
83
- return {
84
- prompt: result.prompt,
85
- excluded: result.excluded,
86
- blocks: result.blocks,
87
- };
88
- }
89
- /**
90
- * Log the current context state to the session log. Called every iteration
91
- * so the session.jsonl shows a full timeline of how the context grows,
92
- * compacts, and what it consists of. The `start` snapshot additionally
93
- * carries the system-prompt block breakdown (which blocks, priorities,
94
- * token estimates, what was excluded by the budget).
95
- */
96
- logContextStat(kind, iteration, slog, blocks) {
97
- const cm = this.deps.contextManager;
98
- if (typeof cm.getSnapshot !== "function")
99
- return;
100
- const snap = cm.getSnapshot();
101
- const history = cm.getActiveHistory();
102
- const systemMsg = history.find((m) => m.role === "system");
103
- slog.logContext({
104
- kind,
105
- iteration,
106
- window: snap.window,
107
- systemBudget: snap.budget.systemPrompt,
108
- reserveBudget: snap.budget.responseReserve,
109
- historyBudget: snap.budget.history,
110
- systemTokens: kind === "start" && systemMsg && typeof systemMsg.content === "string"
111
- ? this.deps.llmProvider.countTokens(systemMsg.content)
112
- : undefined,
113
- toolTokens: snap.toolTokens,
114
- tokens: snap.tokens,
115
- quality: snap.quality,
116
- messageCount: snap.messageCount,
117
- compactionCount: snap.compactionCount,
118
- iterationsSinceCompaction: snap.iterationsSinceCompaction,
119
- blocks,
120
- });
121
- }
122
- getSystemPromptInfo() {
123
- const { prompt, excluded } = this.buildSystemPrompt();
124
- const tokenCount = this.deps.llmProvider.countTokens(prompt);
125
- return { text: prompt, tokenCount, excluded };
126
- }
127
- /**
128
- * Some OpenAI-compatible backends (llama.cpp) omit `usage` from responses,
129
- * leaving apiPromptTokens/apiCompletionTokens at 0. Fall back to local
130
- * estimates so JSON results still carry meaningful token metrics.
131
- */
132
- resolveUsageTokens(apiPromptTokens, apiCompletionTokens, estimatedPromptTokens, completionChars) {
133
- if (apiPromptTokens > 0 || apiCompletionTokens > 0) {
134
- return {
135
- prompt: apiPromptTokens,
136
- completion: apiCompletionTokens,
137
- total: apiPromptTokens + apiCompletionTokens,
138
- };
139
- }
140
- // ~4 chars per token is a reasonable heuristic when the backend gives
141
- // us nothing (matches the pre-tiktoken fallback elsewhere in the code).
142
- const prompt = Math.max(1, estimatedPromptTokens);
143
- const completion = Math.max(0, Math.ceil(completionChars / 4));
144
- return { prompt, completion, total: prompt + completion };
145
- }
146
- refreshSystemPrompt() {
147
- const { prompt } = this.buildSystemPrompt();
148
- const current = this.deps.contextManager.getActiveHistory().find((m) => m.role === "system");
149
- if (!current || current.content !== prompt) {
150
- this.deps.contextManager.updateSystemPrompt?.(prompt);
151
- }
152
- }
153
- truncateToolOutput(output, budget, currentTokens, bounded) {
154
- const remainingBudget = Math.max(0, budget.history - currentTokens);
155
- const maxChars = toolOutputCharLimit(remainingBudget, budget.history, bounded);
156
- if (output.length <= maxChars)
157
- return output;
158
- const truncated = output.slice(0, maxChars);
159
- const removedChars = output.length - maxChars;
160
- return truncated + `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
161
- }
162
- emitPhase(iteration, phase, onPhase) {
163
- this.deps.pluginManager.runOnPhase?.({
164
- iteration,
165
- logger: this.deps.logger,
166
- contextManager: this.deps.contextManager,
167
- }, phase);
168
- onPhase?.(phase);
169
- }
170
- async run(input, onChunk, onMeta, onTool, onPhase) {
171
- this.setScope();
172
- const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
173
- const slog = new SessionLogger(sessionManager, logger);
174
- if (sessionManager && !sessionManager.getActive()) {
175
- sessionManager.create();
176
- logger.debug(`Session started: ${sessionManager.getActive()}`);
177
- }
178
- if (!this.systemPromptAdded &&
179
- !contextManager.getActiveHistory().some((m) => m.role === "system")) {
180
- const lazy = this.deps.lazyPromptBlocks ? await this.deps.lazyPromptBlocks() : [];
181
- if (lazy.length > 0) {
182
- this.deps.promptBlocks.push(...lazy);
183
- }
184
- const { prompt: systemPrompt, excluded } = this.buildSystemPrompt();
185
- contextManager.addMessage({ role: "system", content: systemPrompt });
186
- this.systemPromptAdded = true;
187
- slog.logSystem(systemPrompt.slice(0, 2000));
188
- if (excluded.length > 0) {
189
- slog.logSystem(`[Excluded prompt blocks: ${excluded.length}]`);
190
- }
191
- pluginManager.runOnSessionStart({
192
- logger,
193
- sessionManager: sessionManager?.getActiveMeta(),
194
- contextManager,
195
- });
196
- if (config.session?.baselineCheck !== false) {
197
- const verifier = new StepVerifier(baseDir);
198
- verifier
199
- .runTypeCheck()
200
- .then((tc) => {
201
- if (!tc.passed) {
202
- logger.warn(`Baseline typecheck has issues: ${tc.message?.slice(0, 500)}`);
203
- onMeta?.(pc.yellow(`\n⚠ Baseline typecheck has issues\n`));
204
- }
205
- })
206
- .catch(() => { });
207
- }
208
- }
209
- // A new user message starts a fresh compaction interval — iterations
210
- // from the previous turn must not trigger a compaction on iteration 1
211
- // of this one (observed: turn 2 compacted at iteration 8, deleting the
212
- // just-sent task along with 40+ old turns).
213
- if (typeof contextManager.resetUserTurn === "function") {
214
- contextManager.resetUserTurn();
215
- }
216
- contextManager.addMessage({ role: "user", content: input });
217
- slog.logUser(input);
218
- if (config.moe?.enabled) {
219
- return runWithMoE({
220
- config,
221
- llmProvider,
222
- toolExecutor,
223
- logger,
224
- baseDir: this.deps.baseDir,
225
- }, input, () => this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase), { onMeta, onTool, onPhase });
226
- }
227
- return this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase);
228
- }
229
- async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
230
- const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, baseDir, } = this.deps;
231
- const slog = new SessionLogger(sessionManager, logger);
232
- this.abortController = new AbortController();
233
- let iteration = 0;
234
- let lastText = "";
235
- // The actual most-recent model output (tool commentary, retried answers
236
- // included). Repetition is compared against THIS, not the last *accepted*
237
- // text — a frozen accepted answer made consecutive retries compare
238
- // against a stale baseline and flag every re-answer as repetitive.
239
- let lastModelText = "";
240
- let lastForcedCompactionIteration = -FORCED_COMPACTION_COOLDOWN;
241
- let hallucinationRetries = 0;
242
- let lastToolSignature = "";
243
- let apiPromptTokens = 0;
244
- let apiCompletionTokens = 0;
245
- let apiCompletionChars = 0;
246
- const MAX_HALLUCINATION_RETRIES = 3;
247
- let consecutiveToolFailures = 0;
248
- const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
249
- // Per-tool failure counts and which tools already produced a memory rule.
250
- const toolFailureCounts = new Map();
251
- const memoryRuleRecorded = new Set();
252
- const MIN_REPEATED_TOOL_FAILURES = 3;
253
- let auditRetries = 0;
254
- const MAX_AUDIT_RETRIES = 3;
255
- let emptyResponseRetries = 0;
256
- const MAX_EMPTY_RESPONSE_RETRIES = 2;
257
- let emptyResponseExhausted = false;
258
- let auditFailed = false;
259
- let lastAuditSummary = "";
260
- // Set when the audit gate rejects a final answer and re-prompts: the next
261
- // non-tool response is then a re-answer of an already-completed task, so
262
- // a repetition verdict is expected and must not burn a hallucination retry.
263
- let suppressRepetitionRetry = false;
264
- let repeatedToolCount = 0;
265
- const MAX_REPEATED_TOOL_CALLS = 2;
266
- // Account for tool definitions in context budget (they're sent via body.tools, not messages)
267
- // Tool definitions are recomputed each iteration so `enable_tools`
268
- // (which mutates the shared activeToolTags array) can grow the
269
- // LLM-visible tool set mid-run. Bound per-iteration to keep the budget
270
- // estimate and boundedOutput set in sync with what is actually sent.
271
- let allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
272
- let boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
273
- let toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
274
- contextManager.setToolTokens(toolTokenEstimate);
275
- while (iteration < config.maxToolIterations && !this.shutdownRequested) {
276
- iteration++;
277
- contextManager.noteIteration();
278
- // Re-read the mutable tag set in case enable_tools was called.
279
- allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
280
- boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
281
- toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
282
- contextManager.setToolTokens(toolTokenEstimate);
283
- pluginManager.runOnBeforeThink({
284
- iteration,
285
- logger,
286
- lastUserMessage: input,
287
- contextManager,
288
- onMeta,
289
- sessionLog: {
290
- plan: (event, detail, iter) => slog.logPlan(event, detail, iter),
291
- },
292
- });
293
- if (contextManager.needsCompaction()) {
294
- const result = contextManager.compact();
295
- if (result) {
296
- logger.debug("Context compacted");
297
- slog.logCompaction({ reason: "interval", iteration, ...result });
298
- }
299
- }
300
- const currentTokens = contextManager.getEstimatedTokens();
301
- const budget = contextManager.getBudget();
302
- const quality = contextManager.getQuality();
303
- if (quality < QUALITY_TRIGGER_THRESHOLD &&
304
- contextManager.getCompactionCount() > 0 &&
305
- iteration - lastForcedCompactionIteration >= FORCED_COMPACTION_COOLDOWN) {
306
- lastForcedCompactionIteration = iteration;
307
- const result = contextManager.compact();
308
- logger.warn(`Low context quality (${quality}%) — forced compaction`);
309
- if (result) {
310
- slog.logCompaction({
311
- reason: `quality-triggered (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%)`,
312
- iteration,
313
- ...result,
314
- });
315
- }
316
- }
317
- if (currentTokens > budget.history) {
318
- const result = contextManager.compact();
319
- logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
320
- if (result) {
321
- slog.logCompaction({
322
- reason: `overflow (${currentTokens} > ${budget.history})`,
323
- iteration,
324
- ...result,
325
- });
326
- }
327
- }
328
- this.refreshSystemPrompt();
329
- const history = contextManager.getActiveHistory();
330
- slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
331
- if (iteration === 1) {
332
- const { blocks } = this.buildSystemPrompt();
333
- this.logContextStat("start", iteration, slog, blocks);
334
- }
335
- else {
336
- this.logContextStat("iteration", iteration, slog);
337
- }
338
- let textContent = "";
339
- let reasoningContent = "";
340
- const toolCalls = [];
341
- let sawToolCall = false;
342
- let emittedReasoning = false;
343
- const textChunks = [];
344
- this.emitPhase(iteration, "thinking", onPhase);
345
- const llmStart = Date.now();
346
- const promptBefore = apiPromptTokens;
347
- const completionBefore = apiCompletionTokens;
348
- logger.logLLMRequest(config.model, history.length, input, "agent");
349
- try {
350
- for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
351
- if (this.shutdownRequested)
352
- break;
353
- if (chunk.type === "text" && chunk.content) {
354
- if (emittedReasoning && !textContent) {
355
- onMeta?.("\n\n");
356
- }
357
- textContent += chunk.content;
358
- textChunks.push(chunk.content);
359
- }
360
- if (chunk.type === "reasoning" && chunk.content) {
361
- reasoningContent += chunk.content;
362
- if (config.showReasoning) {
363
- const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, chunk.content);
364
- if (metaOut) {
365
- onMeta?.(pc.dim(metaOut));
366
- }
367
- emittedReasoning = true;
368
- }
369
- }
370
- if (chunk.type === "tool_call" && chunk.toolCall) {
371
- sawToolCall = true;
372
- let parsedArgs;
373
- try {
374
- parsedArgs = JSON.parse(chunk.toolCall.arguments);
375
- }
376
- catch {
377
- parsedArgs = {};
378
- }
379
- toolCalls.push({
380
- id: chunk.toolCall.id,
381
- name: chunk.toolCall.name,
382
- arguments: parsedArgs,
383
- });
384
- }
385
- if (chunk.type === "done" && chunk.usage) {
386
- apiPromptTokens += chunk.usage.promptTokens;
387
- apiCompletionTokens += chunk.usage.completionTokens;
388
- }
389
- }
390
- }
391
- catch (err) {
392
- if (this.shutdownRequested || err?.name === "AbortError") {
393
- logger.info("LLM call aborted (interrupt)");
394
- break;
395
- }
396
- logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
397
- logger.error(`LLM call failed: ${err.message}`);
398
- slog.logError(err.message);
399
- pluginManager.runOnError({ iteration, logger, contextManager }, err);
400
- return {
401
- success: false,
402
- text: lastText,
403
- error: t("error.llm", { message: err.message }),
404
- iterationCount: iteration,
405
- };
406
- }
407
- finally {
408
- this.emitPhase(iteration, "done", onPhase);
409
- }
410
- // Track response length so token metrics stay meaningful even when
411
- // the backend omits `usage` from the response.
412
- apiCompletionChars += (textContent || reasoningContent).length;
413
- logger.logLLMResponse(config.model, (textContent || reasoningContent).length, Date.now() - llmStart, undefined, "agent");
414
- // Log per-call token usage. llama.cpp streaming often omits `usage`,
415
- // so fall back to local estimates (context tokens + chars/4) and
416
- // mark the source — the log must distinguish real API numbers from
417
- // heuristics.
418
- {
419
- const usagePrompt = apiPromptTokens - promptBefore;
420
- const usageCompletion = apiCompletionTokens - completionBefore;
421
- const source = usagePrompt > 0 || usageCompletion > 0 ? "api" : "estimate";
422
- const prompt = source === "api" ? usagePrompt : contextManager.getEstimatedTokens();
423
- const completion = source === "api"
424
- ? usageCompletion
425
- : Math.ceil((textContent || reasoningContent).length / 4);
426
- slog.logLlmUsage(iteration, {
427
- promptTokens: prompt,
428
- completionTokens: completion,
429
- totalTokens: prompt + completion,
430
- source,
431
- durationMs: Date.now() - llmStart,
432
- });
433
- }
434
- if (this.shutdownRequested) {
435
- break;
436
- }
437
- // Show the model's commentary text. When a tool call accompanies the
438
- // response, keep the text too (opencode-like narration), unless it is
439
- // a raw JSON payload that small models sometimes emit instead of
440
- // describing the call. `toolComments: false` restores the old behavior
441
- // of suppressing text next to a tool call.
442
- const toolComments = this.deps.config.ui?.toolComments ?? true;
443
- const showText = textChunks.length > 0 && (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
444
- if (showText) {
445
- for (const chunk of textChunks) {
446
- const textOut = pluginManager.runOnText({ iteration, logger, contextManager }, chunk);
447
- onChunk?.(textOut);
448
- }
449
- }
450
- let llmResponse = null;
451
- if (sawToolCall) {
452
- llmResponse = { type: "tool_call", calls: toolCalls };
453
- }
454
- else if (textContent) {
455
- llmResponse = { type: "text", content: textContent };
456
- }
457
- else if (reasoningContent) {
458
- llmResponse = { type: "reasoning", content: reasoningContent };
459
- }
460
- pluginManager.runOnAfterThink({ iteration, logger, contextManager }, llmResponse);
461
- if (this.deps.exitOnComplete && sawToolCall) {
462
- const signature = toolCalls
463
- .map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
464
- .join("|");
465
- if (signature && signature === lastToolSignature) {
466
- // A repeated identical tool call is often the model re-running
467
- // a command after a confusing result. Give it one more chance
468
- // to produce a final text answer instead of stopping with
469
- // text: "" (observed on 08-r4: bash re-run → empty result).
470
- repeatedToolCount++;
471
- if (repeatedToolCount >= MAX_REPEATED_TOOL_CALLS) {
472
- logger.debug("Exit-on-complete: repeated identical tool call, stopping");
473
- break;
474
- }
475
- contextManager.addMessage({
476
- role: "user",
477
- content: `<system-summary>You just called the same tool with identical arguments. If the task is done, answer with a final text response NOW. If the command failed, try a different approach.</system-summary>`,
478
- });
479
- }
480
- lastToolSignature = signature;
481
- }
482
- if (sawToolCall) {
483
- slog.logAssistant(textContent || "", reasoningContent, toolCalls, iteration);
484
- }
485
- if (sawToolCall) {
486
- contextManager.addMessage({
487
- role: "assistant",
488
- content: textContent || "",
489
- tool_calls: toolCalls.map((tc) => ({
490
- id: tc.id,
491
- type: "function",
492
- function: {
493
- name: tc.name,
494
- arguments: JSON.stringify(tc.arguments),
495
- },
496
- })),
497
- });
498
- const summaries = [];
499
- let anyToolFailed = false;
500
- for (const call of toolCalls) {
501
- this.setScope();
502
- const startTime = Date.now();
503
- pluginManager.runOnToolCall({
504
- toolName: call.name,
505
- args: call.arguments,
506
- });
507
- pluginManager.runOnToolStart({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments });
508
- onTool?.({ type: "start", tool: call.name, args: call.arguments });
509
- slog.logToolCall(call, iteration);
510
- const tokensBeforeTool = contextManager.getEstimatedTokens();
511
- const result = await toolExecutor.execute(call, this.abortController?.signal);
512
- const duration = Date.now() - startTime;
513
- if (!result.success)
514
- anyToolFailed = true;
515
- if (result.success && call.arguments.path) {
516
- const filePath = String(call.arguments.path);
517
- if (call.name === "write_file" || call.name === "edit_file") {
518
- hallucinationDetector.getConsistencyCheck().trackCreatedFile(filePath);
519
- }
520
- else if (call.name === "delete_file") {
521
- hallucinationDetector.getConsistencyCheck().trackDeletedFile(filePath);
522
- }
523
- }
524
- pluginManager.runOnToolEnd({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
525
- if (result.display) {
526
- onMeta?.("\n" + result.display + "\n");
527
- }
528
- else {
529
- const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
530
- onMeta?.("\n" + pc.dim(metaOut) + "\n");
531
- }
532
- if (result.diff) {
533
- onMeta?.("\n" + result.diff + "\n");
534
- }
535
- const currentTokens = contextManager.getEstimatedTokens();
536
- const budget = contextManager.getBudget();
537
- const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens, boundedToolNames.has(call.name));
538
- contextManager.addMessage({
539
- role: "tool",
540
- content: truncatedOutput,
541
- name: call.name,
542
- tool_call_id: call.id,
543
- success: result.success,
544
- arguments: call.arguments,
545
- });
546
- const tokensAfterTool = contextManager.getEstimatedTokens();
547
- onTool?.({
548
- type: "end",
549
- tool: call.name,
550
- args: call.arguments,
551
- duration,
552
- error: !result.success,
553
- ctxDelta: tokensAfterTool - tokensBeforeTool,
554
- });
555
- summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
556
- if (config.session.autoSave) {
557
- slog.logToolResult(call, result, duration, iteration);
558
- }
559
- if (contextManager.needsCompaction()) {
560
- const result = contextManager.compact();
561
- if (result) {
562
- logger.debug("Context compacted after tool result");
563
- slog.logCompaction({
564
- reason: "after_tool",
565
- iteration,
566
- ...result,
567
- });
568
- }
569
- }
570
- // Track per-tool failure counts (NOT just consecutive) so a tool
571
- // that keeps failing while other tools succeed between attempts
572
- // is still learned from (observed: LSP spawn npx ENOENT failed
573
- // 8x in one session, never consecutively, so it never reached
574
- // memory via the 5-consecutive-failures path).
575
- if (!result.success) {
576
- const key = call.name;
577
- const prev = toolFailureCounts.get(key) ?? { count: 0, error: "" };
578
- prev.count++;
579
- prev.error = String(result.output ?? "").slice(0, 200);
580
- toolFailureCounts.set(key, prev);
581
- if (prev.count >= MIN_REPEATED_TOOL_FAILURES && !memoryRuleRecorded.has(key)) {
582
- memoryRuleRecorded.add(key);
583
- const memStore = this.deps.memoryStore;
584
- if (memStore) {
585
- memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.error})`, `Repeated ${key} failures suggest a systemic problem (config, environment, or a broken tool), not a one-off`, "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool");
586
- logger.warn(`Recorded repeated ${key} failures to memory (${prev.count}x)`);
587
- }
588
- }
589
- }
590
- }
591
- if (anyToolFailed) {
592
- consecutiveToolFailures++;
593
- }
594
- else {
595
- consecutiveToolFailures = 0;
596
- }
597
- if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
598
- const recoveryMsg = t("exec.consecutive_failures_recovery", {
599
- count: consecutiveToolFailures,
600
- });
601
- logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
602
- const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
603
- const taskReminder = t("exec.task_reminder", { task: taskSnippet });
604
- contextManager.addMessage({
605
- role: "user",
606
- content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
607
- });
608
- const memStore = this.deps.memoryStore;
609
- if (memStore) {
610
- memStore.appendRule("errors", `${consecutiveToolFailures} consecutive tool failures`, "Multiple tools failing suggests environment or configuration issue", "Check dependencies, verify file paths, try write_file directly instead of shell commands");
611
- }
612
- }
613
- contextManager.addMessage({
614
- role: "user",
615
- content: `<system-summary>${summaries.join("\n")}</system-summary>`,
616
- });
617
- const ui = this.deps.config.ui;
618
- if (ui?.showContextStats) {
619
- const ctxTokens = contextManager.getEstimatedTokens();
620
- const ctxBudget = contextManager.getBudget();
621
- const ctxPct = Math.min(100, Math.round((ctxTokens / ctxBudget.history) * 100));
622
- const barLen = 10;
623
- const filled = Math.round((ctxPct / 100) * barLen);
624
- const ctxBar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
625
- const pctColor = ctxPct >= 75 ? pc.yellow : pc.dim;
626
- const compCount = contextManager.getCompactionCount();
627
- const quality = contextManager.getQuality();
628
- const qualityColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
629
- onMeta?.(`\n ${ctxBar} ${pctColor(`${ctxPct}%`)} ${pc.dim(`ctx: ${ctxTokens}/${ctxBudget.history}`)} ${pc.dim(`compactions: ${compCount}`)} ${qualityColor(`quality: ${quality}%`)}\n`);
630
- }
631
- else if (ui?.showCompaction) {
632
- const compCount = contextManager.getCompactionCount();
633
- if (compCount > this.lastCompactionShown) {
634
- this.lastCompactionShown = compCount;
635
- onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
636
- }
637
- }
638
- // A tool call means the model kept working instead of just
639
- // re-answering — the audit-re-answer leniency no longer applies.
640
- suppressRepetitionRetry = false;
641
- continue;
642
- }
643
- hallucinationDetector.getConfidenceCheck().setPreviousResponse(lastModelText);
644
- // Update AFTER setPreviousResponse so the comparison uses the
645
- // previous iteration's output, not this one (which would otherwise
646
- // always overlap with itself).
647
- if (textContent)
648
- lastModelText = textContent;
649
- const hallucinationResult = await hallucinationDetector.validate(textContent);
650
- if (hallucinationResult.status === "block") {
651
- logger.warn(`Response blocked: ${hallucinationResult.reason}`);
652
- return {
653
- success: false,
654
- text: lastText,
655
- error: t("error.response_blocked", {
656
- reason: hallucinationResult.reason || "",
657
- }),
658
- iterationCount: iteration,
659
- };
660
- }
661
- if (hallucinationResult.status === "warn") {
662
- logger.warn(`Hallucination warning: ${hallucinationResult.reason}`);
663
- const warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
664
- if (onMeta) {
665
- onMeta(`\n${pc.yellow(warnLine)}\n`);
666
- }
667
- else if (onChunk) {
668
- onChunk(`\n${warnLine}\n`);
669
- }
670
- }
671
- if (hallucinationResult.status === "retry") {
672
- if (this.deps.exitOnComplete && textContent?.trim()) {
673
- logger.debug("Exit-on-complete: stopping on first response");
674
- // Save the response BEFORE breaking — lastText is still the
675
- // previous (tool-only) iteration's text, so without this the
676
- // final answer is lost (reported text: "").
677
- lastText = textContent;
678
- break;
679
- }
680
- if (suppressRepetitionRetry && hallucinationResult.kind === "repetition") {
681
- // The audit gate rejected the previous final answer and
682
- // re-prompted the model. Its re-answer restating the completed
683
- // task is naturally "repetitive" — that is expected, not
684
- // degeneration. Downgrade to a warning and let the response
685
- // flow through to the final audit gate again (which is itself
686
- // bounded by MAX_AUDIT_RETRIES).
687
- suppressRepetitionRetry = false;
688
- logger.warn(`Audit-triggered re-answer repetition — not counted as hallucination retry`);
689
- const warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
690
- if (onMeta) {
691
- onMeta(`\n${pc.yellow(warnLine)}\n`);
692
- }
693
- else if (onChunk) {
694
- onChunk(`\n${warnLine}\n`);
695
- }
696
- // fall through to the acceptance path below (assistant message,
697
- // lastText, final audit gate).
698
- }
699
- else {
700
- // NOTE: with exitOnComplete and an EMPTY text we deliberately
701
- // do NOT break — the model produced no usable answer yet (same
702
- // case as the empty-response guard below). Falling through to
703
- // the retry path keeps us from finishing with text: "".
704
- if (hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
705
- logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
706
- return {
707
- success: false,
708
- text: lastText,
709
- error: t("error.response_blocked", {
710
- reason: t("hall.max_retries_exhausted"),
711
- }),
712
- iterationCount: iteration,
713
- };
714
- }
715
- hallucinationRetries++;
716
- logger.warn(`Hallucination retry (${hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${hallucinationResult.reason}`);
717
- if (textContent) {
718
- contextManager.addMessage({
719
- role: "assistant",
720
- content: textContent,
721
- });
722
- }
723
- contextManager.addMessage({
724
- role: "user",
725
- content: `<system-summary>[Retry context: ${hallucinationResult.reason}. Original task: "${input}". You must either call a needed tool or provide a substantive response. Empty replies are not allowed.]</system-summary>`,
726
- });
727
- continue;
728
- }
729
- }
730
- if (textContent) {
731
- contextManager.addMessage({ role: "assistant", content: textContent });
732
- if (config.session.autoSave) {
733
- slog.saveAssistantMessage(textContent);
734
- }
735
- slog.logAssistant(textContent, reasoningContent, undefined, iteration);
736
- }
737
- lastText = textContent;
738
- {
739
- const decisionPatterns = [
740
- ...textContent.matchAll(/(?:plan|decided|decision|решено|план|решение):\s*(.+?)(?:\n|$)/gi),
741
- ];
742
- for (const match of decisionPatterns) {
743
- hallucinationDetector
744
- .getConsistencyCheck()
745
- .trackDecision(match[1].trim(), "agent_response");
746
- }
747
- }
748
- if (!sawToolCall) {
749
- // Guard: the model returned an EMPTY final response (no text, no
750
- // tool calls — often just reasoning content after context
751
- // compaction). Nudge it to produce a real answer instead of
752
- // silently finishing with text: "".
753
- if (!textContent?.trim()) {
754
- if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
755
- emptyResponseRetries++;
756
- logger.warn(`Empty response on iteration ${iteration} (retry ${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
757
- contextManager.addMessage({
758
- role: "user",
759
- content: `<system-summary>Your previous response was empty. Answer the user's task now with a final text response or call a tool. Do not reply with reasoning only.</system-summary>`,
760
- });
761
- continue;
762
- }
763
- emptyResponseExhausted = true;
764
- logger.warn(`Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
765
- }
766
- if (this.deps.finalAudit) {
767
- const audit = await this.deps.finalAudit();
768
- if (audit && !audit.passed) {
769
- logger.warn(`Final audit incomplete: ${audit.summary}`);
770
- const steps = audit.pendingSteps.slice(0, 5).join("; ") || "—";
771
- contextManager.addMessage({
772
- role: "user",
773
- content: `<system-summary>${t("exec.audit_incomplete", {
774
- summary: audit.summary,
775
- steps,
776
- })}</system-summary>`,
777
- });
778
- slog.logAudit(audit.summary, iteration);
779
- auditRetries++;
780
- lastAuditSummary = audit.summary;
781
- if (auditRetries >= MAX_AUDIT_RETRIES || iteration >= config.maxToolIterations - 1) {
782
- logger.warn(`Final audit still incomplete after ${auditRetries} retries — reporting failure`);
783
- auditFailed = true;
784
- break;
785
- }
786
- // The next non-tool response is a forced re-answer of an
787
- // already-completed task — allow one repetition without
788
- // burning the hallucination budget (see retry branch).
789
- suppressRepetitionRetry = true;
790
- continue;
791
- }
792
- }
793
- break;
794
- }
795
- }
796
- const tokensUsed = contextManager.getEstimatedTokens();
797
- const budget = contextManager.getBudget();
798
- const usageTokens = this.resolveUsageTokens(apiPromptTokens, apiCompletionTokens, tokensUsed, apiCompletionChars);
799
- if (iteration >= config.maxToolIterations) {
800
- return {
801
- success: false,
802
- text: lastText,
803
- error: t("error.max_iters", { max: config.maxToolIterations }),
804
- iterationCount: iteration,
805
- contextUsed: tokensUsed,
806
- contextLimit: budget.history,
807
- promptTokens: usageTokens.prompt,
808
- completionTokens: usageTokens.completion,
809
- totalTokens: usageTokens.total,
810
- compactionCount: contextManager.getCompactionCount(),
811
- contextQuality: contextManager.getQuality(),
812
- };
813
- }
814
- return {
815
- success: emptyResponseExhausted || auditFailed ? false : true,
816
- text: lastText,
817
- error: emptyResponseExhausted
818
- ? t("error.empty_response")
819
- : auditFailed
820
- ? t("error.audit_failed", { summary: lastAuditSummary })
821
- : undefined,
822
- iterationCount: iteration,
823
- contextUsed: tokensUsed,
824
- contextLimit: budget.history,
825
- promptTokens: usageTokens.prompt,
826
- completionTokens: usageTokens.completion,
827
- totalTokens: usageTokens.total,
828
- compactionCount: contextManager.getCompactionCount(),
829
- contextQuality: contextManager.getQuality(),
830
- };
831
- }
832
- clearContext() {
833
- this.deps.contextManager.clear();
834
- this.systemPromptAdded = false;
835
- }
836
- async reconfigure(config) {
837
- const { OpenAICompatProvider } = await import("../llm/openai-compat");
838
- const { TokenCounter } = await import("../llm/token-counter");
839
- const newProvider = new OpenAICompatProvider({
840
- model: config.model,
841
- baseUrl: config.provider.baseUrl,
842
- apiKey: config.provider.apiKey,
843
- contextWindow: config.contextWindow,
844
- retry: config.retry,
845
- rateLimits: config.security?.rateLimits,
846
- });
847
- this.deps.llmProvider = newProvider;
848
- this.deps.toolExecutor.updateProvider(newProvider);
849
- this.deps.toolExecutor.ctx.llmProvider = newProvider;
850
- const newTokenCounter = new TokenCounter(config.model);
851
- this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
852
- this.deps.config = config;
853
- }
854
- setContext(messages) {
855
- const { contextManager } = this.deps;
856
- contextManager.clear();
857
- const { prompt: systemPrompt } = this.buildSystemPrompt();
858
- contextManager.addMessage({ role: "system", content: systemPrompt });
859
- this.systemPromptAdded = true;
860
- for (const msg of messages) {
861
- if (msg.role === "system")
862
- continue;
863
- contextManager.addMessage({
864
- role: msg.role,
865
- content: msg.content,
866
- name: msg.name,
867
- });
868
- }
869
- }
870
- shutdown() {
871
- this.shutdownRequested = true;
872
- this.abortController?.abort();
873
- const { pluginManager, logger, sessionManager, contextManager } = this.deps;
874
- contextManager.onCompact = null;
875
- const killed = processRegistry.killAll();
876
- if (killed > 0) {
877
- logger.info(`Killed ${killed} background process(es) on shutdown`);
878
- }
879
- logger.closeSessionLog();
880
- pluginManager.runOnSessionEnd({
881
- logger,
882
- sessionManager: sessionManager?.getActiveMeta(),
883
- contextManager,
884
- });
885
- }
886
- }