micro-models-agent 0.28.17 → 0.29.1
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/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +625 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
package/dist/core/agent.js
CHANGED
|
@@ -1,35 +1,15 @@
|
|
|
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
|
-
import { StepVerifier } from "../modules/execution/verifier";
|
|
10
7
|
const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
11
8
|
const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
|
|
12
|
-
const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
13
|
-
/** True when the text looks like a raw JSON tool payload (garbage to display). */
|
|
14
|
-
function isToolCallJson(text) {
|
|
15
|
-
const trimmed = text.trim();
|
|
16
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
17
|
-
try {
|
|
18
|
-
JSON.parse(trimmed);
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
9
|
export class Agent {
|
|
28
10
|
deps;
|
|
29
11
|
systemPromptAdded = false;
|
|
30
12
|
shutdownRequested = false;
|
|
31
|
-
abortController = null;
|
|
32
|
-
lastCompactionShown = 0;
|
|
33
13
|
constructor(deps) {
|
|
34
14
|
this.deps = deps;
|
|
35
15
|
}
|
|
@@ -51,45 +31,19 @@ export class Agent {
|
|
|
51
31
|
if (dynamic.length > 0) {
|
|
52
32
|
builder.addBlocks(dynamic);
|
|
53
33
|
}
|
|
54
|
-
const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? [])
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
]
|
|
63
|
-
: []);
|
|
34
|
+
const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? [])
|
|
35
|
+
.filter((s) => Boolean(s && s.trim() !== ""))
|
|
36
|
+
.map((content) => ({
|
|
37
|
+
content,
|
|
38
|
+
priority: "low",
|
|
39
|
+
essential: false,
|
|
40
|
+
estimatedTokens: this.deps.llmProvider.countTokens(content),
|
|
41
|
+
}));
|
|
64
42
|
if (pluginBlocks.length > 0) {
|
|
65
43
|
builder.addBlocks(pluginBlocks);
|
|
66
44
|
}
|
|
67
45
|
return builder.build();
|
|
68
46
|
}
|
|
69
|
-
getSystemPromptInfo() {
|
|
70
|
-
const { prompt, excluded } = this.buildSystemPrompt();
|
|
71
|
-
const tokenCount = this.deps.llmProvider.countTokens(prompt);
|
|
72
|
-
return { text: prompt, tokenCount, excluded };
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Some OpenAI-compatible backends (llama.cpp) omit `usage` from responses,
|
|
76
|
-
* leaving apiPromptTokens/apiCompletionTokens at 0. Fall back to local
|
|
77
|
-
* estimates so JSON results still carry meaningful token metrics.
|
|
78
|
-
*/
|
|
79
|
-
resolveUsageTokens(apiPromptTokens, apiCompletionTokens, estimatedPromptTokens, completionChars) {
|
|
80
|
-
if (apiPromptTokens > 0 || apiCompletionTokens > 0) {
|
|
81
|
-
return {
|
|
82
|
-
prompt: apiPromptTokens,
|
|
83
|
-
completion: apiCompletionTokens,
|
|
84
|
-
total: apiPromptTokens + apiCompletionTokens,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
// ~4 chars per token is a reasonable heuristic when the backend gives
|
|
88
|
-
// us nothing (matches the pre-tiktoken fallback elsewhere in the code).
|
|
89
|
-
const prompt = Math.max(1, estimatedPromptTokens);
|
|
90
|
-
const completion = Math.max(0, Math.ceil(completionChars / 4));
|
|
91
|
-
return { prompt, completion, total: prompt + completion };
|
|
92
|
-
}
|
|
93
47
|
refreshSystemPrompt() {
|
|
94
48
|
const { prompt } = this.buildSystemPrompt();
|
|
95
49
|
const current = this.deps.contextManager
|
|
@@ -117,8 +71,8 @@ export class Agent {
|
|
|
117
71
|
}
|
|
118
72
|
async run(input, onChunk, onMeta, onTool, onPhase) {
|
|
119
73
|
this.setScope();
|
|
120
|
-
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager,
|
|
121
|
-
const slog = new SessionLogger(sessionManager
|
|
74
|
+
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, } = this.deps;
|
|
75
|
+
const slog = new SessionLogger(sessionManager);
|
|
122
76
|
if (sessionManager && !sessionManager.getActive()) {
|
|
123
77
|
sessionManager.create();
|
|
124
78
|
logger.debug(`Session started: ${sessionManager.getActive()}`);
|
|
@@ -136,18 +90,6 @@ export class Agent {
|
|
|
136
90
|
logger,
|
|
137
91
|
sessionManager: sessionManager?.getActiveMeta(),
|
|
138
92
|
});
|
|
139
|
-
if (config.session?.baselineCheck !== false) {
|
|
140
|
-
const verifier = new StepVerifier(baseDir);
|
|
141
|
-
verifier
|
|
142
|
-
.runTypeCheck()
|
|
143
|
-
.then((tc) => {
|
|
144
|
-
if (!tc.passed) {
|
|
145
|
-
logger.warn(`Baseline typecheck has issues: ${tc.message?.slice(0, 500)}`);
|
|
146
|
-
onMeta?.(pc.yellow(`\n⚠ Baseline typecheck has issues\n`));
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
.catch(() => { });
|
|
150
|
-
}
|
|
151
93
|
}
|
|
152
94
|
contextManager.addMessage({ role: "user", content: input });
|
|
153
95
|
slog.logUser(input);
|
|
@@ -163,43 +105,25 @@ export class Agent {
|
|
|
163
105
|
return this.executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase);
|
|
164
106
|
}
|
|
165
107
|
async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
|
|
166
|
-
const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager,
|
|
167
|
-
const slog = new SessionLogger(sessionManager
|
|
168
|
-
this.abortController = new AbortController();
|
|
108
|
+
const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, } = this.deps;
|
|
109
|
+
const slog = new SessionLogger(sessionManager);
|
|
169
110
|
let iteration = 0;
|
|
170
111
|
let lastText = "";
|
|
171
112
|
let hallucinationRetries = 0;
|
|
172
113
|
let lastToolSignature = "";
|
|
173
114
|
let apiPromptTokens = 0;
|
|
174
115
|
let apiCompletionTokens = 0;
|
|
175
|
-
let apiCompletionChars = 0;
|
|
176
116
|
const MAX_HALLUCINATION_RETRIES = 3;
|
|
177
117
|
let consecutiveToolFailures = 0;
|
|
178
118
|
const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
179
|
-
let auditRetries = 0;
|
|
180
|
-
const MAX_AUDIT_RETRIES = 3;
|
|
181
|
-
let emptyResponseRetries = 0;
|
|
182
|
-
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
183
|
-
let emptyResponseExhausted = false;
|
|
184
|
-
let repeatedToolCount = 0;
|
|
185
|
-
const MAX_REPEATED_TOOL_CALLS = 2;
|
|
186
|
-
// Account for tool definitions in context budget (they're sent via body.tools, not messages)
|
|
187
|
-
const allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
|
|
188
|
-
const toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum +
|
|
189
|
-
Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
|
|
190
|
-
contextManager.setToolTokens(toolTokenEstimate);
|
|
191
119
|
while (iteration < config.maxToolIterations && !this.shutdownRequested) {
|
|
192
120
|
iteration++;
|
|
193
|
-
contextManager.noteIteration();
|
|
194
121
|
pluginManager.runOnBeforeThink({
|
|
195
122
|
iteration,
|
|
196
123
|
logger,
|
|
197
124
|
lastUserMessage: input,
|
|
198
125
|
contextManager,
|
|
199
126
|
onMeta,
|
|
200
|
-
sessionLog: {
|
|
201
|
-
plan: (event, detail, iter) => slog.logPlan(event, detail, iter),
|
|
202
|
-
},
|
|
203
127
|
});
|
|
204
128
|
if (contextManager.needsCompaction()) {
|
|
205
129
|
contextManager.compact();
|
|
@@ -208,13 +132,6 @@ export class Agent {
|
|
|
208
132
|
}
|
|
209
133
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
210
134
|
const budget = contextManager.getBudget();
|
|
211
|
-
const quality = contextManager.getQuality();
|
|
212
|
-
if (quality < QUALITY_TRIGGER_THRESHOLD &&
|
|
213
|
-
contextManager.getCompactionCount() > 0) {
|
|
214
|
-
contextManager.compact();
|
|
215
|
-
logger.warn(`Low context quality (${quality}%) — forced compaction`);
|
|
216
|
-
slog.logCompaction(`quality-triggered compaction (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%), iteration ${iteration}`, iteration, currentTokens, budget.history);
|
|
217
|
-
}
|
|
218
135
|
if (currentTokens > budget.history) {
|
|
219
136
|
contextManager.compact();
|
|
220
137
|
logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
|
|
@@ -222,7 +139,8 @@ export class Agent {
|
|
|
222
139
|
}
|
|
223
140
|
this.refreshSystemPrompt();
|
|
224
141
|
const history = contextManager.getActiveHistory();
|
|
225
|
-
|
|
142
|
+
const allTools = toolExecutor.getToolDefinitions(this.deps.toolTags);
|
|
143
|
+
slog.logToolDefs(allTools.length, allTools.map((t) => t.name), iteration);
|
|
226
144
|
let textContent = "";
|
|
227
145
|
let reasoningContent = "";
|
|
228
146
|
const toolCalls = [];
|
|
@@ -230,12 +148,8 @@ export class Agent {
|
|
|
230
148
|
let emittedReasoning = false;
|
|
231
149
|
const textChunks = [];
|
|
232
150
|
this.emitPhase(iteration, "thinking", onPhase);
|
|
233
|
-
const llmStart = Date.now();
|
|
234
|
-
logger.logLLMRequest(config.model, history.length, input, "agent");
|
|
235
151
|
try {
|
|
236
|
-
for await (const chunk of llmProvider.chat(history,
|
|
237
|
-
if (this.shutdownRequested)
|
|
238
|
-
break;
|
|
152
|
+
for await (const chunk of llmProvider.chat(history, allTools)) {
|
|
239
153
|
if (chunk.type === "text" && chunk.content) {
|
|
240
154
|
if (emittedReasoning && !textContent) {
|
|
241
155
|
onMeta?.("\n\n");
|
|
@@ -275,11 +189,6 @@ export class Agent {
|
|
|
275
189
|
}
|
|
276
190
|
}
|
|
277
191
|
catch (err) {
|
|
278
|
-
if (this.shutdownRequested || err?.name === "AbortError") {
|
|
279
|
-
logger.info("LLM call aborted (interrupt)");
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
|
|
283
192
|
logger.error(`LLM call failed: ${err.message}`);
|
|
284
193
|
slog.logError(err.message);
|
|
285
194
|
pluginManager.runOnError({ iteration, logger }, err);
|
|
@@ -293,22 +202,10 @@ export class Agent {
|
|
|
293
202
|
finally {
|
|
294
203
|
this.emitPhase(iteration, "done", onPhase);
|
|
295
204
|
}
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
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) {
|
|
205
|
+
// Display buffered text only if no tool call in this response.
|
|
206
|
+
// When a tool call is present, text is just the model describing
|
|
207
|
+
// its tool call (e.g. raw JSON args) — suppress it.
|
|
208
|
+
if (!sawToolCall && textChunks.length > 0) {
|
|
312
209
|
for (const chunk of textChunks) {
|
|
313
210
|
const textOut = pluginManager.runOnText({ iteration, logger }, chunk);
|
|
314
211
|
onChunk?.(textOut);
|
|
@@ -330,19 +227,8 @@ export class Agent {
|
|
|
330
227
|
.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
|
|
331
228
|
.join("|");
|
|
332
229
|
if (signature && signature === lastToolSignature) {
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
});
|
|
230
|
+
logger.debug("Exit-on-complete: repeated identical tool call, stopping");
|
|
231
|
+
break;
|
|
346
232
|
}
|
|
347
233
|
lastToolSignature = signature;
|
|
348
234
|
}
|
|
@@ -374,35 +260,26 @@ export class Agent {
|
|
|
374
260
|
pluginManager.runOnToolStart({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
375
261
|
onTool?.({ type: "start", tool: call.name, args: call.arguments });
|
|
376
262
|
slog.logToolCall(call, iteration);
|
|
377
|
-
const
|
|
378
|
-
const result = await toolExecutor.execute(call, this.abortController?.signal);
|
|
263
|
+
const result = await toolExecutor.execute(call);
|
|
379
264
|
const duration = Date.now() - startTime;
|
|
380
265
|
if (!result.success)
|
|
381
266
|
anyToolFailed = true;
|
|
382
|
-
if (result.success && call.arguments.path) {
|
|
383
|
-
const filePath = String(call.arguments.path);
|
|
384
|
-
if (call.name === "write_file" || call.name === "edit_file") {
|
|
385
|
-
hallucinationDetector
|
|
386
|
-
.getConsistencyCheck()
|
|
387
|
-
.trackCreatedFile(filePath);
|
|
388
|
-
}
|
|
389
|
-
else if (call.name === "delete_file") {
|
|
390
|
-
hallucinationDetector
|
|
391
|
-
.getConsistencyCheck()
|
|
392
|
-
.trackDeletedFile(filePath);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
267
|
pluginManager.runOnToolEnd({ iteration, logger }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
396
268
|
if (result.display) {
|
|
397
269
|
onMeta?.("\n" + result.display + "\n");
|
|
398
270
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
onMeta?.("\n" + pc.dim(metaOut) + "\n");
|
|
402
|
-
}
|
|
271
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
|
|
272
|
+
onMeta?.("\n" + pc.dim(metaOut) + "\n");
|
|
403
273
|
if (result.diff) {
|
|
404
274
|
onMeta?.("\n" + result.diff + "\n");
|
|
405
275
|
}
|
|
276
|
+
onTool?.({
|
|
277
|
+
type: "end",
|
|
278
|
+
tool: call.name,
|
|
279
|
+
args: call.arguments,
|
|
280
|
+
duration,
|
|
281
|
+
error: !result.success,
|
|
282
|
+
});
|
|
406
283
|
const currentTokens = contextManager.getEstimatedTokens();
|
|
407
284
|
const budget = contextManager.getBudget();
|
|
408
285
|
const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens);
|
|
@@ -411,17 +288,6 @@ export class Agent {
|
|
|
411
288
|
content: truncatedOutput,
|
|
412
289
|
name: call.name,
|
|
413
290
|
tool_call_id: call.id,
|
|
414
|
-
success: result.success,
|
|
415
|
-
arguments: call.arguments,
|
|
416
|
-
});
|
|
417
|
-
const tokensAfterTool = contextManager.getEstimatedTokens();
|
|
418
|
-
onTool?.({
|
|
419
|
-
type: "end",
|
|
420
|
-
tool: call.name,
|
|
421
|
-
args: call.arguments,
|
|
422
|
-
duration,
|
|
423
|
-
error: !result.success,
|
|
424
|
-
ctxDelta: tokensAfterTool - tokensBeforeTool,
|
|
425
291
|
});
|
|
426
292
|
summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
|
|
427
293
|
if (config.session.autoSave) {
|
|
@@ -439,9 +305,7 @@ export class Agent {
|
|
|
439
305
|
consecutiveToolFailures = 0;
|
|
440
306
|
}
|
|
441
307
|
if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
|
|
442
|
-
const recoveryMsg = t("exec.consecutive_failures_recovery", {
|
|
443
|
-
count: consecutiveToolFailures,
|
|
444
|
-
});
|
|
308
|
+
const recoveryMsg = t("exec.consecutive_failures_recovery", { count: consecutiveToolFailures });
|
|
445
309
|
logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
|
|
446
310
|
const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
|
|
447
311
|
const taskReminder = t("exec.task_reminder", { task: taskSnippet });
|
|
@@ -449,44 +313,14 @@ export class Agent {
|
|
|
449
313
|
role: "user",
|
|
450
314
|
content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
|
|
451
315
|
});
|
|
452
|
-
if (sessionManager) {
|
|
453
|
-
const activeSession = sessionManager.getActiveMeta();
|
|
454
|
-
if (activeSession) {
|
|
455
|
-
const memDir = join(baseDir, ".mma", "memory");
|
|
456
|
-
const memStore = new MemoryStore(memDir);
|
|
457
|
-
memStore.appendRule("errors", `${consecutiveToolFailures} consecutive tool failures`, "Multiple tools failing suggests environment or configuration issue", "Check dependencies, verify file paths, try write_file directly instead of shell commands");
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
316
|
}
|
|
461
317
|
contextManager.addMessage({
|
|
462
318
|
role: "user",
|
|
463
319
|
content: `<system-summary>${summaries.join("\n")}</system-summary>`,
|
|
464
320
|
});
|
|
465
|
-
const ui = this.deps.config.ui;
|
|
466
|
-
if (ui?.showContextStats) {
|
|
467
|
-
const ctxTokens = contextManager.getEstimatedTokens();
|
|
468
|
-
const ctxBudget = contextManager.getBudget();
|
|
469
|
-
const ctxPct = Math.min(100, Math.round((ctxTokens / ctxBudget.history) * 100));
|
|
470
|
-
const barLen = 10;
|
|
471
|
-
const filled = Math.round((ctxPct / 100) * barLen);
|
|
472
|
-
const ctxBar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
|
|
473
|
-
const pctColor = ctxPct >= 75 ? pc.yellow : pc.dim;
|
|
474
|
-
const compCount = contextManager.getCompactionCount();
|
|
475
|
-
const quality = contextManager.getQuality();
|
|
476
|
-
const qualityColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
|
|
477
|
-
onMeta?.(`\n ${ctxBar} ${pctColor(`${ctxPct}%`)} ${pc.dim(`ctx: ${ctxTokens}/${ctxBudget.history}`)} ${pc.dim(`compactions: ${compCount}`)} ${qualityColor(`quality: ${quality}%`)}\n`);
|
|
478
|
-
}
|
|
479
|
-
else if (ui?.showCompaction) {
|
|
480
|
-
const compCount = contextManager.getCompactionCount();
|
|
481
|
-
if (compCount > this.lastCompactionShown) {
|
|
482
|
-
this.lastCompactionShown = compCount;
|
|
483
|
-
onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
321
|
continue;
|
|
487
322
|
}
|
|
488
|
-
hallucinationDetector.
|
|
489
|
-
const hallucinationResult = await hallucinationDetector.validate(textContent);
|
|
323
|
+
const hallucinationResult = hallucinationDetector.validate(textContent);
|
|
490
324
|
if (hallucinationResult.status === "block") {
|
|
491
325
|
logger.warn(`Response blocked: ${hallucinationResult.reason}`);
|
|
492
326
|
return {
|
|
@@ -500,27 +334,17 @@ export class Agent {
|
|
|
500
334
|
}
|
|
501
335
|
if (hallucinationResult.status === "warn") {
|
|
502
336
|
logger.warn(`Hallucination warning: ${hallucinationResult.reason}`);
|
|
503
|
-
const
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
else if (onChunk) {
|
|
508
|
-
onChunk(`\n${warnLine}\n`);
|
|
337
|
+
const warnPrefix = t("hall.uncertainty_prefix");
|
|
338
|
+
if (onChunk) {
|
|
339
|
+
onChunk(warnPrefix);
|
|
509
340
|
}
|
|
341
|
+
lastText = warnPrefix + lastText;
|
|
510
342
|
}
|
|
511
343
|
if (hallucinationResult.status === "retry") {
|
|
512
|
-
if (this.deps.exitOnComplete
|
|
344
|
+
if (this.deps.exitOnComplete) {
|
|
513
345
|
logger.debug("Exit-on-complete: stopping on first response");
|
|
514
|
-
// Save the response BEFORE breaking — lastText is still the
|
|
515
|
-
// previous (tool-only) iteration's text, so without this the
|
|
516
|
-
// final answer is lost (reported text: "").
|
|
517
|
-
lastText = textContent;
|
|
518
346
|
break;
|
|
519
347
|
}
|
|
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
348
|
if (hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
|
|
525
349
|
logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
|
|
526
350
|
return {
|
|
@@ -548,40 +372,11 @@ export class Agent {
|
|
|
548
372
|
}
|
|
549
373
|
if (textContent) {
|
|
550
374
|
contextManager.addMessage({ role: "assistant", content: textContent });
|
|
551
|
-
|
|
552
|
-
slog.saveAssistantMessage(textContent);
|
|
553
|
-
}
|
|
375
|
+
slog.saveAssistantMessage(textContent);
|
|
554
376
|
slog.logAssistant(textContent, reasoningContent, undefined, iteration);
|
|
555
377
|
}
|
|
556
378
|
lastText = textContent;
|
|
557
|
-
{
|
|
558
|
-
const decisionPatterns = [
|
|
559
|
-
...textContent.matchAll(/(?:plan|decided|decision|решено|план|решение):\s*(.+?)(?:\n|$)/gi),
|
|
560
|
-
];
|
|
561
|
-
for (const match of decisionPatterns) {
|
|
562
|
-
hallucinationDetector
|
|
563
|
-
.getConsistencyCheck()
|
|
564
|
-
.trackDecision(match[1].trim(), "agent_response");
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
379
|
if (!sawToolCall) {
|
|
568
|
-
// Guard: the model returned an EMPTY final response (no text, no
|
|
569
|
-
// tool calls — often just reasoning content after context
|
|
570
|
-
// compaction). Nudge it to produce a real answer instead of
|
|
571
|
-
// silently finishing with text: "".
|
|
572
|
-
if (!textContent?.trim()) {
|
|
573
|
-
if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
|
|
574
|
-
emptyResponseRetries++;
|
|
575
|
-
logger.warn(`Empty response on iteration ${iteration} (retry ${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
|
|
576
|
-
contextManager.addMessage({
|
|
577
|
-
role: "user",
|
|
578
|
-
content: `<system-summary>Your previous response was empty. Answer the user's task now with a final text response or call a tool. Do not reply with reasoning only.</system-summary>`,
|
|
579
|
-
});
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
emptyResponseExhausted = true;
|
|
583
|
-
logger.warn(`Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
|
|
584
|
-
}
|
|
585
380
|
if (this.deps.finalAudit) {
|
|
586
381
|
const audit = await this.deps.finalAudit();
|
|
587
382
|
if (audit && !audit.passed) {
|
|
@@ -595,10 +390,7 @@ export class Agent {
|
|
|
595
390
|
})}</system-summary>`,
|
|
596
391
|
});
|
|
597
392
|
slog.logAudit(audit.summary, iteration);
|
|
598
|
-
|
|
599
|
-
if (auditRetries >= MAX_AUDIT_RETRIES ||
|
|
600
|
-
iteration >= config.maxToolIterations - 1) {
|
|
601
|
-
logger.warn(`Final audit still incomplete after ${auditRetries} retries — finishing anyway`);
|
|
393
|
+
if (iteration >= config.maxToolIterations - 1) {
|
|
602
394
|
break;
|
|
603
395
|
}
|
|
604
396
|
continue;
|
|
@@ -609,7 +401,6 @@ export class Agent {
|
|
|
609
401
|
}
|
|
610
402
|
const tokensUsed = contextManager.getEstimatedTokens();
|
|
611
403
|
const budget = contextManager.getBudget();
|
|
612
|
-
const usageTokens = this.resolveUsageTokens(apiPromptTokens, apiCompletionTokens, tokensUsed, apiCompletionChars);
|
|
613
404
|
if (iteration >= config.maxToolIterations) {
|
|
614
405
|
return {
|
|
615
406
|
success: false,
|
|
@@ -618,49 +409,26 @@ export class Agent {
|
|
|
618
409
|
iterationCount: iteration,
|
|
619
410
|
contextUsed: tokensUsed,
|
|
620
411
|
contextLimit: budget.history,
|
|
621
|
-
promptTokens:
|
|
622
|
-
completionTokens:
|
|
623
|
-
totalTokens:
|
|
624
|
-
compactionCount: contextManager.getCompactionCount(),
|
|
625
|
-
contextQuality: contextManager.getQuality(),
|
|
412
|
+
promptTokens: apiPromptTokens,
|
|
413
|
+
completionTokens: apiCompletionTokens,
|
|
414
|
+
totalTokens: apiPromptTokens + apiCompletionTokens,
|
|
626
415
|
};
|
|
627
416
|
}
|
|
628
417
|
return {
|
|
629
|
-
success:
|
|
418
|
+
success: true,
|
|
630
419
|
text: lastText,
|
|
631
|
-
error: emptyResponseExhausted ? t("error.empty_response") : undefined,
|
|
632
420
|
iterationCount: iteration,
|
|
633
421
|
contextUsed: tokensUsed,
|
|
634
422
|
contextLimit: budget.history,
|
|
635
|
-
promptTokens:
|
|
636
|
-
completionTokens:
|
|
637
|
-
totalTokens:
|
|
638
|
-
compactionCount: contextManager.getCompactionCount(),
|
|
639
|
-
contextQuality: contextManager.getQuality(),
|
|
423
|
+
promptTokens: apiPromptTokens,
|
|
424
|
+
completionTokens: apiCompletionTokens,
|
|
425
|
+
totalTokens: apiPromptTokens + apiCompletionTokens,
|
|
640
426
|
};
|
|
641
427
|
}
|
|
642
428
|
clearContext() {
|
|
643
429
|
this.deps.contextManager.clear();
|
|
644
430
|
this.systemPromptAdded = false;
|
|
645
431
|
}
|
|
646
|
-
async reconfigure(config) {
|
|
647
|
-
const { OpenAICompatProvider } = await import("../llm/openai-compat");
|
|
648
|
-
const { TokenCounter } = await import("../llm/token-counter");
|
|
649
|
-
const newProvider = new OpenAICompatProvider({
|
|
650
|
-
model: config.model,
|
|
651
|
-
baseUrl: config.provider.baseUrl,
|
|
652
|
-
apiKey: config.provider.apiKey,
|
|
653
|
-
contextWindow: config.contextWindow,
|
|
654
|
-
retry: config.retry,
|
|
655
|
-
rateLimits: config.security?.rateLimits,
|
|
656
|
-
});
|
|
657
|
-
this.deps.llmProvider = newProvider;
|
|
658
|
-
this.deps.toolExecutor.updateProvider(newProvider);
|
|
659
|
-
this.deps.toolExecutor.ctx.llmProvider = newProvider;
|
|
660
|
-
const newTokenCounter = new TokenCounter(config.model);
|
|
661
|
-
this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
|
|
662
|
-
this.deps.config = config;
|
|
663
|
-
}
|
|
664
432
|
setContext(messages) {
|
|
665
433
|
const { contextManager } = this.deps;
|
|
666
434
|
contextManager.clear();
|
|
@@ -679,14 +447,12 @@ export class Agent {
|
|
|
679
447
|
}
|
|
680
448
|
shutdown() {
|
|
681
449
|
this.shutdownRequested = true;
|
|
682
|
-
this.abortController?.abort();
|
|
683
450
|
const { pluginManager, logger, sessionManager, contextManager } = this.deps;
|
|
684
451
|
contextManager.onCompact = null;
|
|
685
452
|
const killed = processRegistry.killAll();
|
|
686
453
|
if (killed > 0) {
|
|
687
454
|
logger.info(`Killed ${killed} background process(es) on shutdown`);
|
|
688
455
|
}
|
|
689
|
-
logger.closeSessionLog();
|
|
690
456
|
pluginManager.runOnSessionEnd({
|
|
691
457
|
logger,
|
|
692
458
|
sessionManager: sessionManager?.getActiveMeta(),
|