micro-models-agent 0.59.0 → 0.60.1

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