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