micro-models-agent 0.41.2 → 0.43.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 (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +19 -9
  3. package/dist/cli/completer.js +37 -36
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +23 -48
  6. package/dist/cli/repl-commands.js +12 -40
  7. package/dist/cli/repl.js +87 -217
  8. package/dist/cli/security-commands.js +7 -5
  9. package/dist/cli/setup.js +26 -8
  10. package/dist/config/config.js +5 -52
  11. package/dist/config/defaults.js +5 -29
  12. package/dist/config/experts.js +1 -1
  13. package/dist/config/index.js +3 -3
  14. package/dist/config/security.js +10 -3
  15. package/dist/core/agent-moe.js +10 -2
  16. package/dist/core/agent.js +82 -273
  17. package/dist/core/bootstrap.js +13 -80
  18. package/dist/core/index.js +2 -2
  19. package/dist/core/prompt-builder.js +2 -23
  20. package/dist/core/session-logger.js +4 -46
  21. package/dist/i18n/en.json +2 -75
  22. package/dist/i18n/ru.json +1 -74
  23. package/dist/index.js +1 -1
  24. package/dist/llm/image-utils.js +5 -4
  25. package/dist/llm/index.js +4 -4
  26. package/dist/llm/model-loader.js +6 -6
  27. package/dist/llm/openai-compat.js +34 -40
  28. package/dist/llm/orchestrator.js +29 -33
  29. package/dist/llm/response.js +9 -9
  30. package/dist/logger/app-logger.js +1 -1
  31. package/dist/logger/index.js +1 -1
  32. package/dist/main.js +648 -183
  33. package/dist/migration/backup.js +13 -13
  34. package/dist/migration/detect.js +11 -11
  35. package/dist/migration/index.js +2 -2
  36. package/dist/modules/browser/actions.js +4 -34
  37. package/dist/modules/browser/bridge-server.mjs +202 -202
  38. package/dist/modules/browser/cookie-store.js +6 -6
  39. package/dist/modules/browser/index.js +5 -7
  40. package/dist/modules/browser/module.js +7 -8
  41. package/dist/modules/browser/session.js +84 -87
  42. package/dist/modules/browser/snapshot.js +58 -92
  43. package/dist/modules/browser/types.js +1 -4
  44. package/dist/modules/certification/cli.js +4 -2
  45. package/dist/modules/certification/fact-checker.js +3 -1
  46. package/dist/modules/certification/loader.js +9 -3
  47. package/dist/modules/certification/runner.js +4 -1
  48. package/dist/modules/context/index.js +1 -1
  49. package/dist/modules/context/manager.js +86 -160
  50. package/dist/modules/execution/auditor.js +25 -177
  51. package/dist/modules/execution/module.js +544 -201
  52. package/dist/modules/execution/moe-executor.js +0 -25
  53. package/dist/modules/execution/plan-store.js +3 -1
  54. package/dist/modules/execution/plan-validator.js +10 -10
  55. package/dist/modules/execution/planner.js +1 -6
  56. package/dist/modules/execution/stuck-detector.js +10 -173
  57. package/dist/modules/execution/verifier.js +42 -86
  58. package/dist/modules/hallucination/confidence.js +1 -8
  59. package/dist/modules/hallucination/detector.js +5 -2
  60. package/dist/modules/hallucination/factual.js +64 -3
  61. package/dist/modules/hallucination/index.js +1 -1
  62. package/dist/modules/hallucination/js-identifiers.js +0 -190
  63. package/dist/modules/hallucination/llm-judge.js +3 -1
  64. package/dist/modules/indexer/cache.js +7 -9
  65. package/dist/modules/indexer/index.js +3 -3
  66. package/dist/modules/indexer/module.js +42 -95
  67. package/dist/modules/indexer/walker.js +17 -17
  68. package/dist/modules/lsp/client.js +31 -74
  69. package/dist/modules/lsp/config.js +33 -87
  70. package/dist/modules/lsp/index.js +3 -3
  71. package/dist/modules/lsp/module.js +21 -185
  72. package/dist/modules/mcp/module.js +6 -2
  73. package/dist/modules/memory/index.js +1 -1
  74. package/dist/modules/memory/module.js +23 -71
  75. package/dist/modules/memory/search.js +9 -11
  76. package/dist/modules/memory/store.js +13 -13
  77. package/dist/modules/pipelines/engine.js +10 -10
  78. package/dist/modules/pipelines/index.js +3 -3
  79. package/dist/modules/pipelines/parser.js +14 -17
  80. package/dist/modules/pipelines/template.js +1 -1
  81. package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
  82. package/dist/modules/plugins/builtin/notify.js +2 -3
  83. package/dist/modules/plugins/index.js +1 -1
  84. package/dist/modules/plugins/loader.js +17 -59
  85. package/dist/modules/plugins/manager.js +17 -73
  86. package/dist/modules/processes/index.js +1 -1
  87. package/dist/modules/processes/registry.js +46 -135
  88. package/dist/modules/registry.js +2 -4
  89. package/dist/modules/security/audit-notifier.js +39 -39
  90. package/dist/modules/security/command-validator.js +8 -2
  91. package/dist/modules/security/data-sanitizer.js +9 -1
  92. package/dist/modules/security/encryption.js +56 -58
  93. package/dist/modules/security/network-validator.js +9 -1
  94. package/dist/modules/security/path-validator.js +3 -1
  95. package/dist/modules/security/security-policies.js +19 -3
  96. package/dist/modules/security/session-encryption.js +1 -1
  97. package/dist/modules/security/session-isolation.js +8 -8
  98. package/dist/modules/session/index.js +3 -3
  99. package/dist/modules/session/module.js +5 -5
  100. package/dist/modules/session/store.js +9 -3
  101. package/dist/modules/skills/module.js +2 -1
  102. package/dist/modules/updater/checker.js +6 -70
  103. package/dist/modules/updater/index.js +1 -2
  104. package/dist/modules/user-profile/compressor.js +2 -2
  105. package/dist/modules/user-profile/index.js +1 -1
  106. package/dist/modules/user-profile/profile.js +9 -9
  107. package/dist/tools/attach-image.js +1 -1
  108. package/dist/tools/bash.js +19 -178
  109. package/dist/tools/browser.js +29 -46
  110. package/dist/tools/executor.js +5 -4
  111. package/dist/tools/file-info.js +12 -13
  112. package/dist/tools/filter-tools.js +2 -9
  113. package/dist/tools/glob-tool.js +11 -11
  114. package/dist/tools/grep-tool.js +3 -1
  115. package/dist/tools/index.js +2 -13
  116. package/dist/tools/list-dir.js +17 -18
  117. package/dist/tools/load-skill.js +3 -1
  118. package/dist/tools/path-utils.js +4 -4
  119. package/dist/tools/pipeline-run.js +25 -25
  120. package/dist/tools/process-kill.js +11 -11
  121. package/dist/tools/process-list.js +22 -20
  122. package/dist/tools/process-log.js +18 -22
  123. package/dist/tools/question.js +3 -1
  124. package/dist/tools/read-file.js +2 -10
  125. package/dist/tools/recall.js +37 -44
  126. package/dist/tools/registry.js +4 -15
  127. package/dist/tools/remember.js +29 -29
  128. package/dist/tools/scope-check.js +9 -9
  129. package/dist/tools/subagent.js +9 -54
  130. package/dist/tools/user-input.js +1 -1
  131. package/dist/tools/web-browse.js +3 -3
  132. package/dist/tools/web-fetch.js +3 -3
  133. package/dist/tools/web-search.js +3 -3
  134. package/dist/tools/write-file.js +3 -1
  135. package/dist/ui/box.js +5 -1
  136. package/dist/ui/index.js +6 -6
  137. package/dist/ui/md-formatter.js +33 -33
  138. package/dist/ui/output.js +5 -5
  139. package/dist/ui/renderer.js +10 -15
  140. package/dist/ui/table.js +1 -1
  141. package/package.json +48 -48
  142. package/dist/cli/plugin-commands.js +0 -36
  143. package/dist/cli/run-result.js +0 -22
  144. package/dist/core/version.js +0 -24
  145. package/dist/modules/artifacts/store.js +0 -61
  146. package/dist/modules/browser/bridge-client.js +0 -199
  147. package/dist/modules/browser/bridge-path.js +0 -10
  148. package/dist/modules/browser/driver.js +0 -136
  149. package/dist/modules/context/chunk-query.js +0 -100
  150. package/dist/modules/context/fact-extractor.js +0 -162
  151. package/dist/modules/context/history.js +0 -15
  152. package/dist/modules/execution/audit-runners.js +0 -152
  153. package/dist/modules/execution/execution-plugin.js +0 -272
  154. package/dist/modules/execution/plan-tool.js +0 -508
  155. package/dist/modules/execution/windows-commands.js +0 -41
  156. package/dist/modules/indexer/project-profile.js +0 -183
  157. package/dist/modules/lsp/check-tool.js +0 -58
  158. package/dist/modules/lsp/command.js +0 -60
  159. package/dist/modules/lsp/probe.js +0 -76
  160. package/dist/modules/lsp/project-root.js +0 -32
  161. package/dist/modules/lsp/startup-check.js +0 -141
  162. package/dist/modules/processes/detect.js +0 -34
  163. package/dist/modules/skills/matcher.js +0 -27
  164. package/dist/modules/updater/module.js +0 -116
  165. package/dist/tools/chunk-query.js +0 -99
  166. package/dist/tools/download-file.js +0 -116
  167. package/dist/tools/enable-tools.js +0 -58
  168. package/dist/tools/hidden-tools-block.js +0 -37
  169. package/dist/ui/line-editor.js +0 -703
  170. package/dist/ui/line-math.js +0 -69
  171. package/dist/ui/plan-view.js +0 -103
@@ -1,18 +1,15 @@
1
+ import { join } from "path";
1
2
  import { t } from "../i18n/index";
2
3
  import { pc } from "../ui/colors";
3
4
  import { PromptBuilder } from "./prompt-builder";
4
5
  import { processRegistry } from "../modules/processes";
5
6
  import { SessionLogger } from "./session-logger";
6
7
  import { runWithMoE } from "./agent-moe";
8
+ import { MemoryStore } from "../modules/memory/store";
7
9
  import { StepVerifier } from "../modules/execution/verifier";
8
10
  const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
9
11
  const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
10
12
  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
13
  /** True when the text looks like a raw JSON tool payload (garbage to display). */
17
14
  function isToolCallJson(text) {
18
15
  const trimmed = text.trim();
@@ -27,19 +24,6 @@ function isToolCallJson(text) {
27
24
  }
28
25
  return false;
29
26
  }
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
27
  export class Agent {
44
28
  deps;
45
29
  systemPromptAdded = false;
@@ -59,7 +43,8 @@ export class Agent {
59
43
  }
60
44
  }
61
45
  buildSystemPrompt() {
62
- const systemBudget = Math.floor(this.deps.config.contextWindow * this.deps.config.contextBudget.systemPrompt);
46
+ const systemBudget = Math.floor(this.deps.config.contextWindow *
47
+ this.deps.config.contextBudget.systemPrompt);
63
48
  const builder = new PromptBuilder(systemBudget);
64
49
  builder.addBlocks(this.deps.promptBlocks);
65
50
  const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
@@ -79,45 +64,7 @@ export class Agent {
79
64
  if (pluginBlocks.length > 0) {
80
65
  builder.addBlocks(pluginBlocks);
81
66
  }
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
- });
67
+ return builder.build();
121
68
  }
