micro-models-agent 0.43.3 → 0.45.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 (188) hide show
  1. package/README.md +312 -312
  2. package/bin/mma.mjs +41 -41
  3. package/dist/main.js +333 -66
  4. package/dist/skills/builtin/git.md +36 -36
  5. package/dist/skills/builtin/typescript.md +35 -35
  6. package/package.json +1 -1
  7. package/dist/cli/commands.js +0 -333
  8. package/dist/cli/completer.js +0 -168
  9. package/dist/cli/index.js +0 -2
  10. package/dist/cli/main.js +0 -140
  11. package/dist/cli/repl-commands.js +0 -633
  12. package/dist/cli/repl.js +0 -486
  13. package/dist/cli/security-commands.js +0 -166
  14. package/dist/cli/setup.js +0 -249
  15. package/dist/config/config.js +0 -202
  16. package/dist/config/defaults.js +0 -100
  17. package/dist/config/experts.js +0 -15
  18. package/dist/config/index.js +0 -3
  19. package/dist/config/security.js +0 -200
  20. package/dist/config/types.js +0 -1
  21. package/dist/core/agent-moe.js +0 -110
  22. package/dist/core/agent.js +0 -695
  23. package/dist/core/bootstrap.js +0 -337
  24. package/dist/core/index.js +0 -2
  25. package/dist/core/prompt-builder.js +0 -55
  26. package/dist/core/session-logger.js +0 -155
  27. package/dist/core/types.js +0 -1
  28. package/dist/core/workspace.js +0 -76
  29. package/dist/i18n/en.json +0 -525
  30. package/dist/i18n/index.js +0 -46
  31. package/dist/i18n/ru.json +0 -525
  32. package/dist/index.js +0 -22
  33. package/dist/llm/image-utils.js +0 -144
  34. package/dist/llm/index.js +0 -4
  35. package/dist/llm/model-loader.js +0 -78
  36. package/dist/llm/openai-compat.js +0 -353
  37. package/dist/llm/orchestrator.js +0 -194
  38. package/dist/llm/provider.js +0 -10
  39. package/dist/llm/response.js +0 -39
  40. package/dist/llm/token-counter.js +0 -39
  41. package/dist/llm/types.js +0 -1
  42. package/dist/logger/app-logger.js +0 -143
  43. package/dist/logger/file-log.js +0 -151
  44. package/dist/logger/index.js +0 -1
  45. package/dist/migration/backup.js +0 -45
  46. package/dist/migration/detect.js +0 -50
  47. package/dist/migration/index.js +0 -2
  48. package/dist/modules/browser/actions.js +0 -46
  49. package/dist/modules/browser/cookie-store.js +0 -24
  50. package/dist/modules/browser/index.js +0 -5
  51. package/dist/modules/browser/module.js +0 -28
  52. package/dist/modules/browser/session.js +0 -335
  53. package/dist/modules/browser/snapshot.js +0 -114
  54. package/dist/modules/browser/types.js +0 -9
  55. package/dist/modules/certification/cli.js +0 -176
  56. package/dist/modules/certification/fact-checker.js +0 -84
  57. package/dist/modules/certification/loader.js +0 -111
  58. package/dist/modules/certification/manifest.js +0 -50
  59. package/dist/modules/certification/runner.js +0 -162
  60. package/dist/modules/certification/scenarios.js +0 -124
  61. package/dist/modules/certification/types.js +0 -1
  62. package/dist/modules/context/index.js +0 -1
  63. package/dist/modules/context/manager.js +0 -349
  64. package/dist/modules/execution/auditor.js +0 -66
  65. package/dist/modules/execution/index.js +0 -8
  66. package/dist/modules/execution/module.js +0 -779
  67. package/dist/modules/execution/moe-executor.js +0 -266
  68. package/dist/modules/execution/plan-coverage.js +0 -68
  69. package/dist/modules/execution/plan-persister.js +0 -46
  70. package/dist/modules/execution/plan-store.js +0 -159
  71. package/dist/modules/execution/plan-validator.js +0 -153
  72. package/dist/modules/execution/planner.js +0 -85
  73. package/dist/modules/execution/stuck-detector.js +0 -347
  74. package/dist/modules/execution/tracker.js +0 -67
  75. package/dist/modules/execution/types.js +0 -1
  76. package/dist/modules/execution/verifier.js +0 -178
  77. package/dist/modules/hallucination/confidence.js +0 -59
  78. package/dist/modules/hallucination/consistency.js +0 -26
  79. package/dist/modules/hallucination/detector.js +0 -46
  80. package/dist/modules/hallucination/factual.js +0 -190
  81. package/dist/modules/hallucination/index.js +0 -5
  82. package/dist/modules/hallucination/js-identifiers.js +0 -72
  83. package/dist/modules/hallucination/llm-judge.js +0 -103
  84. package/dist/modules/index.js +0 -5
  85. package/dist/modules/indexer/cache.js +0 -38
  86. package/dist/modules/indexer/index.js +0 -3
  87. package/dist/modules/indexer/module.js +0 -192
  88. package/dist/modules/indexer/walker.js +0 -101
  89. package/dist/modules/lsp/client.js +0 -235
  90. package/dist/modules/lsp/config.js +0 -81
  91. package/dist/modules/lsp/index.js +0 -3
  92. package/dist/modules/lsp/module.js +0 -68
  93. package/dist/modules/lsp/types.js +0 -1
  94. package/dist/modules/mcp/client.js +0 -399
  95. package/dist/modules/mcp/index.js +0 -3
  96. package/dist/modules/mcp/module.js +0 -146
  97. package/dist/modules/mcp/registry.js +0 -15
  98. package/dist/modules/memory/index.js +0 -1
  99. package/dist/modules/memory/module.js +0 -48
  100. package/dist/modules/memory/search.js +0 -40
  101. package/dist/modules/memory/store.js +0 -69
  102. package/dist/modules/pipelines/engine.js +0 -60
  103. package/dist/modules/pipelines/index.js +0 -3
  104. package/dist/modules/pipelines/parser.js +0 -53
  105. package/dist/modules/pipelines/template.js +0 -14
  106. package/dist/modules/plugins/builtin/lint-on-write.js +0 -226
  107. package/dist/modules/plugins/builtin/notify.js +0 -8
  108. package/dist/modules/plugins/index.js +0 -1
  109. package/dist/modules/plugins/loader.js +0 -28
  110. package/dist/modules/plugins/manager.js +0 -161
  111. package/dist/modules/plugins/types.js +0 -1
  112. package/dist/modules/processes/index.js +0 -2
  113. package/dist/modules/processes/registry.js +0 -238
  114. package/dist/modules/processes/runner.js +0 -23
  115. package/dist/modules/registry.js +0 -45
  116. package/dist/modules/security/audit-log.js +0 -136
  117. package/dist/modules/security/audit-notifier.js +0 -292
  118. package/dist/modules/security/command-validator.js +0 -211
  119. package/dist/modules/security/content-scanner.js +0 -53
  120. package/dist/modules/security/data-sanitizer.js +0 -97
  121. package/dist/modules/security/encryption.js +0 -240
  122. package/dist/modules/security/index.js +0 -14
  123. package/dist/modules/security/network-validator.js +0 -79
  124. package/dist/modules/security/path-validator.js +0 -209
  125. package/dist/modules/security/rate-limiter.js +0 -119
  126. package/dist/modules/security/security-policies.js +0 -547
  127. package/dist/modules/security/session-encryption.js +0 -210
  128. package/dist/modules/security/session-isolation.js +0 -95
  129. package/dist/modules/session/index.js +0 -3
  130. package/dist/modules/session/manager.js +0 -172
  131. package/dist/modules/session/module.js +0 -24
  132. package/dist/modules/session/store.js +0 -228
  133. package/dist/modules/session/types.js +0 -1
  134. package/dist/modules/skills/index.js +0 -2
  135. package/dist/modules/skills/loader.js +0 -72
  136. package/dist/modules/skills/module.js +0 -130
  137. package/dist/modules/types.js +0 -1
  138. package/dist/modules/updater/checker.js +0 -32
  139. package/dist/modules/updater/index.js +0 -1
  140. package/dist/modules/user-profile/compressor.js +0 -16
  141. package/dist/modules/user-profile/index.js +0 -1
  142. package/dist/modules/user-profile/profile.js +0 -68
  143. package/dist/tools/approve.js +0 -32
  144. package/dist/tools/attach-image.js +0 -89
  145. package/dist/tools/bash.js +0 -337
  146. package/dist/tools/browser.js +0 -97
  147. package/dist/tools/create-dir.js +0 -55
  148. package/dist/tools/delete-file.js +0 -62
  149. package/dist/tools/edit-file.js +0 -79
  150. package/dist/tools/executor.js +0 -145
  151. package/dist/tools/file-info.js +0 -45
  152. package/dist/tools/filter-tools.js +0 -10
  153. package/dist/tools/glob-tool.js +0 -26
  154. package/dist/tools/grep-tool.js +0 -86
  155. package/dist/tools/index.js +0 -67
  156. package/dist/tools/list-dir.js +0 -47
  157. package/dist/tools/load-skill.js +0 -44
  158. package/dist/tools/mcp-call.js +0 -68
  159. package/dist/tools/move-file.js +0 -85
  160. package/dist/tools/path-utils.js +0 -51
  161. package/dist/tools/pipeline-run.js +0 -144
  162. package/dist/tools/preview.js +0 -2
  163. package/dist/tools/process-kill.js +0 -29
  164. package/dist/tools/process-list.js +0 -38
  165. package/dist/tools/process-log.js +0 -41
  166. package/dist/tools/question.js +0 -142
  167. package/dist/tools/read-file.js +0 -83
  168. package/dist/tools/recall.js +0 -110
  169. package/dist/tools/registry.js +0 -36
  170. package/dist/tools/remember.js +0 -67
  171. package/dist/tools/scope-check.js +0 -30
  172. package/dist/tools/search-history.js +0 -84
  173. package/dist/tools/subagent.js +0 -151
  174. package/dist/tools/types.js +0 -1
  175. package/dist/tools/user-input.js +0 -123
  176. package/dist/tools/web-browse.js +0 -86
  177. package/dist/tools/web-fetch.js +0 -98
  178. package/dist/tools/web-search.js +0 -78
  179. package/dist/tools/write-file.js +0 -83
  180. package/dist/ui/box.js +0 -81
  181. package/dist/ui/colors.js +0 -4
  182. package/dist/ui/diff.js +0 -178
  183. package/dist/ui/index.js +0 -6
  184. package/dist/ui/md-formatter.js +0 -212
  185. package/dist/ui/output.js +0 -13
  186. package/dist/ui/renderer.js +0 -204
  187. package/dist/ui/spinner.js +0 -70
  188. package/dist/ui/table.js +0 -144
