micro-models-agent 0.41.0 → 0.41.2
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 +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +1111 -954
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
package/dist/core/agent.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { join } from "path";
|
|
2
1
|
import { t } from "../i18n/index";
|
|
3
2
|
import { pc } from "../ui/colors";
|
|
4
3
|
import { PromptBuilder } from "./prompt-builder";
|
|
5
4
|
import { processRegistry } from "../modules/processes";
|
|
6
5
|
import { SessionLogger } from "./session-logger";
|
|
7
6
|
import { runWithMoE } from "./agent-moe";
|
|
8
|
-
import { MemoryStore } from "../modules/memory/store";
|
|
9
7
|
import { StepVerifier } from "../modules/execution/verifier";
|
|
10
8
|
const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
11
9
|
const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
|
|
12
10
|
const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
11
|
+
/** Minimum iterations between quality-triggered forced compactions. Without
|
|
12
|
+
* this, a low-quality context re-triggers compaction on EVERY iteration
|
|
13
|
+
* (observed: 56 compactions in ~28 min) and the compaction itself can't
|
|
14
|
+
* restore quality, so the agent burns the whole budget compacting. */
|
|
15
|
+
const FORCED_COMPACTION_COOLDOWN = 3;
|
|
13
16
|
/** True when the text looks like a raw JSON tool payload (garbage to display). */
|
|
14
17
|
function isToolCallJson(text) {
|
|
15
18
|
const trimmed = text.trim();
|
|
@@ -24,6 +27,19 @@ function isToolCallJson(text) {
|
|
|
24
27
|
}
|
|
25
28
|
return false;
|
|
26
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Max chars a tool result may occupy, given remaining budget and whether
|
|
32
|
+
* the tool bounds its own output. Tools that declare `boundedOutput` (e.g.
|
|
33
|
+
* read_file with its line limit) are never truncated by the budget — a
|
|
34
|
+
* near-full context used to cut them to ~2K chars, making the model believe
|
|
35
|
+
* files were truncated and re-read them forever.
|
|
36
|
+
*/
|
|
37
|
+
export function toolOutputCharLimit(remainingBudget, historyBudget, bounded) {
|
|
38
|
+
if (bounded)
|
|
39
|
+
return Number.MAX_SAFE_INTEGER;
|
|
40
|
+
const maxCharsByRatio = Math.floor(historyBudget * TOOL_RESULT_MAX_TOKENS_RATIO * 2);
|
|
41
|
+
return Math.min(remainingBudget, maxCharsByRatio, TOOL_RESULT_ABSOLUTE_MAX_CHARS);
|
|
42
|
+
}
|
|
27
43
|
export class Agent {
|
|
28
44
|
deps;
|
|
29
45
|
systemPromptAdded = false;
|
|
@@ -43,8 +59,7 @@ export class Agent {
|
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
buildSystemPrompt() {
|
|
46
|
-
const systemBudget = Math.floor(this.deps.config.contextWindow *
|
|
47
|
-
this.deps.config.contextBudget.systemPrompt);
|
|
62
|
+
const systemBudget = Math.floor(this.deps.config.contextWindow * this.deps.config.contextBudget.systemPrompt);
|
|
48
63
|
const builder = new PromptBuilder(systemBudget);
|
|
49
64
|
builder.addBlocks(this.deps.promptBlocks);
|
|
50
65
|
const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
|
|
@@ -64,7 +79,45 @@ export class Agent {
|
|
|
64
79
|
if (pluginBlocks.length > 0) {
|
|
65
80
|
builder.addBlocks(pluginBlocks);
|
|
66
81
|
}
|
|
67
|
-
|
|
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
|
+
});
|
|
68
121
|
}
|
|
69
122
|
getSystemPromptInfo() {
|
|
70
123
|
const { prompt, excluded } = this.buildSystemPrompt();
|
|
@@ -92,27 +145,26 @@ export class Agent {
|
|
|
92
145
|
}
|
|
93
146
|
refreshSystemPrompt() {
|
|
94
147
|
const { prompt } = this.buildSystemPrompt();
|
|
95
|
-
const current = this.deps.contextManager
|
|
96
|
-
.getActiveHistory()
|
|
97
|
-
.find((m) => m.role === "system");
|
|
148
|
+
const current = this.deps.contextManager.getActiveHistory().find((m) => m.role === "system");
|
|
98
149
|
if (!current || current.content !== prompt) {
|
|
99
150
|
this.deps.contextManager.updateSystemPrompt?.(prompt);
|
|
100
151
|
}
|
|
101
152
|
}
|
|
102
|
-
truncateToolOutput(output, budget, currentTokens) {
|
|
103
|
-
const remainingBudget = budget.history - currentTokens;
|
|
104
|
-
const
|
|
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);
|
|
153
|
+
truncateToolOutput(output, budget, currentTokens, bounded) {
|
|
154
|
+
const remainingBudget = Math.max(0, budget.history - currentTokens);
|
|
155
|
+
const maxChars = toolOutputCharLimit(remainingBudget, budget.history, bounded);
|
|
107
156
|
if (output.length <= maxChars)
|
|
108
157
|
return output;
|
|
109
158
|
const truncated = output.slice(0, maxChars);
|
|
110
159
|
const removedChars = output.length - maxChars;
|
|
111
|
-
return (truncated
|
|
112
|
-
`\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`);
|
|
160
|
+
return truncated + `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
|
|
113
161
|
}
|
|
114
162
|
emitPhase(iteration, phase, onPhase) {
|
|
115
|
-
this.deps.pluginManager.runOnPhase?.({
|
|
163
|
+
this.deps.pluginManager.runOnPhase?.({
|
|
164
|
+
iteration,
|
|
165
|
+
logger: this.deps.logger,
|
|
166
|
+
contextManager: this.deps.contextManager,
|
|
167
|
+
}, phase);
|
|
116
168
|
onPhase?.(phase);
|
|
117
169
|
}
|
|
118
170
|
async run(input, onChunk, onMeta, onTool, onPhase) {
|
|
@@ -125,6 +177,10 @@ export class Agent {
|
|
|
125
177
|
}
|
|
126
178
|
if (!this.systemPromptAdded &&
|
|
127
179
|
!contextManager.getActiveHistory().some((m) => m.role === "system")) {
|
|
180
|
+
const lazy = this.deps.lazyPromptBlocks ? await this.deps.lazyPromptBlocks() : [];
|
|
181
|
+
if (lazy.length > 0) {
|
|
182
|
+
this.deps.promptBlocks.push(...lazy);
|
|
183
|
+
}
|
|
128
184
|
const { prompt: systemPrompt, excluded } = this.buildSystemPrompt();
|
|
129
185
|
contextManager.addMessage({ role: "system", content: systemPrompt });
|
|
130
186
|
this.systemPromptAdded = true;
|
|
@@ -135,6 +191,7 @@ export class Agent {
|
|
|
135
191
|
pluginManager.runOnSessionStart({
|
|
136
192
|
logger,
|
|
137
193
|
sessionManager: sessionManager?.getActiveMeta(),
|
|
194
|
+
contextManager,
|
|
138
195
|
});
|
|
139
196
|
if (config.session?.baselineCheck !== false) {
|
|
140
197
|
const verifier = new StepVerifier(baseDir);
|
|
@@ -149,6 +206,13 @@ export class Agent {
|
|
|
149
206
|
.catch(() => { });
|
|
150
207
|
}
|
|
151
208
|
}
|
|
209
|
+
// A new user message starts a fresh compaction interval — iterations
|
|
210
|
+
// from the previous turn must not trigger a compaction on iteration 1
|
|
211
|
+
// of this one (observed: turn 2 compacted at iteration 8, deleting the
|
|
212
|
+
// just-sent task along with 40+ old turns).
|
|
213
|
+
if (typeof contextManager.resetUserTurn === "function") {
|
|
214
|
+
contextManager.resetUserTurn();
|
|
215
|
+
}
|
|
152
216
|
contextManager.addMessage({ role: "user", content: input });
|
|
153
217
|
slog.logUser(input);
|
|
154
218
|
if (config.moe?.enabled) {
|
|
@@ -168,6 +232,12 @@ export class Agent {
|
|
|
168
232
|
this.abortController = new AbortController();
|
|
169
233
|
let iteration = 0;
|
|
170
234
|
let lastText = "";
|
|
235
|
+
// The actual most-recent model output (tool commentary, retried answers
|
|
236
|
+
// included). Repetition is compared against THIS, not the last *accepted*
|
|
237
|
+
// text — a frozen accepted answer made consecutive retries compare
|
|
238
|
+
// against a stale baseline and flag every re-answer as repetitive.
|
|
239
|
+
let lastModelText = "";
|
|
240
|
+
let lastForcedCompactionIteration = -FORCED_COMPACTION_COOLDOWN;
|
|
171
241
|
let hallucinationRetries = 0;
|
|
172
242
|
let lastToolSignature = "";
|
|
173
243
|
let apiPromptTokens = 0;
|
|
@@ -176,21 +246,40 @@ export class Agent {
|
|
|
176
246
|
const MAX_HALLUCINATION_RETRIES = 3;
|
|
177
247
|
let consecutiveToolFailures = 0;
|
|
178
248
|
const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
249
|
+
// Per-tool failure counts and which tools already produced a memory rule.
|
|
250
|
+
const toolFailureCounts = new Map();
|
|
251
|
+
const memoryRuleRecorded = new Set();
|
|
252
|
+
const MIN_REPEATED_TOOL_FAILURES = 3;
|
|
179
253
|
let auditRetries = 0;
|
|
180
254
|
const MAX_AUDIT_RETRIES = 3;
|
|
181
255
|
let emptyResponseRetries = 0;
|
|
182
256
|
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
183
257
|
let emptyResponseExhausted = false;
|
|
258
|
+
let auditFailed = false;
|
|
259
|
+
let lastAuditSummary = "";
|
|
260
|
+
// Set when the audit gate rejects a final answer and re-prompts: the next
|
|
261
|
+
// non-tool response is then a re-answer of an already-completed task, so
|
|
262
|
+
// a repetition verdict is expected and must not burn a hallucination retry.
|
|
263
|
+
let suppressRepetitionRetry = false;
|
|
184
264
|
let repeatedToolCount = 0;
|
|
185
265
|
const MAX_REPEATED_TOOL_CALLS = 2;
|
|
186
266
|
// Account for tool definitions in context budget (they're sent via body.tools, not messages)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
190
274
|
contextManager.setToolTokens(toolTokenEstimate);
|
|
191
275
|
while (iteration < config.maxToolIterations && !this.shutdownRequested) {
|
|
192
276
|
iteration++;
|
|
193
277
|
contextManager.noteIteration();
|
|
278
|
+
// Re-read the mutable tag set in case enable_tools was called.
|
|
279
|
+
allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
|
|
280
|
+
boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
|
|
281
|
+
toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
|
|
282
|
+
contextManager.setToolTokens(toolTokenEstimate);
|
|
194
283
|
pluginManager.runOnBeforeThink({
|
|
195
284
|
iteration,
|
|
196
285
|
logger,
|
|
@@ -202,27 +291,50 @@ export class Agent {
|
|
|
202
291
|
},
|
|
203
292
|
});
|
|
204
293
|
if (contextManager.needsCompaction()) {
|
|
205
|
-
contextManager.compact();
|
|
206
|
-
|
|
207
|
-
|
|
294
|
+
const result = contextManager.compact();
|
|
295
|
+
if (result) {
|
|
296
|
+
logger.debug("Context compacted");
|
|
297
|
+
slog.logCompaction({ reason: "interval", iteration, ...result });
|
|
298
|
+
}
|
|
208
299
|
}
|
|
209
300
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
210
301
|
const budget = contextManager.getBudget();
|
|
211
302
|
const quality = contextManager.getQuality();
|
|
212
303
|
if (quality < QUALITY_TRIGGER_THRESHOLD &&
|
|
213
|
-
contextManager.getCompactionCount() > 0
|
|
214
|
-
|
|
304
|
+
contextManager.getCompactionCount() > 0 &&
|
|
305
|
+
iteration - lastForcedCompactionIteration >= FORCED_COMPACTION_COOLDOWN) {
|
|
306
|
+
lastForcedCompactionIteration = iteration;
|
|
307
|
+
const result = contextManager.compact();
|
|
215
308
|
logger.warn(`Low context quality (${quality}%) — forced compaction`);
|
|
216
|
-
|
|
309
|
+
if (result) {
|
|
310
|
+
slog.logCompaction({
|
|
311
|
+
reason: `quality-triggered (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%)`,
|
|
312
|
+
iteration,
|
|
313
|
+
...result,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
217
316
|
}
|
|
218
317
|
if (currentTokens > budget.history) {
|
|
219
|
-
contextManager.compact();
|
|
318
|
+
const result = contextManager.compact();
|
|
220
319
|
logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
|
|
221
|
-
|
|
320
|
+
if (result) {
|
|
321
|
+
slog.logCompaction({
|
|
322
|
+
reason: `overflow (${currentTokens} > ${budget.history})`,
|
|
323
|
+
iteration,
|
|
324
|
+
...result,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
222
327
|
}
|
|
223
328
|
this.refreshSystemPrompt();
|
|
224
329
|
const history = contextManager.getActiveHistory();
|
|
225
330
|
slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
|
|
331
|
+
if (iteration === 1) {
|
|
332
|
+
const { blocks } = this.buildSystemPrompt();
|
|
333
|
+
this.logContextStat("start", iteration, slog, blocks);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.logContextStat("iteration", iteration, slog);
|
|
337
|
+
}
|
|
226
338
|
let textContent = "";
|
|
227
339
|
let reasoningContent = "";
|
|
228
340
|
const toolCalls = [];
|
|
@@ -231,6 +343,8 @@ export class Agent {
|
|
|
231
343
|
const textChunks = [];
|
|
232
344
|
this.emitPhase(iteration, "thinking", onPhase);
|
|
233
345
|
const llmStart = Date.now();
|
|
346
|
+
const promptBefore = apiPromptTokens;
|
|
347
|
+
const completionBefore = apiCompletionTokens;
|
|
234
348
|
logger.logLLMRequest(config.model, history.length, input, "agent");
|
|
235
349
|
try {
|
|
236
350
|
for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
|
|
@@ -246,7 +360,7 @@ export class Agent {
|
|
|
246
360
|
if (chunk.type === "reasoning" && chunk.content) {
|
|
247
361
|
reasoningContent += chunk.content;
|
|
248
362
|
if (config.showReasoning) {
|
|
249
|
-
const metaOut = pluginManager.runOnMeta({ iteration, logger }, chunk.content);
|
|
363
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, chunk.content);
|
|
250
364
|
if (metaOut) {
|
|
251
365
|
onMeta?.(pc.dim(metaOut));
|
|
252
366
|
}
|
|
@@ -282,7 +396,7 @@ export class Agent {
|
|
|
282
396
|
logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
|
|
283
397
|
logger.error(`LLM call failed: ${err.message}`);
|
|
284
398
|
slog.logError(err.message);
|
|
285
|
-
pluginManager.runOnError({ iteration, logger }, err);
|
|
399
|
+
pluginManager.runOnError({ iteration, logger, contextManager }, err);
|
|
286
400
|
return {
|
|
287
401
|
success: false,
|
|
288
402
|
text: lastText,
|
|
@@ -297,6 +411,26 @@ export class Agent {
|
|
|
297
411
|
// the backend omits `usage` from the response.
|
|
298
412
|
apiCompletionChars += (textContent || reasoningContent).length;
|
|
299
413
|
logger.logLLMResponse(config.model, (textContent || reasoningContent).length, Date.now() - llmStart, undefined, "agent");
|
|
414
|
+
// Log per-call token usage. llama.cpp streaming often omits `usage`,
|
|
415
|
+
// so fall back to local estimates (context tokens + chars/4) and
|
|
416
|
+
// mark the source — the log must distinguish real API numbers from
|
|
417
|
+
// heuristics.
|
|
418
|
+
{
|
|
419
|
+
const usagePrompt = apiPromptTokens - promptBefore;
|
|
420
|
+
const usageCompletion = apiCompletionTokens - completionBefore;
|
|
421
|
+
const source = usagePrompt > 0 || usageCompletion > 0 ? "api" : "estimate";
|
|
422
|
+
const prompt = source === "api" ? usagePrompt : contextManager.getEstimatedTokens();
|
|
423
|
+
const completion = source === "api"
|
|
424
|
+
? usageCompletion
|
|
425
|
+
: Math.ceil((textContent || reasoningContent).length / 4);
|
|
426
|
+
slog.logLlmUsage(iteration, {
|
|
427
|
+
promptTokens: prompt,
|
|
428
|
+
completionTokens: completion,
|
|
429
|
+
totalTokens: prompt + completion,
|
|
430
|
+
source,
|
|
431
|
+
durationMs: Date.now() - llmStart,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
300
434
|
if (this.shutdownRequested) {
|
|
301
435
|
break;
|
|
302
436
|
}
|
|
@@ -306,11 +440,10 @@ export class Agent {
|
|
|
306
440
|
// describing the call. `toolComments: false` restores the old behavior
|
|
307
441
|
// of suppressing text next to a tool call.
|
|
308
442
|
const toolComments = this.deps.config.ui?.toolComments ?? true;
|
|
309
|
-
const showText = textChunks.length > 0 &&
|
|
310
|
-
(!sawToolCall || (toolComments && !isToolCallJson(textContent)));
|
|
443
|
+
const showText = textChunks.length > 0 && (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
|
|
311
444
|
if (showText) {
|
|
312
445
|
for (const chunk of textChunks) {
|
|
313
|
-
const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
|
|
446
|
+
const textOut = pluginManager.runOnText({ iteration, logger, contextManager }, chunk);
|
|
314
447
|
onChunk?.(textOut);
|
|
315
448
|
}
|
|
316
449
|
}
|
|
@@ -324,7 +457,7 @@ export class Agent {
|
|
|
324
457
|
else if (reasoningContent) {
|
|
325
458
|
llmResponse = { type: "reasoning", content: reasoningContent };
|
|
326
459
|
}
|
|
327
|
-
pluginManager.runOnAfterThink({ iteration, logger }, llmResponse);
|
|
460
|
+
pluginManager.runOnAfterThink({ iteration, logger, contextManager }, llmResponse);
|
|
328
461
|
if (this.deps.exitOnComplete && sawToolCall) {
|
|
329
462
|
const signature = toolCalls
|
|
330
463
|
.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
|
|
@@ -371,7 +504,7 @@ export class Agent {
|
|
|
371
504
|
toolName: call.name,
|
|
372
505
|
args: call.arguments,
|
|
373
506
|
});
|
|
374
|
-
pluginManager.runOnToolStart({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
507
|
+
pluginManager.runOnToolStart({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
375
508
|
onTool?.({ type: "start", tool: call.name, args: call.arguments });
|
|
376
509
|
slog.logToolCall(call, iteration);
|
|
377
510
|
const tokensBeforeTool = contextManager.getEstimatedTokens();
|
|
@@ -382,22 +515,18 @@ export class Agent {
|
|
|
382
515
|
if (result.success && call.arguments.path) {
|
|
383
516
|
const filePath = String(call.arguments.path);
|
|
384
517
|
if (call.name === "write_file" || call.name === "edit_file") {
|
|
385
|
-
hallucinationDetector
|
|
386
|
-
.getConsistencyCheck()
|
|
387
|
-
.trackCreatedFile(filePath);
|
|
518
|
+
hallucinationDetector.getConsistencyCheck().trackCreatedFile(filePath);
|
|
388
519
|
}
|
|
389
520
|
else if (call.name === "delete_file") {
|
|
390
|
-
hallucinationDetector
|
|
391
|
-
.getConsistencyCheck()
|
|
392
|
-
.trackDeletedFile(filePath);
|
|
521
|
+
hallucinationDetector.getConsistencyCheck().trackDeletedFile(filePath);
|
|
393
522
|
}
|
|
394
523
|
}
|
|
395
|
-
pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
524
|
+
pluginManager.runOnToolEnd({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
396
525
|
if (result.display) {
|
|
397
526
|
onMeta?.("\n" + result.display + "\n");
|
|
398
527
|
}
|
|
399
528
|
else {
|
|
400
|
-
const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
|
|
529
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
|
|
401
530
|
onMeta?.("\n" + pc.dim(metaOut) + "\n");
|
|
402
531
|
}
|
|
403
532
|
if (result.diff) {
|
|
@@ -405,7 +534,7 @@ export class Agent {
|
|
|
405
534
|
}
|
|
406
535
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
407
536
|
const budget = contextManager.getBudget();
|
|
408
|
-
const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens);
|
|
537
|
+
const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens, boundedToolNames.has(call.name));
|
|
409
538
|
contextManager.addMessage({
|
|
410
539
|
role: "tool",
|
|
411
540
|
content: truncatedOutput,
|
|
@@ -428,8 +557,35 @@ export class Agent {
|
|
|
428
557
|
slog.logToolResult(call, result, duration, iteration);
|
|
429
558
|
}
|
|
430
559
|
if (contextManager.needsCompaction()) {
|
|
431
|
-
contextManager.compact();
|
|
432
|
-
|
|
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
|
+
}
|
|
433
589
|
}
|
|
434
590
|
}
|
|
435
591
|
if (anyToolFailed) {
|
|
@@ -449,13 +605,9 @@ export class Agent {
|
|
|
449
605
|
role: "user",
|
|
450
606
|
content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
|
|
451
607
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
}
|
|
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");
|
|
459
611
|
}
|
|
460
612
|
}
|
|
461
613
|
contextManager.addMessage({
|
|
@@ -483,9 +635,17 @@ export class Agent {
|
|
|
483
635
|
onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
|
|
484
636
|
}
|
|
485
637
|
}
|
|
638
|
+
// A tool call means the model kept working instead of just
|
|
639
|
+
// re-answering — the audit-re-answer leniency no longer applies.
|
|
640
|
+
suppressRepetitionRetry = false;
|
|
486
641
|
continue;
|
|
487
642
|
}
|
|
488
|
-
hallucinationDetector.getConfidenceCheck().setPreviousResponse(
|
|
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;
|
|
489
649
|
const hallucinationResult = await hallucinationDetector.validate(textContent);
|
|
490
650
|
if (hallucinationResult.status === "block") {
|
|
491
651
|
logger.warn(`Response blocked: ${hallucinationResult.reason}`);
|
|
@@ -517,34 +677,55 @@ export class Agent {
|
|
|
517
677
|
lastText = textContent;
|
|
518
678
|
break;
|
|
519
679
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
|
|
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).
|
|
534
698
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
}
|
|
538
723
|
contextManager.addMessage({
|
|
539
|
-
role: "
|
|
540
|
-
content:
|
|
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>`,
|
|
541
726
|
});
|
|
727
|
+
continue;
|
|
542
728
|
}
|
|
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
729
|
}
|
|
549
730
|
if (textContent) {
|
|
550
731
|
contextManager.addMessage({ role: "assistant", content: textContent });
|
|
@@ -596,11 +777,16 @@ export class Agent {
|
|
|
596
777
|
});
|
|
597
778
|
slog.logAudit(audit.summary, iteration);
|
|
598
779
|
auditRetries++;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
logger.warn(`Final audit still incomplete after ${auditRetries} retries —
|
|
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;
|
|
602
784
|
break;
|
|
603
785
|
}
|
|
786
|
+
// The next non-tool response is a forced re-answer of an
|
|
787
|
+
// already-completed task — allow one repetition without
|
|
788
|
+
// burning the hallucination budget (see retry branch).
|
|
789
|
+
suppressRepetitionRetry = true;
|
|
604
790
|
continue;
|
|
605
791
|
}
|
|
606
792
|
}
|
|
@@ -626,9 +812,13 @@ export class Agent {
|
|
|
626
812
|
};
|
|
627
813
|
}
|
|
628
814
|
return {
|
|
629
|
-
success: emptyResponseExhausted ? false : true,
|
|
815
|
+
success: emptyResponseExhausted || auditFailed ? false : true,
|
|
630
816
|
text: lastText,
|
|
631
|
-
error: emptyResponseExhausted
|
|
817
|
+
error: emptyResponseExhausted
|
|
818
|
+
? t("error.empty_response")
|
|
819
|
+
: auditFailed
|
|
820
|
+
? t("error.audit_failed", { summary: lastAuditSummary })
|
|
821
|
+
: undefined,
|
|
632
822
|
iterationCount: iteration,
|
|
633
823
|
contextUsed: tokensUsed,
|
|
634
824
|
contextLimit: budget.history,
|
|
@@ -690,6 +880,7 @@ export class Agent {
|
|
|
690
880
|
pluginManager.runOnSessionEnd({
|
|
691
881
|
logger,
|
|
692
882
|
sessionManager: sessionManager?.getActiveMeta(),
|
|
883
|
+
contextManager,
|
|
693
884
|
});
|
|
694
885
|
}
|
|
695
886
|
}
|