micro-models-agent 0.39.1 → 0.40.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.
- package/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +116 -3
- package/dist/cli/main.js +35 -8
- package/dist/cli/repl-commands.js +633 -0
- package/dist/cli/repl.js +110 -611
- package/dist/cli/setup.js +32 -12
- package/dist/config/config.js +46 -30
- package/dist/config/defaults.js +10 -1
- package/dist/config/security.js +15 -8
- package/dist/core/agent-moe.js +24 -12
- package/dist/core/agent.js +281 -47
- package/dist/core/bootstrap.js +52 -36
- package/dist/core/session-logger.js +35 -2
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +79 -15
- package/dist/i18n/index.js +12 -9
- package/dist/i18n/ru.json +79 -15
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +39 -10
- package/dist/logger/app-logger.js +83 -16
- package/dist/logger/file-log.js +151 -0
- package/dist/main.js +492 -283
- package/dist/modules/browser/bridge-server.mjs +113 -105
- package/dist/modules/browser/session.js +108 -60
- package/dist/modules/certification/cli.js +176 -0
- package/dist/modules/certification/fact-checker.js +84 -0
- package/dist/modules/certification/loader.js +111 -0
- package/dist/modules/certification/manifest.js +50 -0
- package/dist/modules/certification/runner.js +162 -0
- package/dist/modules/certification/scenarios.js +124 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/manager.js +119 -10
- package/dist/modules/execution/auditor.js +33 -39
- package/dist/modules/execution/index.js +8 -6
- package/dist/modules/execution/module.js +474 -32
- package/dist/modules/execution/moe-executor.js +97 -40
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +159 -0
- package/dist/modules/execution/planner.js +63 -13
- package/dist/modules/execution/stuck-detector.js +252 -39
- package/dist/modules/execution/tracker.js +21 -7
- package/dist/modules/execution/verifier.js +46 -17
- package/dist/modules/hallucination/confidence.js +7 -2
- package/dist/modules/hallucination/consistency.js +8 -42
- package/dist/modules/hallucination/detector.js +26 -21
- package/dist/modules/hallucination/factual.js +170 -150
- package/dist/modules/hallucination/index.js +5 -4
- package/dist/modules/hallucination/js-identifiers.js +72 -0
- package/dist/modules/hallucination/llm-judge.js +103 -0
- package/dist/modules/index.js +5 -5
- package/dist/modules/lsp/client.js +235 -0
- package/dist/modules/lsp/config.js +81 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +68 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +8 -2
- package/dist/modules/memory/store.js +4 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
- package/dist/modules/processes/index.js +1 -2
- package/dist/modules/processes/registry.js +125 -35
- package/dist/modules/processes/runner.js +9 -110
- package/dist/modules/security/audit-log.js +30 -10
- package/dist/modules/security/command-validator.js +42 -16
- package/dist/modules/security/content-scanner.js +9 -8
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +64 -10
- package/dist/modules/security/security-policies.js +221 -67
- package/dist/modules/security/session-encryption.js +42 -25
- package/dist/modules/session/manager.js +15 -10
- package/dist/modules/session/store.js +62 -8
- package/dist/modules/skills/index.js +2 -3
- package/dist/modules/skills/module.js +10 -23
- package/dist/tools/bash.js +287 -90
- package/dist/tools/create-dir.js +0 -1
- package/dist/tools/delete-file.js +0 -1
- package/dist/tools/edit-file.js +10 -8
- package/dist/tools/executor.js +57 -7
- package/dist/tools/grep-tool.js +51 -29
- package/dist/tools/index.js +55 -40
- package/dist/tools/load-skill.js +14 -18
- package/dist/tools/move-file.js +3 -2
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +15 -5
- package/dist/tools/search-history.js +42 -22
- package/dist/tools/subagent.js +21 -12
- package/dist/tools/web-browse.js +54 -25
- package/dist/tools/web-fetch.js +60 -34
- package/dist/tools/web-search.js +39 -20
- package/dist/tools/write-file.js +13 -10
- package/dist/ui/diff.js +9 -16
- package/dist/ui/renderer.js +69 -6
- package/package.json +48 -45
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
package/dist/core/agent.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
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";
|
|
9
|
+
import { StepVerifier } from "../modules/execution/verifier";
|
|
7
10
|
const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
8
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
|
+
}
|
|
9
27
|
export class Agent {
|
|
10
28
|
deps;
|
|
11
29
|
systemPromptAdded = false;
|
|
12
30
|
shutdownRequested = false;
|
|
31
|
+
abortController = null;
|
|
32
|
+
lastCompactionShown = 0;
|
|
13
33
|
constructor(deps) {
|
|
14
34
|
this.deps = deps;
|
|
15
35
|
}
|
|
@@ -31,19 +51,45 @@ export class Agent {
|
|
|
31
51
|
if (dynamic.length > 0) {
|
|
32
52
|
builder.addBlocks(dynamic);
|
|
33
53
|
}
|
|
34
|
-
const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? [])
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
: []);
|
|
42
64
|
if (pluginBlocks.length > 0) {
|
|
43
65
|
builder.addBlocks(pluginBlocks);
|
|
44
66
|
}
|
|
45
67
|
return builder.build();
|
|
46
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
|
+
}
|
|
47
93
|
refreshSystemPrompt() {
|
|
48
94
|
const { prompt } = this.buildSystemPrompt();
|
|
49
95
|
const current = this.deps.contextManager
|
|
@@ -71,8 +117,8 @@ export class Agent {
|
|
|
71
117
|
}
|
|
72
118
|
async run(input, onChunk, onMeta, onTool, onPhase) {
|
|
73
119
|
this.setScope();
|
|
74
|
-
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, } = this.deps;
|
|
75
|
-
const slog = new SessionLogger(sessionManager);
|
|
120
|
+
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
|
|
121
|
+
const slog = new SessionLogger(sessionManager, logger);
|
|
76
122
|
if (sessionManager && !sessionManager.getActive()) {
|
|
77
123
|
sessionManager.create();
|
|
78
124
|
logger.debug(`Session started: ${sessionManager.getActive()}`);
|
|
@@ -90,6 +136,18 @@ export class Agent {
|
|
|
90
136
|
logger,
|
|
91
137
|
sessionManager: sessionManager?.getActiveMeta(),
|
|
92
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
|
+
}
|
|
93
151
|
}
|
|
94
152
|
contextManager.addMessage({ role: "user", content: input });
|
|
95
153
|
slog.logUser(input);
|
|
@@ -105,25 +163,43 @@ export class Agent {
|
|
|
105
163
|
return this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase);
|
|
106
164
|
}
|
|
107
165
|
async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
|
|
108
|
-
const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, } = this.deps;
|
|
109
|
-
const slog = new SessionLogger(sessionManager);
|
|
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();
|
|
110
169
|
let iteration = 0;
|
|
111
170
|
let lastText = "";
|
|
112
171
|
let hallucinationRetries = 0;
|
|
113
172
|
let lastToolSignature = "";
|
|
114
173
|
let apiPromptTokens = 0;
|
|
115
174
|
let apiCompletionTokens = 0;
|
|
175
|
+
let apiCompletionChars = 0;
|
|
116
176
|
const MAX_HALLUCINATION_RETRIES = 3;
|
|
117
177
|
let consecutiveToolFailures = 0;
|
|
118
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);
|
|
119
191
|
while (iteration < config.maxToolIterations && !this.shutdownRequested) {
|
|
120
192
|
iteration++;
|
|
193
|
+
contextManager.noteIteration();
|
|
121
194
|
pluginManager.runOnBeforeThink({
|
|
122
195
|
iteration,
|
|
123
196
|
logger,
|
|
124
197
|
lastUserMessage: input,
|
|
125
198
|
contextManager,
|
|
126
199
|
onMeta,
|
|
200
|
+
sessionLog: {
|
|
201
|
+
plan: (event, detail, iter) => slog.logPlan(event, detail, iter),
|
|
202
|
+
},
|
|
127
203
|
});
|
|
128
204
|
if (contextManager.needsCompaction()) {
|
|
129
205
|
contextManager.compact();
|
|
@@ -132,6 +208,13 @@ export class Agent {
|
|
|
132
208
|
}
|
|
133
209
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
134
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
|
+
}
|
|
135
218
|
if (currentTokens > budget.history) {
|
|
136
219
|
contextManager.compact();
|
|
137
220
|
logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
|
|
@@ -139,8 +222,7 @@ export class Agent {
|
|
|
139
222
|
}
|
|
140
223
|
this.refreshSystemPrompt();
|
|
141
224
|
const history = contextManager.getActiveHistory();
|
|
142
|
-
|
|
143
|
-
slog.logToolDefs(allTools.length, allTools.map((t) => t.name), iteration);
|
|
225
|
+
slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
|
|
144
226
|
let textContent = "";
|
|
145
227
|
let reasoningContent = "";
|
|
146
228
|
const toolCalls = [];
|
|
@@ -148,8 +230,12 @@ export class Agent {
|
|
|
148
230
|
let emittedReasoning = false;
|
|
149
231
|
const textChunks = [];
|
|
150
232
|
this.emitPhase(iteration, "thinking", onPhase);
|
|
233
|
+
const llmStart = Date.now();
|
|
234
|
+
logger.logLLMRequest(config.model, history.length, input, "agent");
|
|
151
235
|
try {
|
|
152
|
-
for await (const chunk of llmProvider.chat(history,
|
|
236
|
+
for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
|
|
237
|
+
if (this.shutdownRequested)
|
|
238
|
+
break;
|
|
153
239
|
if (chunk.type === "text" && chunk.content) {
|
|
154
240
|
if (emittedReasoning && !textContent) {
|
|
155
241
|
onMeta?.("\n\n");
|
|
@@ -189,6 +275,11 @@ export class Agent {
|
|
|
189
275
|
}
|
|
190
276
|
}
|
|
191
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");
|
|
192
283
|
logger.error(`LLM call failed: ${err.message}`);
|
|
193
284
|
slog.logError(err.message);
|
|
194
285
|
pluginManager.runOnError({ iteration, logger }, err);
|
|
@@ -202,10 +293,22 @@ export class Agent {
|
|
|
202
293
|
finally {
|
|
203
294
|
this.emitPhase(iteration, "done", onPhase);
|
|
204
295
|
}
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
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) {
|
|
209
312
|
for (const chunk of textChunks) {
|
|
210
313
|
const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
|
|
211
314
|
onChunk?.(textOut);
|
|
@@ -227,8 +330,19 @@ export class Agent {
|
|
|
227
330
|
.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
|
|
228
331
|
.join("|");
|
|
229
332
|
if (signature && signature === lastToolSignature) {
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
});
|
|
232
346
|
}
|
|
233
347
|
lastToolSignature = signature;
|
|
234
348
|
}
|
|
@@ -260,26 +374,35 @@ export class Agent {
|
|
|
260
374
|
pluginManager.runOnToolStart({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
261
375
|
onTool?.({ type: "start", tool: call.name, args: call.arguments });
|
|
262
376
|
slog.logToolCall(call, iteration);
|
|
263
|
-
const
|
|
377
|
+
const tokensBeforeTool = contextManager.getEstimatedTokens();
|
|
378
|
+
const result = await toolExecutor.execute(call, this.abortController?.signal);
|
|
264
379
|
const duration = Date.now() - startTime;
|
|
265
380
|
if (!result.success)
|
|
266
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
|
+
}
|
|
267
395
|
pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
268
396
|
if (result.display) {
|
|
269
397
|
onMeta?.("\n" + result.display + "\n");
|
|
270
398
|
}
|
|
271
|
-
|
|
272
|
-
|
|
399
|
+
else {
|
|
400
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
|
|
401
|
+
onMeta?.("\n" + pc.dim(metaOut) + "\n");
|
|
402
|
+
}
|
|
273
403
|
if (result.diff) {
|
|
274
404
|
onMeta?.("\n" + result.diff + "\n");
|
|
275
405
|
}
|
|
276
|
-
onTool?.({
|
|
277
|
-
type: "end",
|
|
278
|
-
tool: call.name,
|
|
279
|
-
args: call.arguments,
|
|
280
|
-
duration,
|
|
281
|
-
error: !result.success,
|
|
282
|
-
});
|
|
283
406
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
284
407
|
const budget = contextManager.getBudget();
|
|
285
408
|
const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens);
|
|
@@ -288,6 +411,17 @@ export class Agent {
|
|
|
288
411
|
content: truncatedOutput,
|
|
289
412
|
name: call.name,
|
|
290
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,
|
|
291
425
|
});
|
|
292
426
|
summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
|
|
293
427
|
if (config.session.autoSave) {
|
|
@@ -305,7 +439,9 @@ export class Agent {
|
|
|
305
439
|
consecutiveToolFailures = 0;
|
|
306
440
|
}
|
|
307
441
|
if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
|
|
308
|
-
const recoveryMsg = t("exec.consecutive_failures_recovery", {
|
|
442
|
+
const recoveryMsg = t("exec.consecutive_failures_recovery", {
|
|
443
|
+
count: consecutiveToolFailures,
|
|
444
|
+
});
|
|
309
445
|
logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
|
|
310
446
|
const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
|
|
311
447
|
const taskReminder = t("exec.task_reminder", { task: taskSnippet });
|
|
@@ -313,14 +449,44 @@ export class Agent {
|
|
|
313
449
|
role: "user",
|
|
314
450
|
content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
|
|
315
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
|
+
}
|
|
316
460
|
}
|
|
317
461
|
contextManager.addMessage({
|
|
318
462
|
role: "user",
|
|
319
463
|
content: `<system-summary>${summaries.join("\n")}</system-summary>`,
|
|
320
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
|
+
}
|
|
321
486
|
continue;
|
|
322
487
|
}
|
|
323
|
-
|
|
488
|
+
hallucinationDetector.getConfidenceCheck().setPreviousResponse(lastText);
|
|
489
|
+
const hallucinationResult = await hallucinationDetector.validate(textContent);
|
|
324
490
|
if (hallucinationResult.status === "block") {
|
|
325
491
|
logger.warn(`Response blocked: ${hallucinationResult.reason}`);
|
|
326
492
|
return {
|
|
@@ -334,17 +500,27 @@ export class Agent {
|
|
|
334
500
|
}
|
|
335
501
|
if (hallucinationResult.status === "warn") {
|
|
336
502
|
logger.warn(`Hallucination warning: ${hallucinationResult.reason}`);
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
|
|
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`);
|
|
340
509
|
}
|
|
341
|
-
lastText = warnPrefix + lastText;
|
|
342
510
|
}
|
|
343
511
|
if (hallucinationResult.status === "retry") {
|
|
344
|
-
if (this.deps.exitOnComplete) {
|
|
512
|
+
if (this.deps.exitOnComplete && textContent?.trim()) {
|
|
345
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;
|
|
346
518
|
break;
|
|
347
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: "".
|
|
348
524
|
if (hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
|
|
349
525
|
logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
|
|
350
526
|
return {
|
|
@@ -372,11 +548,40 @@ export class Agent {
|
|
|
372
548
|
}
|
|
373
549
|
if (textContent) {
|
|
374
550
|
contextManager.addMessage({ role: "assistant", content: textContent });
|
|
375
|
-
|
|
551
|
+
if (config.session.autoSave) {
|
|
552
|
+
slog.saveAssistantMessage(textContent);
|
|
553
|
+
}
|
|
376
554
|
slog.logAssistant(textContent, reasoningContent, undefined, iteration);
|
|
377
555
|
}
|
|
378
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
|
+
}
|
|
379
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
|
+
}
|
|
380
585
|
if (this.deps.finalAudit) {
|
|
381
586
|
const audit = await this.deps.finalAudit();
|
|
382
587
|
if (audit && !audit.passed) {
|
|
@@ -390,7 +595,10 @@ export class Agent {
|
|
|
390
595
|
})}</system-summary>`,
|
|
391
596
|
});
|
|
392
597
|
slog.logAudit(audit.summary, iteration);
|
|
393
|
-
|
|
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`);
|
|
394
602
|
break;
|
|
395
603
|
}
|
|
396
604
|
continue;
|
|
@@ -401,6 +609,7 @@ export class Agent {
|
|
|
401
609
|
}
|
|
402
610
|
const tokensUsed = contextManager.getEstimatedTokens();
|
|
403
611
|
const budget = contextManager.getBudget();
|
|
612
|
+
const usageTokens = this.resolveUsageTokens(apiPromptTokens, apiCompletionTokens, tokensUsed, apiCompletionChars);
|
|
404
613
|
if (iteration >= config.maxToolIterations) {
|
|
405
614
|
return {
|
|
406
615
|
success: false,
|
|
@@ -409,26 +618,49 @@ export class Agent {
|
|
|
409
618
|
iterationCount: iteration,
|
|
410
619
|
contextUsed: tokensUsed,
|
|
411
620
|
contextLimit: budget.history,
|
|
412
|
-
promptTokens:
|
|
413
|
-
completionTokens:
|
|
414
|
-
totalTokens:
|
|
621
|
+
promptTokens: usageTokens.prompt,
|
|
622
|
+
completionTokens: usageTokens.completion,
|
|
623
|
+
totalTokens: usageTokens.total,
|
|
624
|
+
compactionCount: contextManager.getCompactionCount(),
|
|
625
|
+
contextQuality: contextManager.getQuality(),
|
|
415
626
|
};
|
|
416
627
|
}
|
|
417
628
|
return {
|
|
418
|
-
success: true,
|
|
629
|
+
success: emptyResponseExhausted ? false : true,
|
|
419
630
|
text: lastText,
|
|
631
|
+
error: emptyResponseExhausted ? t("error.empty_response") : undefined,
|
|
420
632
|
iterationCount: iteration,
|
|
421
633
|
contextUsed: tokensUsed,
|
|
422
634
|
contextLimit: budget.history,
|
|
423
|
-
promptTokens:
|
|
424
|
-
completionTokens:
|
|
425
|
-
totalTokens:
|
|
635
|
+
promptTokens: usageTokens.prompt,
|
|
636
|
+
completionTokens: usageTokens.completion,
|
|
637
|
+
totalTokens: usageTokens.total,
|
|
638
|
+
compactionCount: contextManager.getCompactionCount(),
|
|
639
|
+
contextQuality: contextManager.getQuality(),
|
|
426
640
|
};
|
|
427
641
|
}
|
|
428
642
|
clearContext() {
|
|
429
643
|
this.deps.contextManager.clear();
|
|
430
644
|
this.systemPromptAdded = false;
|
|
431
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
|
+
}
|
|
432
664
|
setContext(messages) {
|
|
433
665
|
const { contextManager } = this.deps;
|
|
434
666
|
contextManager.clear();
|
|
@@ -447,12 +679,14 @@ export class Agent {
|
|
|
447
679
|
}
|
|
448
680
|
shutdown() {
|
|
449
681
|
this.shutdownRequested = true;
|
|
682
|
+
this.abortController?.abort();
|
|
450
683
|
const { pluginManager, logger, sessionManager, contextManager } = this.deps;
|
|
451
684
|
contextManager.onCompact = null;
|
|
452
685
|
const killed = processRegistry.killAll();
|
|
453
686
|
if (killed > 0) {
|
|
454
687
|
logger.info(`Killed ${killed} background process(es) on shutdown`);
|
|
455
688
|
}
|
|
689
|
+
logger.closeSessionLog();
|
|
456
690
|
pluginManager.runOnSessionEnd({
|
|
457
691
|
logger,
|
|
458
692
|
sessionManager: sessionManager?.getActiveMeta(),
|