122
69
  getSystemPromptInfo() {
123
70
  const { prompt, excluded } = this.buildSystemPrompt();
@@ -145,26 +92,27 @@ export class Agent {
145
92
  }
146
93
  refreshSystemPrompt() {
147
94
  const { prompt } = this.buildSystemPrompt();
148
- const current = this.deps.contextManager.getActiveHistory().find((m) => m.role === "system");
95
+ const current = this.deps.contextManager
96
+ .getActiveHistory()
97
+ .find((m) => m.role === "system");
149
98
  if (!current || current.content !== prompt) {
150
99
  this.deps.contextManager.updateSystemPrompt?.(prompt);
151
100
  }
152
101
  }
153
- truncateToolOutput(output, budget, currentTokens, bounded) {
154
- const remainingBudget = Math.max(0, budget.history - currentTokens);
155
- const maxChars = toolOutputCharLimit(remainingBudget, budget.history, bounded);
102
+ truncateToolOutput(output, budget, currentTokens) {
103
+ const remainingBudget = budget.history - currentTokens;
104
+ const maxCharsByBudget = Math.floor(remainingBudget * 0.5 * 2);
105
+ const maxCharsByRatio = Math.floor(budget.history * TOOL_RESULT_MAX_TOKENS_RATIO * 2);
106
+ const maxChars = Math.min(maxCharsByBudget, maxCharsByRatio, TOOL_RESULT_ABSOLUTE_MAX_CHARS);
156
107
  if (output.length <= maxChars)
157
108
  return output;
158
109
  const truncated = output.slice(0, maxChars);
159
110
  const removedChars = output.length - maxChars;
160
- return truncated + `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
111
+ return (truncated +
112
+ `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`);
161
113
  }
162
114
  emitPhase(iteration, phase, onPhase) {
163
- this.deps.pluginManager.runOnPhase?.({
164
- iteration,
165
- logger: this.deps.logger,
166
- contextManager: this.deps.contextManager,
167
- }, phase);
115
+ this.deps.pluginManager.runOnPhase?.({ iteration, logger: this.deps.logger }, phase);
168
116
  onPhase?.(phase);
169
117
  }
170
118
  async run(input, onChunk, onMeta, onTool, onPhase) {
@@ -177,10 +125,6 @@ export class Agent {
177
125
  }
178
126
  if (!this.systemPromptAdded &&
179
127
  !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
128
  const { prompt: systemPrompt, excluded } = this.buildSystemPrompt();
185
129
  contextManager.addMessage({ role: "system", content: systemPrompt });
186
130
  this.systemPromptAdded = true;
@@ -191,7 +135,6 @@ export class Agent {
191
135
  pluginManager.runOnSessionStart({
192
136
  logger,
193
137
  sessionManager: sessionManager?.getActiveMeta(),
194
- contextManager,
195
138
  });
196
139
  if (config.session?.baselineCheck !== false) {
197
140
  const verifier = new StepVerifier(baseDir);
@@ -206,13 +149,6 @@ export class Agent {
206
149
  .catch(() => { });
207
150
  }
208
151
  }
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
152
  contextManager.addMessage({ role: "user", content: input });
217
153
  slog.logUser(input);
218
154
  if (config.moe?.enabled) {
@@ -232,12 +168,6 @@ export class Agent {
232
168
  this.abortController = new AbortController();
233
169
  let iteration = 0;
234
170
  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
171
  let hallucinationRetries = 0;
242
172
  let lastToolSignature = "";
243
173
  let apiPromptTokens = 0;
@@ -246,40 +176,21 @@ export class Agent {
246
176
  const MAX_HALLUCINATION_RETRIES = 3;
247
177
  let consecutiveToolFailures = 0;
248
178
  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
179
  let auditRetries = 0;
254
180
  const MAX_AUDIT_RETRIES = 3;
255
181
  let emptyResponseRetries = 0;
256
182
  const MAX_EMPTY_RESPONSE_RETRIES = 2;
257
183
  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
184
  let repeatedToolCount = 0;
265
185
  const MAX_REPEATED_TOOL_CALLS = 2;
266
186
  // 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);
187
+ const allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
188
+ const toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum +
189
+ Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
274
190
  contextManager.setToolTokens(toolTokenEstimate);
275
191
  while (iteration < config.maxToolIterations && !this.shutdownRequested) {
276
192
  iteration++;
277
193
  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
194
  pluginManager.runOnBeforeThink({
284
195
  iteration,
285
196
  logger,
@@ -291,50 +202,27 @@ export class Agent {
291
202
  },
292
203
  });
293
204
  if (contextManager.needsCompaction()) {
294
- const result = contextManager.compact();
295
- if (result) {
296
- logger.debug("Context compacted");
297
- slog.logCompaction({ reason: "interval", iteration, ...result });
298
- }
205
+ contextManager.compact();
206
+ logger.debug("Context compacted");
207
+ slog.logCompaction(`regular compaction, iteration ${iteration}`, iteration);
299
208
  }
300
209
  const currentTokens = contextManager.getEstimatedTokens();
301
210
  const budget = contextManager.getBudget();
302
211
  const quality = contextManager.getQuality();
303
212
  if (quality < QUALITY_TRIGGER_THRESHOLD &&
304
- contextManager.getCompactionCount() > 0 &&
305
- iteration - lastForcedCompactionIteration >= FORCED_COMPACTION_COOLDOWN) {
306
- lastForcedCompactionIteration = iteration;
307
- const result = contextManager.compact();
213
+ contextManager.getCompactionCount() > 0) {
214
+ contextManager.compact();
308
215
  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
- }
216
+ slog.logCompaction(`quality-triggered compaction (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%), iteration ${iteration}`, iteration, currentTokens, budget.history);
316
217
  }
317
218
  if (currentTokens > budget.history) {
318
- const result = contextManager.compact();
219
+ contextManager.compact();
319
220
  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
- }
221
+ slog.logCompaction(`forced compaction (${currentTokens} > ${budget.history}), iteration ${iteration}`, iteration, currentTokens, budget.history);
327
222
  }
328
223
  this.refreshSystemPrompt();
329
224
  const history = contextManager.getActiveHistory();
330
225
  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
226
  let textContent = "";
339
227
  let reasoningContent = "";
340
228
  const toolCalls = [];
@@ -343,8 +231,6 @@ export class Agent {
343
231
  const textChunks = [];
344
232
  this.emitPhase(iteration, "thinking", onPhase);
345
233
  const llmStart = Date.now();
346
- const promptBefore = apiPromptTokens;
347
- const completionBefore = apiCompletionTokens;
348
234
  logger.logLLMRequest(config.model, history.length, input, "agent");
349
235
  try {
350
236
  for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
@@ -360,7 +246,7 @@ export class Agent {
360
246
  if (chunk.type === "reasoning" && chunk.content) {
361
247
  reasoningContent += chunk.content;
362
248
  if (config.showReasoning) {
363
- const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, chunk.content);
249
+ const metaOut = pluginManager.runOnMeta({ iteration, logger }, chunk.content);
364
250
  if (metaOut) {
365
251
  onMeta?.(pc.dim(metaOut));
366
252
  }
@@ -396,7 +282,7 @@ export class Agent {
396
282
  logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
397
283
  logger.error(`LLM call failed: ${err.message}`);
398
284
  slog.logError(err.message);
399
- pluginManager.runOnError({ iteration, logger, contextManager }, err);
285
+ pluginManager.runOnError({ iteration, logger }, err);
400
286
  return {
401
287
  success: false,
402
288
  text: lastText,
@@ -411,26 +297,6 @@ export class Agent {
411
297
  // the backend omits `usage` from the response.
412
298
  apiCompletionChars += (textContent || reasoningContent).length;
413
299
  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
300
  if (this.shutdownRequested) {
435
301
  break;
436
302
  }
@@ -440,10 +306,11 @@ export class Agent {
440
306
  // describing the call. `toolComments: false` restores the old behavior
441
307
  // of suppressing text next to a tool call.
442
308
  const toolComments = this.deps.config.ui?.toolComments ?? true;
443
- const showText = textChunks.length > 0 && (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
309
+ const showText = textChunks.length > 0 &&
310
+ (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
444
311
  if (showText) {
445
312
  for (const chunk of textChunks) {
446
- const textOut = pluginManager.runOnText({ iteration, logger, contextManager }, chunk);
313
+ const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
447
314
  onChunk?.(textOut);
448
315
  }
449
316
  }
@@ -457,7 +324,7 @@ export class Agent {
457
324
  else if (reasoningContent) {
458
325
  llmResponse = { type: "reasoning", content: reasoningContent };
459
326
  }
460
- pluginManager.runOnAfterThink({ iteration, logger, contextManager }, llmResponse);
327
+ pluginManager.runOnAfterThink({ iteration, logger }, llmResponse);
461
328
  if (this.deps.exitOnComplete && sawToolCall) {
462
329
  const signature = toolCalls
463
330
  .map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
@@ -504,7 +371,7 @@ export class Agent {
504
371
  toolName: call.name,
505
372
  args: call.arguments,
506
373
  });
507
- pluginManager.runOnToolStart({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments });
374
+ pluginManager.runOnToolStart({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments });
508
375
  onTool?.({ type: "start", tool: call.name, args: call.arguments });
509
376
  slog.logToolCall(call, iteration);
510
377
  const tokensBeforeTool = contextManager.getEstimatedTokens();
@@ -515,18 +382,22 @@ export class Agent {
515
382
  if (result.success && call.arguments.path) {
516
383
  const filePath = String(call.arguments.path);
517
384
  if (call.name === "write_file" || call.name === "edit_file") {
518
- hallucinationDetector.getConsistencyCheck().trackCreatedFile(filePath);
385
+ hallucinationDetector
386
+ .getConsistencyCheck()
387
+ .trackCreatedFile(filePath);
519
388
  }
520
389
  else if (call.name === "delete_file") {
521
- hallucinationDetector.getConsistencyCheck().trackDeletedFile(filePath);
390
+ hallucinationDetector
391
+ .getConsistencyCheck()
392
+ .trackDeletedFile(filePath);
522
393
  }
523
394
  }
524
- pluginManager.runOnToolEnd({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
395
+ pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
525
396
  if (result.display) {
526
397
  onMeta?.("\n" + result.display + "\n");
527
398
  }
528
399
  else {
529
- const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
400
+ const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
530
401
  onMeta?.("\n" + pc.dim(metaOut) + "\n");
531
402
  }
532
403
  if (result.diff) {
@@ -534,7 +405,7 @@ export class Agent {
534
405
  }
535
406
  const currentTokens = contextManager.getEstimatedTokens();
536
407
  const budget = contextManager.getBudget();
537
- const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens, boundedToolNames.has(call.name));
408
+ const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens);
538
409
  contextManager.addMessage({
539
410
  role: "tool",
540
411
  content: truncatedOutput,
@@ -557,35 +428,8 @@ export class Agent {
557
428
  slog.logToolResult(call, result, duration, iteration);
558
429
  }
559
430
  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
- }
431
+ contextManager.compact();
432
+ logger.debug("Context compacted after tool result");
589
433
  }
590
434
  }
591
435
  if (anyToolFailed) {
@@ -605,9 +449,13 @@ export class Agent {
605
449
  role: "user",
606
450
  content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
607
451
  });
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");
452
+ if (sessionManager) {
453
+ const activeSession = sessionManager.getActiveMeta();
454
+ if (activeSession) {
455
+ const memDir = join(baseDir, ".mma", "memory");
456
+ const memStore = new MemoryStore(memDir);
457
+ 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");
458
+ }
611
459
  }
612
460
  }
613
461
  contextManager.addMessage({
@@ -635,17 +483,9 @@ export class Agent {
635
483
  onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
636
484
  }
637
485
  }
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
486
  continue;
642
487
  }
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;
488
+ hallucinationDetector.getConfidenceCheck().setPreviousResponse(lastText);
649
489
  const hallucinationResult = await hallucinationDetector.validate(textContent);
650
490
  if (hallucinationResult.status === "block") {
651
491
  logger.warn(`Response blocked: ${hallucinationResult.reason}`);
@@ -677,55 +517,34 @@ export class Agent {
677
517
  lastText = textContent;
678
518
  break;
679
519
  }
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).
520
+ // NOTE: with exitOnComplete and an EMPTY text we deliberately do NOT
521
+ // break the model produced no usable answer yet (same case as the
522
+ // empty-response guard below). Falling through to the retry path
523
+ // keeps us from finishing with text: "".
524
+ if (hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
525
+ logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
526
+ return {
527
+ success: false,
528
+ text: lastText,
529
+ error: t("error.response_blocked", {
530
+ reason: t("hall.max_retries_exhausted"),
531
+ }),
532
+ iterationCount: iteration,
533
+ };
698
534
  }
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
- }
535
+ hallucinationRetries++;
536
+ logger.warn(`Hallucination retry (${hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${hallucinationResult.reason}`);
537
+ if (textContent) {
723
538
  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>`,
539
+ role: "assistant",
540
+ content: textContent,
726
541
  });
727
- continue;
728
542
  }
543
+ contextManager.addMessage({
544
+ role: "user",
545
+ 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>`,
546
+ });
547
+ continue;
729
548
  }
730
549
  if (textContent) {
731
550
  contextManager.addMessage({ role: "assistant", content: textContent });
@@ -777,16 +596,11 @@ export class Agent {
777
596
  });
778
597
  slog.logAudit(audit.summary, iteration);
779
598
  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;
599
+ if (auditRetries >= MAX_AUDIT_RETRIES ||
600
+ iteration >= config.maxToolIterations - 1) {
601
+ logger.warn(`Final audit still incomplete after ${auditRetries} retries — finishing anyway`);
784
602
  break;
785
603
  }
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
604
  continue;
791
605
  }
792
606
  }
@@ -812,13 +626,9 @@ export class Agent {
812
626
  };
813
627
  }
814
628
  return {
815
- success: emptyResponseExhausted || auditFailed ? false : true,
629
+ success: emptyResponseExhausted ? false : true,
816
630
  text: lastText,
817
- error: emptyResponseExhausted
818
- ? t("error.empty_response")
819
- : auditFailed
820
- ? t("error.audit_failed", { summary: lastAuditSummary })
821
- : undefined,
631
+ error: emptyResponseExhausted ? t("error.empty_response") : undefined,
822
632
  iterationCount: iteration,
823
633
  contextUsed: tokensUsed,
824
634
  contextLimit: budget.history,
@@ -880,7 +690,6 @@ export class Agent {
880
690
  pluginManager.runOnSessionEnd({
881
691
  logger,
882
692
  sessionManager: sessionManager?.getActiveMeta(),
883
- contextManager,
884
693
  });
885
694
  }
886
695
  }