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.
Files changed (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +625 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
@@ -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?.() ?? []).flatMap((content) => content && content.trim() !== ""
55
- ? [
56
- {
57
- content,
58
- priority: "low",
59
- essential: false,
60
- estimatedTokens: this.deps.llmProvider.countTokens(content),
61
- },
62
- ]
63
- : []);
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, baseDir, } = this.deps;
121
- const slog = new SessionLogger(sessionManager, logger);
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, baseDir, } = this.deps;
167
- const slog = new SessionLogger(sessionManager, logger);
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
- slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
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, allToolsForBudget, this.abortController?.signal)) {
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
- // Track response length so token metrics stay meaningful even when
297
- // the backend omits `usage` from the response.
298
- apiCompletionChars += (textContent || reasoningContent).length;
299
- logger.logLLMResponse(config.model, (textContent || reasoningContent).length, Date.now() - llmStart, undefined, "agent");
300
- if (this.shutdownRequested) {
301
- break;
302
- }
303
- // Show the model's commentary text. When a tool call accompanies the
304
- // response, keep the text too (opencode-like narration), unless it is
305
- // a raw JSON payload that small models sometimes emit instead of
306
- // describing the call. `toolComments: false` restores the old behavior
307
- // of suppressing text next to a tool call.
308
- const toolComments = this.deps.config.ui?.toolComments ?? true;
309
- const showText = textChunks.length > 0 &&
310
- (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
311
- if (showText) {
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
- // A repeated identical tool call is often the model re-running
334
- // a command after a confusing result. Give it one more chance
335
- // to produce a final text answer instead of stopping with
336
- // text: "" (observed on 08-r4: bash re-run → empty result).
337
- repeatedToolCount++;
338
- if (repeatedToolCount >= MAX_REPEATED_TOOL_CALLS) {
339
- logger.debug("Exit-on-complete: repeated identical tool call, stopping");
340
- break;
341
- }
342
- contextManager.addMessage({
343
- role: "user",
344
- content: `<system-summary>You just called the same tool with identical arguments. If the task is done, answer with a final text response NOW. If the command failed, try a different approach.</system-summary>`,
345
- });
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 tokensBeforeTool = contextManager.getEstimatedTokens();
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
- else {
400
- const metaOut = pluginManager.runOnMeta({ iteration, logger }, result.output);
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.getConfidenceCheck().setPreviousResponse(lastText);
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 warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
504
- if (onMeta) {
505
- onMeta(`\n${pc.yellow(warnLine)}\n`);
506
- }
507
- else if (onChunk) {
508
- onChunk(`\n${warnLine}\n`);
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 && textContent?.trim()) {
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
- if (config.session.autoSave) {
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
- auditRetries++;
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: usageTokens.prompt,
622
- completionTokens: usageTokens.completion,
623
- totalTokens: usageTokens.total,
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: emptyResponseExhausted ? false : true,
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: usageTokens.prompt,
636
- completionTokens: usageTokens.completion,
637
- totalTokens: usageTokens.total,
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(),