@@ -1,695 +0,0 @@
1
- import { join } from "path";
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 { MemoryStore } from "../modules/memory/store";
9
- import { StepVerifier } from "../modules/execution/verifier";
10
- const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
11
- const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
12
- const QUALITY_TRIGGER_THRESHOLD = 40;
13
- /** True when the text looks like a raw JSON tool payload (garbage to display). */
14
- function isToolCallJson(text) {
15
- const trimmed = text.trim();
16
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
17
- try {
18
- JSON.parse(trimmed);
19
- return true;
20
- }
21
- catch {
22
- return false;
23
- }
24
- }
25
- return false;
26
- }
27
- export class Agent {
28
- deps;
29
- systemPromptAdded = false;
30
- shutdownRequested = false;
31
- abortController = null;
32
- lastCompactionShown = 0;
33
- constructor(deps) {
34
- this.deps = deps;
35
- }
36
- /** Expose context manager for REPL image attachment and other direct access. */
37
- get contextManager() {
38
- return this.deps.contextManager;
39
- }
40
- setScope() {
41
- if (this.deps.scope) {
42
- this.deps.toolExecutor.setScope(this.deps.scope);
43
- }
44
- }
45
- buildSystemPrompt() {
46
- const systemBudget = Math.floor(this.deps.config.contextWindow *
47
- this.deps.config.contextBudget.systemPrompt);
48
- const builder = new PromptBuilder(systemBudget);
49
- builder.addBlocks(this.deps.promptBlocks);
50
- const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
51
- if (dynamic.length > 0) {
52
- builder.addBlocks(dynamic);
53
- }
54
- const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? []).flatMap((content) => content && content.trim() !== ""
55
- ? [
56
- {
57
- content,
58
- priority: "low",
59
- essential: false,
60
- estimatedTokens: this.deps.llmProvider.countTokens(content),
61
- },
62
- ]
63
- : []);
64
- if (pluginBlocks.length > 0) {
65
- builder.addBlocks(pluginBlocks);
66
- }
67
- return builder.build();
68
- }
69
- getSystemPromptInfo() {
70
- const { prompt, excluded } = this.buildSystemPrompt();
71
- const tokenCount = this.deps.llmProvider.countTokens(prompt);
72
- return { text: prompt, tokenCount, excluded };
73
- }
74
- /**
75
- * Some OpenAI-compatible backends (llama.cpp) omit `usage` from responses,
76
- * leaving apiPromptTokens/apiCompletionTokens at 0. Fall back to local
77
- * estimates so JSON results still carry meaningful token metrics.
78
- */
79
- resolveUsageTokens(apiPromptTokens, apiCompletionTokens, estimatedPromptTokens, completionChars) {
80
- if (apiPromptTokens > 0 || apiCompletionTokens > 0) {
81
- return {
82
- prompt: apiPromptTokens,
83
- completion: apiCompletionTokens,
84
- total: apiPromptTokens + apiCompletionTokens,
85
- };
86
- }
87
- // ~4 chars per token is a reasonable heuristic when the backend gives
88
- // us nothing (matches the pre-tiktoken fallback elsewhere in the code).
89
- const prompt = Math.max(1, estimatedPromptTokens);
90
- const completion = Math.max(0, Math.ceil(completionChars / 4));
91
- return { prompt, completion, total: prompt + completion };
92
- }
93
- refreshSystemPrompt() {
94
- const { prompt } = this.buildSystemPrompt();
95
- const current = this.deps.contextManager
96
- .getActiveHistory()
97
- .find((m) => m.role === "system");
98
- if (!current || current.content !== prompt) {
99
- this.deps.contextManager.updateSystemPrompt?.(prompt);
100
- }
101
- }
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);
107
- if (output.length <= maxChars)
108
- return output;
109
- const truncated = output.slice(0, maxChars);
110
- const removedChars = output.length - maxChars;
111
- return (truncated +
112
- `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`);
113
- }
114
- emitPhase(iteration, phase, onPhase) {
115
- this.deps.pluginManager.runOnPhase?.({ iteration, logger: this.deps.logger }, phase);
116
- onPhase?.(phase);
117
- }
118
- async run(input, onChunk, onMeta, onTool, onPhase) {
119
- this.setScope();
120
- const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
121
- const slog = new SessionLogger(sessionManager, logger);
122
- if (sessionManager && !sessionManager.getActive()) {
123
- sessionManager.create();
124
- logger.debug(`Session started: ${sessionManager.getActive()}`);
125
- }
126
- if (!this.systemPromptAdded &&
127
- !contextManager.getActiveHistory().some((m) => m.role === "system")) {
128
- const { prompt: systemPrompt, excluded } = this.buildSystemPrompt();
129
- contextManager.addMessage({ role: "system", content: systemPrompt });
130
- this.systemPromptAdded = true;
131
- slog.logSystem(systemPrompt.slice(0, 2000));
132
- if (excluded.length > 0) {
133
- slog.logSystem(`[Excluded prompt blocks: ${excluded.length}]`);
134
- }
135
- pluginManager.runOnSessionStart({
136
- logger,
137
- sessionManager: sessionManager?.getActiveMeta(),
138
- });
139
- if (config.session?.baselineCheck !== false) {
140
- const verifier = new StepVerifier(baseDir);
141
- verifier
142
- .runTypeCheck()
143
- .then((tc) => {
144
- if (!tc.passed) {
145
- logger.warn(`Baseline typecheck has issues: ${tc.message?.slice(0, 500)}`);
146
- onMeta?.(pc.yellow(`\n⚠ Baseline typecheck has issues\n`));
147
- }
148
- })
149
- .catch(() => { });
150
- }
151
- }
152
- contextManager.addMessage({ role: "user", content: input });
153
- slog.logUser(input);
154
- if (config.moe?.enabled) {
155
- return runWithMoE({
156
- config,
157
- llmProvider,
158
- toolExecutor,
159
- logger,
160
- baseDir: this.deps.baseDir,
161
- }, input, () => this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase), { onMeta, onTool, onPhase });
162
- }
163
- return this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase);
164
- }
165
- async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
166
- const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, baseDir, } = this.deps;
167
- const slog = new SessionLogger(sessionManager, logger);
168
- this.abortController = new AbortController();
169
- let iteration = 0;
170
- let lastText = "";
171
- let hallucinationRetries = 0;
172
- let lastToolSignature = "";
173
- let apiPromptTokens = 0;
174
- let apiCompletionTokens = 0;
175
- let apiCompletionChars = 0;
176
- const MAX_HALLUCINATION_RETRIES = 3;
177
- let consecutiveToolFailures = 0;
178
- const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
179
- let auditRetries = 0;
180
- const MAX_AUDIT_RETRIES = 3;
181
- let emptyResponseRetries = 0;
182
- const MAX_EMPTY_RESPONSE_RETRIES = 2;
183
- let emptyResponseExhausted = false;
184
- let repeatedToolCount = 0;
185
- const MAX_REPEATED_TOOL_CALLS = 2;
186
- // Account for tool definitions in context budget (they're sent via body.tools, not messages)
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);
190
- contextManager.setToolTokens(toolTokenEstimate);
191
- while (iteration < config.maxToolIterations && !this.shutdownRequested) {
192
- iteration++;
193
- contextManager.noteIteration();
194
- pluginManager.runOnBeforeThink({
195
- iteration,
196
- logger,
197
- lastUserMessage: input,
198
- contextManager,
199
- onMeta,
200
- sessionLog: {
201
- plan: (event, detail, iter) => slog.logPlan(event, detail, iter),
202
- },
203
- });
204
- if (contextManager.needsCompaction()) {
205
- contextManager.compact();
206
- logger.debug("Context compacted");
207
- slog.logCompaction(`regular compaction, iteration ${iteration}`, iteration);
208
- }
209
- const currentTokens = contextManager.getEstimatedTokens();
210
- const budget = contextManager.getBudget();
211
- const quality = contextManager.getQuality();
212
- if (quality < QUALITY_TRIGGER_THRESHOLD &&
213
- contextManager.getCompactionCount() > 0) {
214
- contextManager.compact();
215
- logger.warn(`Low context quality (${quality}%) — forced compaction`);
216
- slog.logCompaction(`quality-triggered compaction (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%), iteration ${iteration}`, iteration, currentTokens, budget.history);
217
- }
218
- if (currentTokens > budget.history) {
219
- contextManager.compact();
220
- logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
221
- slog.logCompaction(`forced compaction (${currentTokens} > ${budget.history}), iteration ${iteration}`, iteration, currentTokens, budget.history);
222
- }
223
- this.refreshSystemPrompt();
224
- const history = contextManager.getActiveHistory();
225
- slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
226
- let textContent = "";
227
- let reasoningContent = "";
228
- const toolCalls = [];
229
- let sawToolCall = false;
230
- let emittedReasoning = false;
231
- const textChunks = [];
232
- this.emitPhase(iteration, "thinking", onPhase);
233
- const llmStart = Date.now();
234
- logger.logLLMRequest(config.model, history.length, input, "agent");
235
- try {
236
- for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
237
- if (this.shutdownRequested)
238
- break;
239
- if (chunk.type === "text" && chunk.content) {
240
- if (emittedReasoning && !textContent) {
241
- onMeta?.("\n\n");
242
- }
243
- textContent += chunk.content;
244
- textChunks.push(chunk.content);
245
- }
246
- if (chunk.type === "reasoning" && chunk.content) {
247
- reasoningContent += chunk.content;
248
- if (config.showReasoning) {
249
- const metaOut = pluginManager.runOnMeta({ iteration, logger }, chunk.content);
250
- if (metaOut) {
251
- onMeta?.(pc.dim(metaOut));
252
- }
253
- emittedReasoning = true;
254
- }
255
- }
256
- if (chunk.type === "tool_call" && chunk.toolCall) {
257
- sawToolCall = true;
258
- let parsedArgs;
259
- try {
260
- parsedArgs = JSON.parse(chunk.toolCall.arguments);
261
- }
262
- catch {
263
- parsedArgs = {};
264
- }
265
- toolCalls.push({
266
- id: chunk.toolCall.id,
267
- name: chunk.toolCall.name,
268
- arguments: parsedArgs,
269
- });
270
- }
271
- if (chunk.type === "done" && chunk.usage) {
272
- apiPromptTokens += chunk.usage.promptTokens;
273
- apiCompletionTokens += chunk.usage.completionTokens;
274
- }
275
- }
276
- }
277
- catch (err) {
278
- if (this.shutdownRequested || err?.name === "AbortError") {
279
- logger.info("LLM call aborted (interrupt)");
280
- break;
281
- }
282
- logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
283
- logger.error(`LLM call failed: ${err.message}`);
284
- slog.logError(err.message);
285
- pluginManager.runOnError({ iteration, logger }, err);
286
- return {
287
- success: false,
288
- text: lastText,
289
- error: t("error.llm", { message: err.message }),
290
- iterationCount: iteration,
291
- };
292
- }
293
- finally {
294
- this.emitPhase(iteration, "done", onPhase);
295
- }
296
- // Track response length so token metrics stay meaningful even when
297
- // the backend omits `usage` from the response.
298
- apiCompletionChars += (textContent || reasoningContent).length;
299
- logger.logLLMResponse(config.model, (textContent || reasoningContent).length, Date.now() - llmStart, undefined, "agent");
300
- if (this.shutdownRequested) {
301
- break;
302
- }
303
- // Show the model's commentary text. When a tool call accompanies the
304
- // response, keep the text too (opencode-like narration), unless it is
305
- // a raw JSON payload that small models sometimes emit instead of
306
- // describing the call. `toolComments: false` restores the old behavior
307
- // of suppressing text next to a tool call.
308
- const toolComments = this.deps.config.ui?.toolComments ?? true;
309
- const showText = textChunks.length > 0 &&
310
- (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
311
- if (showText) {
312
- for (const chunk of textChunks) {
313
- const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
314
- onChunk?.(textOut);
315
- }
316
- }
317
- let llmResponse = null;
318
- if (sawToolCall) {
319
- llmResponse = { type: "tool_call", calls: toolCalls };
320
- }
321
- else if (textContent) {
322
- llmResponse = { type: "text", content: textContent };
323
- }
324
- else if (reasoningContent) {
325
- llmResponse = { type: "reasoning", content: reasoningContent };
326
- }
327
- pluginManager.runOnAfterThink({ iteration, logger }, llmResponse);
328
- if (this.deps.exitOnComplete && sawToolCall) {
329
- const signature = toolCalls
330
- .map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
331
- .join("|");
332
- if (signature && signature === lastToolSignature) {
333
- // A repeated identical tool call is often the model re-running
334
- // a command after a confusing result. Give it one more chance
335
- // to produce a final text answer instead of stopping with
336
- // text: "" (observed on 08-r4: bash re-run → empty result).
337
- repeatedToolCount++;
338
- if (repeatedToolCount >= MAX_REPEATED_TOOL_CALLS) {
339
- logger.debug("Exit-on-complete: repeated identical tool call, stopping");
340
- break;
341
- }
342
- contextManager.addMessage({
343
- role: "user",
344
- 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>`,
345
- });
346
- }
347
- lastToolSignature = signature;
348
- }
349
- if (sawToolCall) {
350
- slog.logAssistant(textContent || "", reasoningContent, toolCalls, iteration);
351
- }
352
- if (sawToolCall) {
353
- contextManager.addMessage({
354
- role: "assistant",
355
- content: textContent || "",
356
- tool_calls: toolCalls.map((tc) => ({
357
- id: tc.id,
358
- type: "function",
359
- function: {
360
- name: tc.name,
361
- arguments: JSON.stringify(tc.arguments),
362
- },
363
- })),
364
- });
365
- const summaries = [];
366
- let anyToolFailed = false;
367
- for (const call of toolCalls) {
368
- this.setScope();
369
- const startTime = Date.now();
370
- pluginManager.runOnToolCall({
371
- toolName: call.name,
372
- args: call.arguments,
373
- });
374
- pluginManager.runOnToolStart({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments });
375
- onTool?.({ type: "start", tool: call.name, args: call.arguments });
376
- slog.logToolCall(call, iteration);
377
- const tokensBeforeTool = contextManager.getEstimatedTokens();
378
- const result = await toolExecutor.execute(call, this.abortController?.signal);
379
- const duration = Date.now() - startTime;
380
- if (!result.success)
381
- anyToolFailed = true;
382
- if (result.success && call.arguments.path) {
383
- const filePath = String(call.arguments.path);
384
- if (call.name === "write_file" || call.name === "edit_file") {
385
- hallucinationDetector
386
- .getConsistencyCheck()
387
- .trackCreatedFile(filePath);
388
- }
389
- else if (call.name === "delete_file") {
390
- hallucinationDetector
391
- .getConsistencyCheck()
392
- .trackDeletedFile(filePath);
393
- }
394
- }
395
- pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
396
- if (result.display) {
397
- onMeta?.("\n" + result.display + "\n");
398
- }
399
- else {
400
- const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
401
- onMeta?.("\n" + pc.dim(metaOut) + "\n");
402
- }
403
- if (result.diff) {
404
- onMeta?.("\n" + result.diff + "\n");
405
- }
406
- const currentTokens = contextManager.getEstimatedTokens();
407
- const budget = contextManager.getBudget();
408
- const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens);
409
- contextManager.addMessage({
410
- role: "tool",
411
- content: truncatedOutput,
412
- name: call.name,
413
- tool_call_id: call.id,
414
- success: result.success,
415
- arguments: call.arguments,
416
- });
417
- const tokensAfterTool = contextManager.getEstimatedTokens();
418
- onTool?.({
419
- type: "end",
420
- tool: call.name,
421
- args: call.arguments,
422
- duration,
423
- error: !result.success,
424
- ctxDelta: tokensAfterTool - tokensBeforeTool,
425
- });
426
- summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
427
- if (config.session.autoSave) {
428
- slog.logToolResult(call, result, duration, iteration);
429
- }
430
- if (contextManager.needsCompaction()) {
431
- contextManager.compact();
432
- logger.debug("Context compacted after tool result");
433
- }
434
- }
435
- if (anyToolFailed) {
436
- consecutiveToolFailures++;
437
- }
438
- else {
439
- consecutiveToolFailures = 0;
440
- }
441
- if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
442
- const recoveryMsg = t("exec.consecutive_failures_recovery", {
443
- count: consecutiveToolFailures,
444
- });
445
- logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
446
- const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
447
- const taskReminder = t("exec.task_reminder", { task: taskSnippet });
448
- contextManager.addMessage({
449
- role: "user",
450
- content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
451
- });
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
- }
459
- }
460
- }
461
- contextManager.addMessage({
462
- role: "user",
463
- content: `<system-summary>${summaries.join("\n")}</system-summary>`,
464
- });
465
- const ui = this.deps.config.ui;
466
- if (ui?.showContextStats) {
467
- const ctxTokens = contextManager.getEstimatedTokens();
468
- const ctxBudget = contextManager.getBudget();
469
- const ctxPct = Math.min(100, Math.round((ctxTokens / ctxBudget.history) * 100));
470
- const barLen = 10;
471
- const filled = Math.round((ctxPct / 100) * barLen);
472
- const ctxBar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
473
- const pctColor = ctxPct >= 75 ? pc.yellow : pc.dim;
474
- const compCount = contextManager.getCompactionCount();
475
- const quality = contextManager.getQuality();
476
- const qualityColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
477
- onMeta?.(`\n ${ctxBar} ${pctColor(`${ctxPct}%`)} ${pc.dim(`ctx: ${ctxTokens}/${ctxBudget.history}`)} ${pc.dim(`compactions: ${compCount}`)} ${qualityColor(`quality: ${quality}%`)}\n`);
478
- }
479
- else if (ui?.showCompaction) {
480
- const compCount = contextManager.getCompactionCount();
481
- if (compCount > this.lastCompactionShown) {
482
- this.lastCompactionShown = compCount;
483
- onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
484
- }
485
- }
486
- continue;
487
- }
488
- hallucinationDetector.getConfidenceCheck().setPreviousResponse(lastText);
489
- const hallucinationResult = await hallucinationDetector.validate(textContent);
490
- if (hallucinationResult.status === "block") {
491
- logger.warn(`Response blocked: ${hallucinationResult.reason}`);
492
- return {
493
- success: false,
494
- text: lastText,
495
- error: t("error.response_blocked", {
496
- reason: hallucinationResult.reason || "",
497
- }),
498
- iterationCount: iteration,
499
- };
500
- }
501
- if (hallucinationResult.status === "warn") {
502
- logger.warn(`Hallucination warning: ${hallucinationResult.reason}`);
503
- const warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
504
- if (onMeta) {
505
- onMeta(`\n${pc.yellow(warnLine)}\n`);
506
- }
507
- else if (onChunk) {
508
- onChunk(`\n${warnLine}\n`);
509
- }
510
- }
511
- if (hallucinationResult.status === "retry") {
512
- if (this.deps.exitOnComplete && textContent?.trim()) {
513
- logger.debug("Exit-on-complete: stopping on first response");
514
- // Save the response BEFORE breaking — lastText is still the
515
- // previous (tool-only) iteration's text, so without this the
516
- // final answer is lost (reported text: "").
517
- lastText = textContent;
518
- break;
519
- }
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
- };
534
- }
535
- hallucinationRetries++;
536
- logger.warn(`Hallucination retry (${hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${hallucinationResult.reason}`);
537
- if (textContent) {
538
- contextManager.addMessage({
539
- role: "assistant",
540
- content: textContent,
541
- });
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;
548
- }
549
- if (textContent) {
550
- contextManager.addMessage({ role: "assistant", content: textContent });
551
- if (config.session.autoSave) {
552
- slog.saveAssistantMessage(textContent);
553
- }
554
- slog.logAssistant(textContent, reasoningContent, undefined, iteration);
555
- }
556
- lastText = textContent;
557
- {
558
- const decisionPatterns = [
559
- ...textContent.matchAll(/(?:plan|decided|decision|решено|план|решение):\s*(.+?)(?:\n|$)/gi),
560
- ];
561
- for (const match of decisionPatterns) {
562
- hallucinationDetector
563
- .getConsistencyCheck()
564
- .trackDecision(match[1].trim(), "agent_response");
565
- }
566
- }
567
- if (!sawToolCall) {
568
- // Guard: the model returned an EMPTY final response (no text, no
569
- // tool calls — often just reasoning content after context
570
- // compaction). Nudge it to produce a real answer instead of
571
- // silently finishing with text: "".
572
- if (!textContent?.trim()) {
573
- if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
574
- emptyResponseRetries++;
575
- logger.warn(`Empty response on iteration ${iteration} (retry ${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
576
- contextManager.addMessage({
577
- role: "user",
578
- 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>`,
579
- });
580
- continue;
581
- }
582
- emptyResponseExhausted = true;
583
- logger.warn(`Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
584
- }
585
- if (this.deps.finalAudit) {
586
- const audit = await this.deps.finalAudit();
587
- if (audit && !audit.passed) {
588
- logger.warn(`Final audit incomplete: ${audit.summary}`);
589
- const steps = audit.pendingSteps.slice(0, 5).join("; ") || "—";
590
- contextManager.addMessage({
591
- role: "user",
592
- content: `<system-summary>${t("exec.audit_incomplete", {
593
- summary: audit.summary,
594
- steps,
595
- })}</system-summary>`,
596
- });
597
- slog.logAudit(audit.summary, iteration);
598
- auditRetries++;
599
- if (auditRetries >= MAX_AUDIT_RETRIES ||
600
- iteration >= config.maxToolIterations - 1) {
601
- logger.warn(`Final audit still incomplete after ${auditRetries} retries — finishing anyway`);
602
- break;
603
- }
604
- continue;
605
- }
606
- }
607
- break;
608
- }
609
- }
610
- const tokensUsed = contextManager.getEstimatedTokens();
611
- const budget = contextManager.getBudget();
612
- const usageTokens = this.resolveUsageTokens(apiPromptTokens, apiCompletionTokens, tokensUsed, apiCompletionChars);
613
- if (iteration >= config.maxToolIterations) {
614
- return {
615
- success: false,
616
- text: lastText,
617
- error: t("error.max_iters", { max: config.maxToolIterations }),
618
- iterationCount: iteration,
619
- contextUsed: tokensUsed,
620
- contextLimit: budget.history,
621
- promptTokens: usageTokens.prompt,
622
- completionTokens: usageTokens.completion,
623
- totalTokens: usageTokens.total,
624
- compactionCount: contextManager.getCompactionCount(),
625
- contextQuality: contextManager.getQuality(),
626
- };
627
- }
628
- return {
629
- success: emptyResponseExhausted ? false : true,
630
- text: lastText,
631
- error: emptyResponseExhausted ? t("error.empty_response") : undefined,
632
- iterationCount: iteration,
633
- contextUsed: tokensUsed,
634
- contextLimit: budget.history,
635
- promptTokens: usageTokens.prompt,
636
- completionTokens: usageTokens.completion,
637
- totalTokens: usageTokens.total,
638
- compactionCount: contextManager.getCompactionCount(),
639
- contextQuality: contextManager.getQuality(),
640
- };
641
- }
642
- clearContext() {
643
- this.deps.contextManager.clear();
644
- this.systemPromptAdded = false;
645
- }
646
- async reconfigure(config) {
647
- const { OpenAICompatProvider } = await import("../llm/openai-compat");
648
- const { TokenCounter } = await import("../llm/token-counter");
649
- const newProvider = new OpenAICompatProvider({
650
- model: config.model,
651
- baseUrl: config.provider.baseUrl,
652
- apiKey: config.provider.apiKey,
653
- contextWindow: config.contextWindow,
654
- retry: config.retry,
655
- rateLimits: config.security?.rateLimits,
656
- });
657
- this.deps.llmProvider = newProvider;
658
- this.deps.toolExecutor.updateProvider(newProvider);
659
- this.deps.toolExecutor.ctx.llmProvider = newProvider;
660
- const newTokenCounter = new TokenCounter(config.model);
661
- this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
662
- this.deps.config = config;
663
- }
664
- setContext(messages) {
665
- const { contextManager } = this.deps;
666
- contextManager.clear();
667
- const { prompt: systemPrompt } = this.buildSystemPrompt();
668
- contextManager.addMessage({ role: "system", content: systemPrompt });
669
- this.systemPromptAdded = true;
670
- for (const msg of messages) {
671
- if (msg.role === "system")
672
- continue;
673
- contextManager.addMessage({
674
- role: msg.role,
675
- content: msg.content,
676
- name: msg.name,
677
- });
678
- }
679
- }
680
- shutdown() {
681
- this.shutdownRequested = true;
682
- this.abortController?.abort();
683
- const { pluginManager, logger, sessionManager, contextManager } = this.deps;
684
- contextManager.onCompact = null;
685
- const killed = processRegistry.killAll();
686
- if (killed > 0) {
687
- logger.info(`Killed ${killed} background process(es) on shutdown`);
688
- }
689
- logger.closeSessionLog();
690
- pluginManager.runOnSessionEnd({
691
- logger,
692
- sessionManager: sessionManager?.getActiveMeta(),
693
- });
694
- }
695
- }