micro-models-agent 0.63.0 → 1.1.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/CHANGELOG.md +174 -0
- package/dist/cli/cache-line.js +30 -0
- package/dist/cli/command-suggest.js +38 -0
- package/dist/cli/commands.js +285 -60
- package/dist/cli/completer.js +16 -16
- package/dist/cli/json-payload.js +32 -0
- package/dist/cli/main.js +165 -77
- package/dist/cli/plugin-commands.js +5 -4
- package/dist/cli/relaunch.js +37 -0
- package/dist/cli/repl-commands.js +441 -307
- package/dist/cli/repl.js +360 -83
- package/dist/cli/run-result.js +12 -6
- package/dist/cli/security-commands.js +64 -60
- package/dist/cli/setup-order.js +57 -0
- package/dist/cli/setup-prompt.js +49 -0
- package/dist/cli/setup.js +52 -48
- package/dist/config/budget.js +48 -0
- package/dist/config/config.js +132 -70
- package/dist/config/defaults.js +37 -11
- package/dist/config/domains.js +9 -50
- package/dist/config/utils.js +56 -0
- package/dist/core/agent/audit-gate.js +49 -0
- package/dist/core/agent/compaction.js +89 -0
- package/dist/core/agent/constants.js +61 -0
- package/dist/core/agent/context-renderer.js +40 -0
- package/dist/core/agent/hallucination-gate.js +87 -0
- package/dist/core/agent/loop-state.js +53 -0
- package/dist/core/agent/prefix-monitor.js +101 -0
- package/dist/core/agent/reasoning-resolver.js +56 -0
- package/dist/core/agent/token-tracker.js +96 -0
- package/dist/core/agent/tool-batch.js +237 -0
- package/dist/core/agent/tool-output.js +62 -0
- package/dist/core/agent-moe.js +214 -69
- package/dist/core/agent.js +506 -546
- package/dist/core/bootstrap.js +297 -98
- package/dist/core/crash-handler.js +2 -1
- package/dist/core/prompt-builder.js +3 -0
- package/dist/core/prompt-overflow.js +307 -0
- package/dist/core/session-logger.js +34 -2
- package/dist/i18n/en.json +8 -4
- package/dist/i18n/ru.json +8 -4
- package/dist/index.js +5 -1
- package/dist/llm/cache-usage.js +76 -0
- package/dist/llm/image-utils.js +20 -16
- package/dist/llm/llm-errors.js +41 -0
- package/dist/llm/model-loader.js +30 -0
- package/dist/llm/openai-compat.js +287 -101
- package/dist/llm/orchestrator.js +140 -68
- package/dist/llm/provider-budget.js +68 -0
- package/dist/llm/provider.js +0 -1
- package/dist/llm/stream-state.js +26 -0
- package/dist/llm/token-counter.js +28 -0
- package/dist/logger/app-logger.js +12 -15
- package/dist/main.js +1755 -841
- package/dist/migration/detect.js +3 -1
- package/dist/modules/browser/actions.js +0 -3
- package/dist/modules/browser/bridge-client.js +2 -0
- package/dist/modules/browser/bridge-server.mjs +37 -4
- package/dist/modules/browser/driver.js +46 -4
- package/dist/modules/certification/cli.js +85 -42
- package/dist/modules/certification/loader.js +15 -1
- package/dist/modules/certification/manifest.js +126 -15
- package/dist/modules/certification/runner.js +4 -26
- package/dist/modules/certification/scenarios.js +184 -5
- package/dist/modules/certification/syntax-scenarios.js +51 -0
- package/dist/modules/context/chunk-query.js +25 -5
- package/dist/modules/context/fact-extractor.js +6 -2
- package/dist/modules/context/manager.js +23 -7
- package/dist/modules/execution/audit-runners.js +7 -1
- package/dist/modules/execution/auditor.js +3 -3
- package/dist/modules/execution/execution-plugin.js +22 -15
- package/dist/modules/execution/input-from.js +46 -0
- package/dist/modules/execution/module.js +107 -18
- package/dist/modules/execution/moe-executor.js +166 -54
- package/dist/modules/execution/plan-actions.js +524 -0
- package/dist/modules/execution/plan-steps.js +23 -0
- package/dist/modules/execution/plan-store.js +15 -3
- package/dist/modules/execution/plan-tool.js +6 -488
- package/dist/modules/execution/plan-validator.js +24 -0
- package/dist/modules/execution/stuck-detector.js +3 -18
- package/dist/modules/execution/tracker.js +14 -5
- package/dist/modules/execution/transient-error.js +30 -0
- package/dist/modules/execution/verifier.js +94 -7
- package/dist/modules/execution/windows-commands.js +11 -0
- package/dist/modules/hallucination/confidence.js +36 -23
- package/dist/modules/hallucination/consistency.js +3 -0
- package/dist/modules/hallucination/detector.js +8 -3
- package/dist/modules/hallucination/factual.js +26 -7
- package/dist/modules/hallucination/llm-judge.js +12 -2
- package/dist/modules/indexer/map-command.js +35 -0
- package/dist/modules/indexer/map-select.js +87 -0
- package/dist/modules/indexer/module.js +34 -22
- package/dist/modules/indexer/symbols.js +189 -0
- package/dist/modules/indexer/walker.js +96 -42
- package/dist/modules/lsp/check-tool.js +2 -1
- package/dist/modules/lsp/client.js +49 -32
- package/dist/modules/lsp/config.js +55 -2
- package/dist/modules/lsp/module.js +38 -5
- package/dist/modules/lsp/probe.js +4 -3
- package/dist/modules/lsp/project-root.js +41 -1
- package/dist/modules/lsp/startup-check.js +12 -4
- package/dist/modules/mcp/client.js +153 -104
- package/dist/modules/mcp/module.js +165 -41
- package/dist/modules/memory/module.js +4 -3
- package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
- package/dist/modules/plugins/manager.js +47 -84
- package/dist/modules/pricing/index.js +17 -7
- package/dist/modules/pricing/prices.js +30 -12
- package/dist/modules/processes/index.js +1 -0
- package/dist/modules/processes/kill-tree.js +56 -0
- package/dist/modules/processes/registry.js +2 -54
- package/dist/modules/providers/cache.js +23 -0
- package/dist/modules/providers/factory.js +28 -0
- package/dist/modules/providers/fallback.js +7 -5
- package/dist/modules/providers/health.js +2 -1
- package/dist/modules/providers/index.js +1 -0
- package/dist/modules/providers/manager.js +17 -2
- package/dist/modules/providers/presets.js +79 -6
- package/dist/modules/reasoning/policy.js +40 -0
- package/dist/modules/reasoning/probe.js +111 -0
- package/dist/modules/security/audit-notifier.js +42 -27
- package/dist/modules/security/command-validator.js +25 -20
- package/dist/modules/security/encryption.js +6 -12
- package/dist/modules/security/network-validator.js +76 -5
- package/dist/modules/security/path-validator.js +77 -34
- package/dist/modules/security/rate-limiter.js +11 -0
- package/dist/modules/security/security-policies.js +1 -1
- package/dist/modules/security/session-encryption.js +13 -2
- package/dist/modules/security/session-isolation.js +2 -9
- package/dist/modules/session/manager.js +11 -0
- package/dist/modules/session/module.js +11 -3
- package/dist/modules/session/store.js +41 -5
- package/dist/modules/skills/loader.js +7 -1
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/changelog-reader.js +94 -0
- package/dist/modules/updater/dev-detect.js +17 -0
- package/dist/modules/updater/index.js +1 -0
- package/dist/modules/updater/module.js +14 -3
- package/dist/output/bus.js +32 -0
- package/dist/output/channel.js +233 -0
- package/dist/output/format.js +14 -0
- package/dist/output/index.js +7 -0
- package/dist/output/json-sink.js +22 -0
- package/dist/output/machine.js +8 -0
- package/dist/output/session-sink.js +27 -0
- package/dist/output/types.js +1 -0
- package/dist/tools/approve.js +6 -2
- package/dist/tools/attach-image.js +11 -11
- package/dist/tools/auto-fixer.js +198 -0
- package/dist/tools/bash.js +142 -89
- package/dist/tools/chunk-query.js +10 -6
- package/dist/tools/download-file.js +1 -1
- package/dist/tools/edit-file.js +20 -2
- package/dist/tools/executor.js +54 -9
- package/dist/tools/glob-tool.js +7 -0
- package/dist/tools/grep-tool.js +15 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/list-dir.js +3 -1
- package/dist/tools/load-skill.js +2 -1
- package/dist/tools/mcp-call.js +1 -1
- package/dist/tools/move-file.js +5 -4
- package/dist/tools/path-utils.js +7 -0
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/prompt-io.js +28 -0
- package/dist/tools/question.js +12 -12
- package/dist/tools/scope-request.js +91 -0
- package/dist/tools/session-info.js +44 -0
- package/dist/tools/set-thinking.js +71 -0
- package/dist/tools/subagent.js +50 -9
- package/dist/tools/syntax-validator.js +177 -0
- package/dist/tools/user-input.js +16 -9
- package/dist/tools/write-file.js +17 -1
- package/dist/ui/diff.js +10 -0
- package/dist/ui/line-editor.js +179 -26
- package/dist/ui/line-math.js +20 -3
- package/dist/ui/md-formatter.js +100 -10
- package/dist/ui/output.js +5 -4
- package/dist/ui/plan-view.js +2 -7
- package/dist/ui/renderer.js +89 -85
- package/dist/ui/spinner.js +14 -4
- package/dist/utils/error.js +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/retry.js +17 -0
- package/dist/utils/sleep.js +23 -0
- package/dist/utils/truncate.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { FORCED_COMPACTION_COOLDOWN, QUALITY_TRIGGER_THRESHOLD } from "./constants";
|
|
2
|
+
/**
|
|
3
|
+
* Единая точка компакции контекста. В agent.ts логика дублировалась в 4 местах:
|
|
4
|
+
* 1) интервал (needsCompaction) в начале итерации;
|
|
5
|
+
* 2) принудительно по низкому качеству (< QUALITY_TRIGGER_THRESHOLD) с cooldown;
|
|
6
|
+
* 3) принудительно по переполнению бюджета истории;
|
|
7
|
+
* 4) после добавления результата тула.
|
|
8
|
+
*
|
|
9
|
+
* Все четыре консолидированы здесь и выполняют одинаковый контракт:
|
|
10
|
+
* компакция → логирование в session log → обогащение детектора халлюцинаций
|
|
11
|
+
* известными файлами из сводки.
|
|
12
|
+
*/
|
|
13
|
+
export class CompactionService {
|
|
14
|
+
deps;
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
this.deps = deps;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Пре-итерационные проверки: интервал → качество → переполнение.
|
|
20
|
+
* Порядок сохранён из оригинального цикла: каждая следующая проверка
|
|
21
|
+
* видит контекст уже ПОСЛЕ предыдущей компакции.
|
|
22
|
+
*/
|
|
23
|
+
compactIfNeeded(state) {
|
|
24
|
+
this.compactOnInterval(state.iteration);
|
|
25
|
+
this.compactOnQuality(state);
|
|
26
|
+
this.compactOnOverflow(state.iteration);
|
|
27
|
+
}
|
|
28
|
+
/** Проверка после добавления результата тула в контекст. */
|
|
29
|
+
compactAfterTool(state) {
|
|
30
|
+
const { contextManager } = this.deps;
|
|
31
|
+
if (!contextManager.needsCompaction())
|
|
32
|
+
return;
|
|
33
|
+
const result = contextManager.compact();
|
|
34
|
+
if (!result)
|
|
35
|
+
return;
|
|
36
|
+
this.deps.logger.debug("Context compacted after tool result");
|
|
37
|
+
this.deps.slog.logCompaction({ reason: "after_tool", iteration: state.iteration, ...result });
|
|
38
|
+
this.deps.hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
39
|
+
}
|
|
40
|
+
compactOnInterval(iteration) {
|
|
41
|
+
const { contextManager } = this.deps;
|
|
42
|
+
if (!contextManager.needsCompaction())
|
|
43
|
+
return;
|
|
44
|
+
const result = contextManager.compact();
|
|
45
|
+
if (!result)
|
|
46
|
+
return;
|
|
47
|
+
this.deps.logger.debug("Context compacted");
|
|
48
|
+
this.deps.slog.logCompaction({ reason: "interval", iteration, ...result });
|
|
49
|
+
this.deps.hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
50
|
+
}
|
|
51
|
+
compactOnQuality(state) {
|
|
52
|
+
const { contextManager } = this.deps;
|
|
53
|
+
const iteration = state.iteration;
|
|
54
|
+
const quality = contextManager.getQuality();
|
|
55
|
+
const cooldownElapsed = iteration - state.lastForcedCompactionIteration >= FORCED_COMPACTION_COOLDOWN;
|
|
56
|
+
if (quality < QUALITY_TRIGGER_THRESHOLD &&
|
|
57
|
+
contextManager.getCompactionCount() > 0 &&
|
|
58
|
+
cooldownElapsed) {
|
|
59
|
+
state.lastForcedCompactionIteration = iteration;
|
|
60
|
+
const result = contextManager.compact();
|
|
61
|
+
if (!result)
|
|
62
|
+
return;
|
|
63
|
+
this.deps.logger.warn(`Low context quality (${quality}%) — forced compaction`);
|
|
64
|
+
this.deps.slog.logCompaction({
|
|
65
|
+
reason: `quality-triggered (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%)`,
|
|
66
|
+
iteration,
|
|
67
|
+
...result,
|
|
68
|
+
});
|
|
69
|
+
this.deps.hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
compactOnOverflow(iteration) {
|
|
73
|
+
const { contextManager } = this.deps;
|
|
74
|
+
const currentTokens = contextManager.getEstimatedTokens();
|
|
75
|
+
const budget = contextManager.getBudget();
|
|
76
|
+
if (currentTokens <= budget.history)
|
|
77
|
+
return;
|
|
78
|
+
const result = contextManager.compact();
|
|
79
|
+
if (!result)
|
|
80
|
+
return;
|
|
81
|
+
this.deps.logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
|
|
82
|
+
this.deps.slog.logCompaction({
|
|
83
|
+
reason: `overflow (${currentTokens} > ${budget.history})`,
|
|
84
|
+
iteration,
|
|
85
|
+
...result,
|
|
86
|
+
});
|
|
87
|
+
this.deps.hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Константы агентного цикла. Вынесены из agent.ts, чтобы:
|
|
3
|
+
* 1) не растекаться магическими числами по модулям;
|
|
4
|
+
* 2) быть переиспользуемыми между extracted-модулями (compaction, tool-batch и т.д.);
|
|
5
|
+
* 3) каждая константа несёт JSDoc с обоснованием значения.
|
|
6
|
+
*/
|
|
7
|
+
/** Макс. доля бюджета истории под результат одного тула (по токенам). */
|
|
8
|
+
export const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
9
|
+
/** Абсолютный потолок результата тула в символах. */
|
|
10
|
+
export const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
|
|
11
|
+
/**
|
|
12
|
+
* Порог качества контекста, ниже которого включается принудительная
|
|
13
|
+
* компакция. См. FORCED_COMPACTION_COOLDOWN — без cooldown низкое
|
|
14
|
+
* качество ретриггерит компакцию на каждой итерации.
|
|
15
|
+
*/
|
|
16
|
+
export const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
17
|
+
/**
|
|
18
|
+
* Макс. символов аргументов тула в <system-summary>. Не допускает
|
|
19
|
+
* дублирования `content` из write_file поверх role:"tool" сообщения,
|
|
20
|
+
* которое уже несёт результат.
|
|
21
|
+
*/
|
|
22
|
+
export const TOOL_ARGS_SUMMARY_MAX_CHARS = 240;
|
|
23
|
+
/**
|
|
24
|
+
* Мин. число итераций между принудительными компакциями по качеству.
|
|
25
|
+
* Без него компакция не может восстановить качество и выжигает весь
|
|
26
|
+
* бюджет на компактирование (наблюдалось: 56 компакций за ~28 минут).
|
|
27
|
+
*/
|
|
28
|
+
export const FORCED_COMPACTION_COOLDOWN = 3;
|
|
29
|
+
/** Макс. повторов при восстановимой ошибке LLM (например, токен-лимит). */
|
|
30
|
+
export const MAX_LLM_ERROR_RETRIES = 2;
|
|
31
|
+
/** Макс. повторов при вердикте халлюцинации "retry". */
|
|
32
|
+
export const MAX_HALLUCINATION_RETRIES = 3;
|
|
33
|
+
/** Порог подряд идущих неудач тулов до сообщения о восстановлении. */
|
|
34
|
+
export const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
35
|
+
/**
|
|
36
|
+
* Мин. общих (не подряд идущих) неудач тула до записи правила в память.
|
|
37
|
+
* Фиксирует системные проблемы (ENOENT у LSP, сломанный конфиг), которые
|
|
38
|
+
* не всплывают по пути "5 подряд неудач".
|
|
39
|
+
*/
|
|
40
|
+
export const MIN_REPEATED_TOOL_FAILURES = 3;
|
|
41
|
+
/** Макс. повторов финального аудита. */
|
|
42
|
+
export const MAX_AUDIT_RETRIES = 3;
|
|
43
|
+
/** Макс. повторов пустого ответа. */
|
|
44
|
+
export const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
45
|
+
/** Макс. повторных одинаковых тулов в режиме exit-on-complete. */
|
|
46
|
+
export const MAX_REPEATED_TOOL_CALLS = 2;
|
|
47
|
+
/**
|
|
48
|
+
* Макс. повторных одинаковых тулов в интерактивном режиме до жёсткой
|
|
49
|
+
* остановки. Выше, чем в exit-on-complete: сначала модель получает nudge
|
|
50
|
+
* (по одному разу на каждый повтор), и только потом цикл обрывается —
|
|
51
|
+
* без этого модель, игнорирующая подсказки, бесконечно повторяет один
|
|
52
|
+
* и тот же вызов (наблюдалось: session_info подряд после завершения задачи).
|
|
53
|
+
*/
|
|
54
|
+
export const MAX_REPEATED_TOOL_CALLS_INTERACTIVE = 5;
|
|
55
|
+
/**
|
|
56
|
+
* Порог цикла «перфекционизм»: столько правок одного файла (или запусков
|
|
57
|
+
* одной команды) за прогон, после которых цикл подталкивает модель закрыть
|
|
58
|
+
* задачу. Наблюдение ses_mthn3c2a: задача готова на итерации 13, модель ещё
|
|
59
|
+
* 20+ итераций микроредактировала один файл (edit → build → read → edit…).
|
|
60
|
+
*/
|
|
61
|
+
export const MUTATION_CYCLE_NUDGE_THRESHOLD = 5;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { pc } from "../../ui/colors";
|
|
2
|
+
/**
|
|
3
|
+
* Рендер UI-индикаторов контекста (stats-бар и уведомление о компакции).
|
|
4
|
+
*
|
|
5
|
+
* Вынесен из agent.ts, где рендеринг жил прямо в бизнес-логике цикла.
|
|
6
|
+
* `lastCompactionShown` хранится внутри и переживает несколько запусков
|
|
7
|
+
* цикла (как исходное поле Agent) — экземпляр должен быть полем класса,
|
|
8
|
+
* а не создаваться заново на каждый ход.
|
|
9
|
+
*/
|
|
10
|
+
export class ContextRenderer {
|
|
11
|
+
lastCompactionShown = 0;
|
|
12
|
+
render(ui, contextManager, onMeta) {
|
|
13
|
+
if (ui?.showContextStats) {
|
|
14
|
+
this.renderStats(contextManager, onMeta);
|
|
15
|
+
}
|
|
16
|
+
else if (ui?.showCompaction) {
|
|
17
|
+
this.renderCompaction(contextManager, onMeta);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
renderStats(contextManager, onMeta) {
|
|
21
|
+
const ctxTokens = contextManager.getEstimatedTokens();
|
|
22
|
+
const ctxBudget = contextManager.getBudget();
|
|
23
|
+
const ctxPct = Math.min(100, Math.round((ctxTokens / ctxBudget.history) * 100));
|
|
24
|
+
const barLen = 10;
|
|
25
|
+
const filled = Math.round((ctxPct / 100) * barLen);
|
|
26
|
+
const ctxBar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
|
|
27
|
+
const pctColor = ctxPct >= 75 ? pc.yellow : pc.dim;
|
|
28
|
+
const compCount = contextManager.getCompactionCount();
|
|
29
|
+
const quality = contextManager.getQuality();
|
|
30
|
+
const qualityColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
|
|
31
|
+
onMeta?.(`\n ${ctxBar} ${pctColor(`${ctxPct}%`)} ${pc.dim(`ctx: ${ctxTokens}/${ctxBudget.history}`)} ${pc.dim(`compactions: ${compCount}`)} ${qualityColor(`quality: ${quality}%`)}\n`);
|
|
32
|
+
}
|
|
33
|
+
renderCompaction(contextManager, onMeta) {
|
|
34
|
+
const compCount = contextManager.getCompactionCount();
|
|
35
|
+
if (compCount > this.lastCompactionShown) {
|
|
36
|
+
this.lastCompactionShown = compCount;
|
|
37
|
+
onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { pc } from "../../ui/colors";
|
|
2
|
+
import { t } from "../../i18n/index";
|
|
3
|
+
import { MAX_HALLUCINATION_RETRIES } from "./constants";
|
|
4
|
+
/**
|
|
5
|
+
* Вентиль проверки халлюцинаций финального текстового ответа.
|
|
6
|
+
*
|
|
7
|
+
* Объединяет весь граф вердиктов validate() (block/warn/retry) и их
|
|
8
|
+
* под-пути: exit-on-complete fast-exit, подавление повтора после отказа
|
|
9
|
+
* аудита (suppressRepetitionRetry), исчерпание повторов, добавление
|
|
10
|
+
* retry-контекста в сообщения.
|
|
11
|
+
*
|
|
12
|
+
* Возвращает структурированный вердикт; решение о `break`/`continue`/`return`
|
|
13
|
+
* принимает цикл. Вся мутация состояния живёт здесь — цикл остаётся тонким.
|
|
14
|
+
*/
|
|
15
|
+
export class HallucinationGate {
|
|
16
|
+
detector;
|
|
17
|
+
constructor(detector) {
|
|
18
|
+
this.detector = detector;
|
|
19
|
+
}
|
|
20
|
+
async evaluate(deps) {
|
|
21
|
+
const { state, textContent, logger } = deps;
|
|
22
|
+
// Обновляем базу сравнения ДО validate: confidence-чек смотрит на
|
|
23
|
+
// ПРЕДЫДУЩУЮ итерацию, иначе текст пересекался бы сам с собой.
|
|
24
|
+
this.detector.getConfidenceCheck().setPreviousResponse(state.lastModelText);
|
|
25
|
+
if (textContent)
|
|
26
|
+
state.lastModelText = textContent;
|
|
27
|
+
const result = await this.detector.validate(textContent);
|
|
28
|
+
if (result.status === "block") {
|
|
29
|
+
logger.warn(`Response blocked: ${result.reason}`);
|
|
30
|
+
return { action: "block", reason: result.reason || "" };
|
|
31
|
+
}
|
|
32
|
+
if (result.status === "warn") {
|
|
33
|
+
logger.warn(`Hallucination warning: ${result.reason}`);
|
|
34
|
+
this.displayWarnLine(result.reason, deps);
|
|
35
|
+
return { action: "warn" };
|
|
36
|
+
}
|
|
37
|
+
if (result.status === "retry") {
|
|
38
|
+
state.iterHallucinationRetry = true;
|
|
39
|
+
// Exit-on-complete fast-exit НЕ должен обходить финальный аудит: при
|
|
40
|
+
// активном плане модель может ответить "done" с невыполненными шагами
|
|
41
|
+
// (регрессия сертификации: модель ответила, не запустив свой скрипт).
|
|
42
|
+
// Короткое замыкание — только когда аудитить нечего.
|
|
43
|
+
if (deps.exitOnComplete && textContent?.trim() && !deps.hasFinalAudit) {
|
|
44
|
+
logger.debug("Exit-on-complete: stopping on first response");
|
|
45
|
+
// Сохраняем ДО break — lastText всё ещё текст предыдущей (туловой)
|
|
46
|
+
// итерации, и без этого итоговый ответ теряется (reported text: "").
|
|
47
|
+
state.lastText = textContent;
|
|
48
|
+
state.finalAnswerAccepted = true;
|
|
49
|
+
return { action: "exit" };
|
|
50
|
+
}
|
|
51
|
+
if (state.suppressRepetitionRetry && result.kind === "repetition") {
|
|
52
|
+
// Аудит отклонил предыдущий ответ и переспросил; его переформулировка
|
|
53
|
+
// выполненной задачи естественно "повторяющаяся" — это ожидаемо, а не
|
|
54
|
+
// деградация. Снижаем до предупреждения и пропускаем в acceptance path
|
|
55
|
+
// (сам аудит ограничен MAX_AUDIT_RETRIES).
|
|
56
|
+
state.suppressRepetitionRetry = false;
|
|
57
|
+
logger.warn(`Audit-triggered re-answer repetition — not counted as hallucination retry`);
|
|
58
|
+
this.displayWarnLine(result.reason, deps);
|
|
59
|
+
return { action: "accept" };
|
|
60
|
+
}
|
|
61
|
+
if (state.hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
|
|
62
|
+
logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
|
|
63
|
+
return { action: "block", reason: t("hall.max_retries_exhausted") };
|
|
64
|
+
}
|
|
65
|
+
state.hallucinationRetries++;
|
|
66
|
+
logger.warn(`Hallucination retry (${state.hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${result.reason}`);
|
|
67
|
+
if (textContent) {
|
|
68
|
+
deps.contextManager.addMessage({ role: "assistant", content: textContent });
|
|
69
|
+
}
|
|
70
|
+
deps.contextManager.addMessage({
|
|
71
|
+
role: "user",
|
|
72
|
+
content: `<system-summary>[Retry context: ${result.reason}. Original task: "${deps.input}". You must either call a needed tool or provide a substantive response. Empty replies are not allowed.]</system-summary>`,
|
|
73
|
+
});
|
|
74
|
+
return { action: "retry" };
|
|
75
|
+
}
|
|
76
|
+
return { action: "accept" };
|
|
77
|
+
}
|
|
78
|
+
displayWarnLine(reason, deps) {
|
|
79
|
+
const warnLine = `${t("hall.uncertainty_prefix").trim()} ${reason ?? ""}`;
|
|
80
|
+
if (deps.onMeta) {
|
|
81
|
+
deps.onMeta(`\n${pc.yellow(warnLine)}\n`);
|
|
82
|
+
}
|
|
83
|
+
else if (deps.onChunk) {
|
|
84
|
+
deps.onChunk(`\n${warnLine}\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FORCED_COMPACTION_COOLDOWN } from "./constants";
|
|
2
|
+
/** Фабрика начального состояния цикла. */
|
|
3
|
+
export function createLoopState() {
|
|
4
|
+
return {
|
|
5
|
+
iteration: 0,
|
|
6
|
+
lastText: "",
|
|
7
|
+
lastModelText: "",
|
|
8
|
+
lastForcedCompactionIteration: -FORCED_COMPACTION_COOLDOWN,
|
|
9
|
+
hallucinationRetries: 0,
|
|
10
|
+
lastToolSignature: "",
|
|
11
|
+
consecutiveToolFailures: 0,
|
|
12
|
+
consecutiveToolSuccesses: 0,
|
|
13
|
+
repeatedToolCount: 0,
|
|
14
|
+
toolFailureCounts: new Map(),
|
|
15
|
+
memoryRuleRecorded: new Set(),
|
|
16
|
+
toolTargetCounts: new Map(),
|
|
17
|
+
cycleNudgedKeys: new Set(),
|
|
18
|
+
apiPromptTokens: 0,
|
|
19
|
+
apiCompletionTokens: 0,
|
|
20
|
+
apiCompletionChars: 0,
|
|
21
|
+
estimatedPromptTokensTotal: 0,
|
|
22
|
+
totalLlmDuration: 0,
|
|
23
|
+
cacheCachedTotal: 0,
|
|
24
|
+
cacheUncachedTotal: 0,
|
|
25
|
+
cacheWriteTotal: 0,
|
|
26
|
+
cacheSource: "none",
|
|
27
|
+
auditRetries: 0,
|
|
28
|
+
lastAuditSummary: "",
|
|
29
|
+
emptyResponseRetries: 0,
|
|
30
|
+
emptyResponseExhausted: false,
|
|
31
|
+
auditFailed: false,
|
|
32
|
+
finalAnswerAccepted: false,
|
|
33
|
+
suppressRepetitionRetry: false,
|
|
34
|
+
llmErrorRetries: 0,
|
|
35
|
+
iterHallucinationRetry: false,
|
|
36
|
+
iterAuditRejection: false,
|
|
37
|
+
iterRecoverableLlmError: false,
|
|
38
|
+
iterPlanCreated: false,
|
|
39
|
+
iterRepetitive: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Сброс per-итерационных сигналов reasoning policy. Вызывается в цикле
|
|
44
|
+
* ПОСЛЕ оценки политики (политика читает сигналы предыдущей итерации),
|
|
45
|
+
* перед тем как новые сигналы будут выставлены на текущем ходу.
|
|
46
|
+
*/
|
|
47
|
+
export function clearIterationSignals(state) {
|
|
48
|
+
state.iterHallucinationRetry = false;
|
|
49
|
+
state.iterAuditRejection = false;
|
|
50
|
+
state.iterRecoverableLlmError = false;
|
|
51
|
+
state.iterPlanCreated = false;
|
|
52
|
+
state.iterRepetitive = false;
|
|
53
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/core/agent/prefix-monitor
|
|
2
|
+
/**
|
|
3
|
+
* Клиентский монитор стабильности префикса промпта.
|
|
4
|
+
*
|
|
5
|
+
* Провайдеры кешируют только совпавший префикс промпта. Даже когда провайдер
|
|
6
|
+
* не сообщает cache-метрики (Ollama, экзотические прокси), агент может сам
|
|
7
|
+
* посчитать, насколько текущий промпт продолжает предыдущий, и — что важнее —
|
|
8
|
+
* ЧТО именно сломало префикс. Это не заменяет провайдерский `cached_tokens`,
|
|
9
|
+
* но работает всегда и не даёт спутать отсутствие данных с промахом кеша.
|
|
10
|
+
*/
|
|
11
|
+
/** Разделитель сегментов при склейке (провайдер склеивает поля сам). */
|
|
12
|
+
const SEP = "\n";
|
|
13
|
+
/**
|
|
14
|
+
* Маркеры «волатильного» содержимого. Если расхождение префикса попадает на
|
|
15
|
+
* дату/время/UUID/временный путь — это почти наверняка клиентская ошибка
|
|
16
|
+
* стабильности промпта, а не действие модели.
|
|
17
|
+
*/
|
|
18
|
+
const VOLATILE_RE = /(\d{4}-\d{2}-\d{2}|\d{2}:\d{2}:\d{2}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}|[A-Za-z]:\\|\\AppData\\|\\Temp\\|\/tmp\/)/;
|
|
19
|
+
/** Детерминированная сериализация определений тулов для сравнения префикса. */
|
|
20
|
+
export function serializeTools(tools) {
|
|
21
|
+
return tools
|
|
22
|
+
.map((t) => `${t.name}\u0001${t.description}\u0001${JSON.stringify(t.parameters)}`)
|
|
23
|
+
.join("\u0002");
|
|
24
|
+
}
|
|
25
|
+
/** Детерминированная сериализация истории сообщений для сравнения префикса. */
|
|
26
|
+
export function serializeMessages(messages) {
|
|
27
|
+
return messages
|
|
28
|
+
.map((m) => `${m.role}\u0001${typeof m.content === "string" ? m.content : JSON.stringify(m.content)}`)
|
|
29
|
+
.join("\u0002");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Собирает снимок промпта из истории сообщений и определений тулов.
|
|
33
|
+
* Системные сообщения выделяются в отдельный сегмент — они стабильны и
|
|
34
|
+
* ломаются по другим причинам, чем история.
|
|
35
|
+
*/
|
|
36
|
+
export function buildPromptSnapshot(messages, tools) {
|
|
37
|
+
const system = [];
|
|
38
|
+
const rest = [];
|
|
39
|
+
for (const m of messages) {
|
|
40
|
+
if (m.role === "system")
|
|
41
|
+
system.push(m);
|
|
42
|
+
else
|
|
43
|
+
rest.push(m);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
system: serializeMessages(system),
|
|
47
|
+
tools: serializeTools(tools),
|
|
48
|
+
history: serializeMessages(rest),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Склейка сегментов в единую строку с запоминанием границ. */
|
|
52
|
+
function frame(s) {
|
|
53
|
+
const full = s.system + SEP + s.tools + SEP + s.history;
|
|
54
|
+
const toolsStart = s.system.length + SEP.length;
|
|
55
|
+
const historyStart = toolsStart + s.tools.length + SEP.length;
|
|
56
|
+
return { full, toolsStart, historyStart };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Считает общий префикс текущего и предыдущего промпта и классифицирует
|
|
60
|
+
* причину расхождения. `countTokens` передаётся вызывающим, чтобы функция
|
|
61
|
+
* оставалась чистой и не тянула токенизатор в горячий путь.
|
|
62
|
+
*/
|
|
63
|
+
export function diffPrompt(prev, curr, countTokens) {
|
|
64
|
+
const c = frame(curr);
|
|
65
|
+
const currTokens = countTokens(c.full);
|
|
66
|
+
if (!prev) {
|
|
67
|
+
return {
|
|
68
|
+
prevTokens: 0,
|
|
69
|
+
currTokens,
|
|
70
|
+
commonPrefixTokens: 0,
|
|
71
|
+
stableRatio: 0,
|
|
72
|
+
cause: "unknown",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const p = frame(prev);
|
|
76
|
+
const prevTokens = countTokens(p.full);
|
|
77
|
+
// Общий префикс — посимвольно (нижняя граница токенного префикса).
|
|
78
|
+
const maxCp = Math.min(p.full.length, c.full.length);
|
|
79
|
+
let cp = 0;
|
|
80
|
+
while (cp < maxCp && p.full[cp] === c.full[cp])
|
|
81
|
+
cp++;
|
|
82
|
+
const commonPrefixTokens = countTokens(c.full.slice(0, cp));
|
|
83
|
+
const stableRatio = currTokens === 0 ? 0 : Math.min(1, commonPrefixTokens / currTokens);
|
|
84
|
+
// Префикс не сломан: идентично, дописано в хвост или усечено.
|
|
85
|
+
if (cp === maxCp) {
|
|
86
|
+
return { prevTokens, currTokens, commonPrefixTokens, stableRatio, cause: "none" };
|
|
87
|
+
}
|
|
88
|
+
let cause;
|
|
89
|
+
if (cp >= c.historyStart)
|
|
90
|
+
cause = "history";
|
|
91
|
+
else if (cp >= c.toolsStart)
|
|
92
|
+
cause = "tools";
|
|
93
|
+
else
|
|
94
|
+
cause = "system";
|
|
95
|
+
// Волатильное содержимое у точки расхождения перекрывает сегментную причину.
|
|
96
|
+
const window = (c.full.slice(Math.max(0, cp - 40), cp + 160) +
|
|
97
|
+
p.full.slice(Math.max(0, cp - 40), cp + 160));
|
|
98
|
+
if (VOLATILE_RE.test(window))
|
|
99
|
+
cause = "volatile";
|
|
100
|
+
return { prevTokens, currTokens, commonPrefixTokens, stableRatio, cause };
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { evaluateReasoningPolicy } from "../../modules/reasoning/policy";
|
|
2
|
+
/**
|
|
3
|
+
* Резолвер уровня reasoning на итерацию.
|
|
4
|
+
*
|
|
5
|
+
* Приоритет (сохранён из agent.ts):
|
|
6
|
+
* 1. Активный override от set_thinking (в окне overrideCooldown) — побеждает
|
|
7
|
+
* и auto-режим, и фиксированный.
|
|
8
|
+
* 2. auto-режим + пройденный probe — политика (повышение на халлюцинации/
|
|
9
|
+
* отказе аудита/ошибке LLM, понижение на рутине и повторах, затухание).
|
|
10
|
+
* 3. Фиксированный режим.
|
|
11
|
+
*
|
|
12
|
+
* ВАЖНО: сигналы политики (iter*) описывают события ПРЕДЫДУЩЕЙ итерации —
|
|
13
|
+
* они выставляются на retry-путях перед `continue`, поэтому политика должна
|
|
14
|
+
* оцениваться ДО их очистки (это делает вызывающий код через
|
|
15
|
+
* clearIterationSignals сразу после resolve).
|
|
16
|
+
*/
|
|
17
|
+
export class ReasoningEffortResolver {
|
|
18
|
+
resolve(deps) {
|
|
19
|
+
const { state, reasoningConfig, reasoningState } = deps;
|
|
20
|
+
let level;
|
|
21
|
+
const strategy = deps.reasoningStrategy;
|
|
22
|
+
let source = "policy";
|
|
23
|
+
const overrideCooldown = reasoningConfig?.overrideCooldown ?? 0;
|
|
24
|
+
const overrideAge = state.iteration - reasoningState.overrideIteration;
|
|
25
|
+
const overrideActive = overrideCooldown > 0 &&
|
|
26
|
+
reasoningState.overrideIteration >= 0 &&
|
|
27
|
+
overrideAge > 0 &&
|
|
28
|
+
overrideAge <= overrideCooldown;
|
|
29
|
+
if (reasoningState.manual) {
|
|
30
|
+
level = reasoningState.level;
|
|
31
|
+
source = "agent_override";
|
|
32
|
+
}
|
|
33
|
+
else if (overrideActive) {
|
|
34
|
+
level = reasoningState.level;
|
|
35
|
+
source = "agent_override";
|
|
36
|
+
}
|
|
37
|
+
else if (reasoningConfig && reasoningConfig.mode === "auto" && deps.probePassed) {
|
|
38
|
+
const policyResult = evaluateReasoningPolicy({
|
|
39
|
+
hallucinationRetry: state.iterHallucinationRetry,
|
|
40
|
+
auditRejection: state.iterAuditRejection,
|
|
41
|
+
recoverableLlmError: state.iterRecoverableLlmError,
|
|
42
|
+
planCreated: state.iterPlanCreated,
|
|
43
|
+
consecutiveSuccesses: state.consecutiveToolSuccesses,
|
|
44
|
+
isRepetitive: state.iterRepetitive,
|
|
45
|
+
baseline: reasoningConfig?.baseline ?? "medium",
|
|
46
|
+
}, deps.policyState);
|
|
47
|
+
level = policyResult.level;
|
|
48
|
+
source = "policy";
|
|
49
|
+
}
|
|
50
|
+
else if (reasoningConfig && reasoningConfig.mode !== "auto") {
|
|
51
|
+
level = reasoningConfig.mode;
|
|
52
|
+
source = "fixed";
|
|
53
|
+
}
|
|
54
|
+
return { level, strategy, source };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { estimateTokens } from "../../llm/token-counter";
|
|
2
|
+
/**
|
|
3
|
+
* Учёт токенов и стоимости за один запуск цикла.
|
|
4
|
+
*
|
|
5
|
+
* Отвечает за три вещи, которые в agent.ts были размазаны по циклу:
|
|
6
|
+
* 1) замену (НЕ аккумуляцию) running-сумм prompt/completion-токенов из
|
|
7
|
+
* done-чанка стрима — `chat()` вызывается один раз за итерацию, поэтому
|
|
8
|
+
* суммировать нельзя (иначе один реальный запрос посчитается дважды);
|
|
9
|
+
* 2) per-call лог usage + запись стоимости в CostTracker, с честным
|
|
10
|
+
* разделением источника ("api" против локальной оценки, т.к. llama.cpp
|
|
11
|
+
* часто опускает `usage` в стриминге);
|
|
12
|
+
* 3) финальные метрики результата: API-токены, если бэкенд их отдал,
|
|
13
|
+
* иначе fallback-оценка (~4 символа на токен).
|
|
14
|
+
*/
|
|
15
|
+
export class TokenTracker {
|
|
16
|
+
costTracker;
|
|
17
|
+
getProviderName;
|
|
18
|
+
constructor(costTracker, getProviderName) {
|
|
19
|
+
this.costTracker = costTracker;
|
|
20
|
+
this.getProviderName = getProviderName;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Начало итерации: фиксирует baseline API-токенов и аккумулирует оценку
|
|
24
|
+
* prompt-токенов. Возвращает baseline для последующих вызовов.
|
|
25
|
+
*/
|
|
26
|
+
beginIteration(state, estimatedPromptTokens) {
|
|
27
|
+
state.estimatedPromptTokensTotal += estimatedPromptTokens;
|
|
28
|
+
// Кеш-данные относятся к одной итерации; сбрасываем, чтобы отсутствие
|
|
29
|
+
// usage в следующем ходе не приписало старые цифры.
|
|
30
|
+
state.cacheUsage = undefined;
|
|
31
|
+
return {
|
|
32
|
+
promptBefore: state.apiPromptTokens,
|
|
33
|
+
completionBefore: state.apiCompletionTokens,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* done-чанк стрима: ЗАМЕНЯЕТ running-суммы (promptBefore + usage), не
|
|
38
|
+
* аккумулирует — один логический запрос = один `done`.
|
|
39
|
+
*/
|
|
40
|
+
recordApiUsage(state, baseline, usage) {
|
|
41
|
+
state.apiPromptTokens = baseline.promptBefore + usage.promptTokens;
|
|
42
|
+
state.apiCompletionTokens = baseline.completionBefore + usage.completionTokens;
|
|
43
|
+
state.cacheUsage = usage.cache;
|
|
44
|
+
}
|
|
45
|
+
/** Аккумулирует символы ответа — fallback-оценка completion-токенов. */
|
|
46
|
+
addCompletionChars(state, chars) {
|
|
47
|
+
state.apiCompletionChars += chars;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Per-call лог usage + запись стоимости. Источник "api", когда бэкенд
|
|
51
|
+
* отдал цифры; иначе локальная оценка (контекст для prompt, chars/4 для
|
|
52
|
+
* completion) — лог должен отличать реальные числа от эвристики.
|
|
53
|
+
*/
|
|
54
|
+
logUsage(state, baseline, textContent, contextTokens, iteration, durationMs, slog) {
|
|
55
|
+
const usagePrompt = state.apiPromptTokens - baseline.promptBefore;
|
|
56
|
+
const usageCompletion = state.apiCompletionTokens - baseline.completionBefore;
|
|
57
|
+
const source = usagePrompt > 0 || usageCompletion > 0 ? "api" : "estimate";
|
|
58
|
+
const prompt = source === "api" ? usagePrompt : contextTokens;
|
|
59
|
+
const completion = source === "api" ? usageCompletion : estimateTokens(textContent);
|
|
60
|
+
// Накопление кеш-токенов за прогон (для AgentResult.cache).
|
|
61
|
+
if (state.cacheUsage) {
|
|
62
|
+
state.cacheCachedTotal += state.cacheUsage.cachedTokens;
|
|
63
|
+
state.cacheUncachedTotal += state.cacheUsage.uncachedTokens;
|
|
64
|
+
state.cacheWriteTotal += state.cacheUsage.cacheWriteTokens;
|
|
65
|
+
state.cacheSource = state.cacheUsage.source;
|
|
66
|
+
}
|
|
67
|
+
slog.logLlmUsage(iteration, {
|
|
68
|
+
promptTokens: prompt,
|
|
69
|
+
completionTokens: completion,
|
|
70
|
+
totalTokens: prompt + completion,
|
|
71
|
+
source,
|
|
72
|
+
durationMs,
|
|
73
|
+
prefix: state.prefixDelta,
|
|
74
|
+
cache: state.cacheUsage,
|
|
75
|
+
});
|
|
76
|
+
// Кеш учитываем только когда цифры пришли от API: оценка chars/4 о кеше
|
|
77
|
+
// ничего не знает и не должна менять тарификацию.
|
|
78
|
+
this.costTracker.record(prompt, completion, this.getProviderName(), source === "api" ? state.cacheUsage : undefined);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Финальные метрики результата. API-токены в приоритете; иначе — сумма
|
|
82
|
+
* оценок prompt по итерациям и символы completion (~4 символа/токен).
|
|
83
|
+
*/
|
|
84
|
+
resolveFinal(state) {
|
|
85
|
+
if (state.apiPromptTokens > 0 || state.apiCompletionTokens > 0) {
|
|
86
|
+
return {
|
|
87
|
+
prompt: state.apiPromptTokens,
|
|
88
|
+
completion: state.apiCompletionTokens,
|
|
89
|
+
total: state.apiPromptTokens + state.apiCompletionTokens,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const prompt = Math.max(1, state.estimatedPromptTokensTotal);
|
|
93
|
+
const completion = Math.max(0, Math.ceil(state.apiCompletionChars / 4));
|
|
94
|
+
return { prompt, completion, total: prompt + completion };
|
|
95
|
+
}
|
|
96
|
+
}
|