micro-models-agent 0.51.2 → 0.52.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/README.md +358 -358
- package/dist/cli/commands.js +447 -0
- package/dist/cli/completer.js +167 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.js +153 -0
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +761 -0
- package/dist/cli/repl.js +702 -0
- package/dist/cli/run-result.js +33 -0
- package/dist/cli/security-commands.js +164 -0
- package/dist/cli/setup.js +237 -0
- package/dist/config/config.js +276 -0
- package/dist/config/defaults.js +141 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/experts.js +15 -0
- package/dist/config/index.js +4 -0
- package/dist/config/security.js +213 -0
- package/dist/config/types.js +1 -0
- package/dist/core/agent-moe.js +102 -0
- package/dist/core/agent.js +1018 -0
- package/dist/core/bootstrap.js +481 -0
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/index.js +2 -0
- package/dist/core/prompt-builder.js +76 -0
- package/dist/core/session-logger.js +251 -0
- package/dist/core/types.js +1 -0
- package/dist/core/version.js +26 -0
- package/dist/core/workspace.js +76 -0
- package/dist/i18n/en.json +679 -0
- package/dist/i18n/index.js +46 -0
- package/dist/i18n/ru.json +679 -0
- package/dist/index.js +22 -0
- package/dist/llm/image-utils.js +143 -0
- package/dist/llm/index.js +4 -0
- package/dist/llm/model-loader.js +78 -0
- package/dist/llm/openai-compat.js +497 -0
- package/dist/llm/orchestrator.js +200 -0
- package/dist/llm/provider.js +10 -0
- package/dist/llm/response.js +39 -0
- package/dist/llm/token-counter.js +39 -0
- package/dist/llm/types.js +1 -0
- package/dist/logger/app-logger.js +189 -0
- package/dist/logger/file-log.js +151 -0
- package/dist/logger/index.js +1 -0
- package/dist/main.js +509 -100
- package/dist/migration/backup.js +45 -0
- package/dist/migration/detect.js +50 -0
- package/dist/migration/index.js +2 -0
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +76 -0
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +24 -0
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -0
- package/dist/modules/browser/module.js +29 -0
- package/dist/modules/browser/session.js +342 -0
- package/dist/modules/browser/snapshot.js +148 -0
- package/dist/modules/browser/types.js +12 -0
- package/dist/modules/certification/cli.js +213 -0
- package/dist/modules/certification/fact-checker.js +82 -0
- package/dist/modules/certification/loader.js +106 -0
- package/dist/modules/certification/manifest.js +58 -0
- package/dist/modules/certification/runner.js +245 -0
- package/dist/modules/certification/scenarios.js +407 -0
- package/dist/modules/certification/types.js +1 -0
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +168 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -0
- package/dist/modules/context/manager.js +440 -0
- package/dist/modules/execution/audit-runners.js +206 -0
- package/dist/modules/execution/auditor.js +218 -0
- package/dist/modules/execution/execution-plugin.js +431 -0
- package/dist/modules/execution/index.js +8 -0
- package/dist/modules/execution/module.js +625 -0
- package/dist/modules/execution/moe-executor.js +304 -0
- package/dist/modules/execution/plan-coverage.js +68 -0
- package/dist/modules/execution/plan-persister.js +46 -0
- package/dist/modules/execution/plan-store.js +196 -0
- package/dist/modules/execution/plan-tool.js +677 -0
- package/dist/modules/execution/plan-validator.js +153 -0
- package/dist/modules/execution/planner.js +94 -0
- package/dist/modules/execution/stuck-detector.js +746 -0
- package/dist/modules/execution/tracker.js +69 -0
- package/dist/modules/execution/types.js +1 -0
- package/dist/modules/execution/verifier.js +235 -0
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +66 -0
- package/dist/modules/hallucination/consistency.js +26 -0
- package/dist/modules/hallucination/detector.js +47 -0
- package/dist/modules/hallucination/factual.js +169 -0
- package/dist/modules/hallucination/index.js +5 -0
- package/dist/modules/hallucination/js-identifiers.js +262 -0
- package/dist/modules/hallucination/llm-judge.js +101 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/indexer/cache.js +40 -0
- package/dist/modules/indexer/index.js +3 -0
- package/dist/modules/indexer/module.js +246 -0
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +101 -0
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +389 -0
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +135 -0
- package/dist/modules/lsp/index.js +3 -0
- package/dist/modules/lsp/module.js +260 -0
- package/dist/modules/lsp/probe.js +86 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +144 -0
- package/dist/modules/lsp/types.js +1 -0
- package/dist/modules/mcp/client.js +399 -0
- package/dist/modules/mcp/index.js +3 -0
- package/dist/modules/mcp/module.js +142 -0
- package/dist/modules/mcp/registry.js +15 -0
- package/dist/modules/memory/index.js +1 -0
- package/dist/modules/memory/module.js +96 -0
- package/dist/modules/memory/search.js +42 -0
- package/dist/modules/memory/store.js +69 -0
- package/dist/modules/pipelines/engine.js +60 -0
- package/dist/modules/pipelines/index.js +3 -0
- package/dist/modules/pipelines/parser.js +56 -0
- package/dist/modules/pipelines/template.js +14 -0
- package/dist/modules/plugins/builtin/lint-on-write.js +334 -0
- package/dist/modules/plugins/builtin/notify.js +9 -0
- package/dist/modules/plugins/index.js +1 -0
- package/dist/modules/plugins/loader.js +70 -0
- package/dist/modules/plugins/manager.js +261 -0
- package/dist/modules/plugins/types.js +1 -0
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -0
- package/dist/modules/processes/registry.js +327 -0
- package/dist/modules/processes/runner.js +23 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +48 -0
- package/dist/modules/security/audit-log.js +136 -0
- package/dist/modules/security/audit-notifier.js +292 -0
- package/dist/modules/security/command-validator.js +219 -0
- package/dist/modules/security/content-scanner.js +53 -0
- package/dist/modules/security/data-sanitizer.js +89 -0
- package/dist/modules/security/encryption.js +242 -0
- package/dist/modules/security/index.js +14 -0
- package/dist/modules/security/network-validator.js +88 -0
- package/dist/modules/security/path-validator.js +203 -0
- package/dist/modules/security/rate-limiter.js +119 -0
- package/dist/modules/security/security-policies.js +531 -0
- package/dist/modules/security/session-encryption.js +210 -0
- package/dist/modules/security/session-isolation.js +95 -0
- package/dist/modules/session/index.js +3 -0
- package/dist/modules/session/manager.js +172 -0
- package/dist/modules/session/module.js +24 -0
- package/dist/modules/session/store.js +222 -0
- package/dist/modules/session/types.js +1 -0
- package/dist/modules/skills/index.js +2 -0
- package/dist/modules/skills/loader.js +72 -0
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +129 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/updater/checker.js +96 -0
- package/dist/modules/updater/index.js +2 -0
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +16 -0
- package/dist/modules/user-profile/index.js +1 -0
- package/dist/modules/user-profile/profile.js +68 -0
- package/dist/skills/builtin/git.md +36 -36
- package/dist/skills/builtin/typescript.md +35 -35
- package/dist/tools/approve.js +33 -0
- package/dist/tools/attach-image.js +101 -0
- package/dist/tools/bash.js +519 -0
- package/dist/tools/browser.js +115 -0
- package/dist/tools/chunk-query.js +100 -0
- package/dist/tools/create-dir.js +56 -0
- package/dist/tools/delete-file.js +63 -0
- package/dist/tools/download-file.js +117 -0
- package/dist/tools/edit-file.js +80 -0
- package/dist/tools/enable-tools.js +59 -0
- package/dist/tools/executor.js +154 -0
- package/dist/tools/file-info.js +47 -0
- package/dist/tools/filter-tools.js +17 -0
- package/dist/tools/glob-tool.js +27 -0
- package/dist/tools/grep-tool.js +125 -0
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +78 -0
- package/dist/tools/list-dir.js +49 -0
- package/dist/tools/load-skill.js +43 -0
- package/dist/tools/mcp-call.js +69 -0
- package/dist/tools/move-file.js +86 -0
- package/dist/tools/path-utils.js +101 -0
- package/dist/tools/pipeline-run.js +145 -0
- package/dist/tools/preview.js +2 -0
- package/dist/tools/process-kill.js +40 -0
- package/dist/tools/process-list.js +37 -0
- package/dist/tools/process-log.js +54 -0
- package/dist/tools/question.js +141 -0
- package/dist/tools/read-file.js +179 -0
- package/dist/tools/recall.js +118 -0
- package/dist/tools/registry.js +47 -0
- package/dist/tools/remember.js +68 -0
- package/dist/tools/scope-check.js +32 -0
- package/dist/tools/search-history.js +85 -0
- package/dist/tools/subagent.js +196 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/user-input.js +123 -0
- package/dist/tools/web-browse.js +87 -0
- package/dist/tools/web-fetch.js +119 -0
- package/dist/tools/web-search.js +105 -0
- package/dist/tools/write-file.js +82 -0
- package/dist/ui/box.js +77 -0
- package/dist/ui/colors.js +4 -0
- package/dist/ui/diff.js +178 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/line-editor.js +822 -0
- package/dist/ui/line-math.js +73 -0
- package/dist/ui/md-formatter.js +212 -0
- package/dist/ui/output.js +13 -0
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +259 -0
- package/dist/ui/spinner.js +70 -0
- package/dist/ui/table.js +144 -0
- package/package.json +6 -4
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import { ProviderManager } from "../modules/providers/manager";
|
|
2
|
+
import { t } from "../i18n/index";
|
|
3
|
+
import { pc } from "../ui/colors";
|
|
4
|
+
import { PromptBuilder } from "./prompt-builder";
|
|
5
|
+
import { processRegistry } from "../modules/processes";
|
|
6
|
+
import { SessionLogger } from "./session-logger";
|
|
7
|
+
import { runWithMoE } from "./agent-moe";
|
|
8
|
+
import { CostTracker } from "../modules/pricing/index";
|
|
9
|
+
const TOOL_RESULT_MAX_TOKENS_RATIO = 0.3;
|
|
10
|
+
const TOOL_RESULT_ABSOLUTE_MAX_CHARS = 15000;
|
|
11
|
+
const QUALITY_TRIGGER_THRESHOLD = 40;
|
|
12
|
+
/** Minimum iterations between quality-triggered forced compactions. Without
|
|
13
|
+
* this, a low-quality context re-triggers compaction on EVERY iteration
|
|
14
|
+
* (observed: 56 compactions in ~28 min) and the compaction itself can't
|
|
15
|
+
* restore quality, so the agent burns the whole budget compacting. */
|
|
16
|
+
const FORCED_COMPACTION_COOLDOWN = 3;
|
|
17
|
+
const MAX_LLM_ERROR_RETRIES = 2;
|
|
18
|
+
/** True when the text looks like a raw JSON tool payload (garbage to display). */
|
|
19
|
+
function isToolCallJson(text) {
|
|
20
|
+
const trimmed = text.trim();
|
|
21
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
22
|
+
try {
|
|
23
|
+
JSON.parse(trimmed);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Max chars a tool result may occupy, given remaining budget and whether
|
|
34
|
+
* the tool bounds its own output. Tools that declare `boundedOutput` (e.g.
|
|
35
|
+
* read_file with its line limit) are never truncated by the budget — a
|
|
36
|
+
* near-full context used to cut them to ~2K chars, making the model believe
|
|
37
|
+
* files were truncated and re-read them forever.
|
|
38
|
+
*/
|
|
39
|
+
export function toolOutputCharLimit(remainingBudget, historyBudget, bounded) {
|
|
40
|
+
if (bounded)
|
|
41
|
+
return Number.MAX_SAFE_INTEGER;
|
|
42
|
+
const maxCharsByRatio = Math.floor(historyBudget * TOOL_RESULT_MAX_TOKENS_RATIO * 2);
|
|
43
|
+
return Math.min(remainingBudget, maxCharsByRatio, TOOL_RESULT_ABSOLUTE_MAX_CHARS);
|
|
44
|
+
}
|
|
45
|
+
export class Agent {
|
|
46
|
+
deps;
|
|
47
|
+
systemPromptAdded = false;
|
|
48
|
+
shutdownRequested = false;
|
|
49
|
+
abortController = null;
|
|
50
|
+
lastCompactionShown = 0;
|
|
51
|
+
costTracker;
|
|
52
|
+
constructor(deps) {
|
|
53
|
+
this.deps = deps;
|
|
54
|
+
this.costTracker = new CostTracker(deps.config.model, deps.config.pricing);
|
|
55
|
+
}
|
|
56
|
+
/** Expose context manager for REPL image attachment and other direct access. */
|
|
57
|
+
get contextManager() {
|
|
58
|
+
return this.deps.contextManager;
|
|
59
|
+
}
|
|
60
|
+
callerProvenance() {
|
|
61
|
+
return {
|
|
62
|
+
provider: this.deps.config.provider?.type || "unknown",
|
|
63
|
+
model: this.deps.config.model,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Typed module accessor for CLI/REPL commands (e.g. /lsp). */
|
|
67
|
+
getModule(name) {
|
|
68
|
+
return this.deps.moduleRegistry?.get(name);
|
|
69
|
+
}
|
|
70
|
+
setScope() {
|
|
71
|
+
if (this.deps.scope) {
|
|
72
|
+
this.deps.toolExecutor.setScope(this.deps.scope);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
buildSystemPrompt() {
|
|
76
|
+
const systemBudget = Math.floor(this.deps.config.contextWindow * this.deps.config.contextBudget.systemPrompt);
|
|
77
|
+
const builder = new PromptBuilder(systemBudget);
|
|
78
|
+
builder.addBlocks(this.deps.promptBlocks);
|
|
79
|
+
const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
|
|
80
|
+
if (dynamic.length > 0) {
|
|
81
|
+
builder.addBlocks(dynamic);
|
|
82
|
+
}
|
|
83
|
+
const pluginBlocks = (this.deps.pluginManager.runOnBuildPrompt?.() ?? []).flatMap((content) => content && content.trim() !== ""
|
|
84
|
+
? [
|
|
85
|
+
{
|
|
86
|
+
content,
|
|
87
|
+
priority: "low",
|
|
88
|
+
essential: false,
|
|
89
|
+
estimatedTokens: this.deps.llmProvider.countTokens(content),
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
: []);
|
|
93
|
+
if (pluginBlocks.length > 0) {
|
|
94
|
+
builder.addBlocks(pluginBlocks);
|
|
95
|
+
}
|
|
96
|
+
const result = builder.build();
|
|
97
|
+
return {
|
|
98
|
+
prompt: result.prompt,
|
|
99
|
+
excluded: result.excluded,
|
|
100
|
+
blocks: result.blocks,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Log the current context state to the session log. Called every iteration
|
|
105
|
+
* so the session.jsonl shows a full timeline of how the context grows,
|
|
106
|
+
* compacts, and what it consists of. The `start` snapshot additionally
|
|
107
|
+
* carries the system-prompt block breakdown (which blocks, priorities,
|
|
108
|
+
* token estimates, what was excluded by the budget).
|
|
109
|
+
*/
|
|
110
|
+
logContextStat(kind, iteration, slog, blocks) {
|
|
111
|
+
const cm = this.deps.contextManager;
|
|
112
|
+
if (typeof cm.getSnapshot !== "function")
|
|
113
|
+
return;
|
|
114
|
+
const snap = cm.getSnapshot();
|
|
115
|
+
const history = cm.getActiveHistory();
|
|
116
|
+
const systemMsg = history.find((m) => m.role === "system");
|
|
117
|
+
slog.logContext({
|
|
118
|
+
kind,
|
|
119
|
+
iteration,
|
|
120
|
+
window: snap.window,
|
|
121
|
+
systemBudget: snap.budget.systemPrompt,
|
|
122
|
+
reserveBudget: snap.budget.responseReserve,
|
|
123
|
+
historyBudget: snap.budget.history,
|
|
124
|
+
systemTokens: kind === "start" && systemMsg && typeof systemMsg.content === "string"
|
|
125
|
+
? this.deps.llmProvider.countTokens(systemMsg.content)
|
|
126
|
+
: undefined,
|
|
127
|
+
toolTokens: snap.toolTokens,
|
|
128
|
+
tokens: snap.tokens,
|
|
129
|
+
quality: snap.quality,
|
|
130
|
+
messageCount: snap.messageCount,
|
|
131
|
+
compactionCount: snap.compactionCount,
|
|
132
|
+
iterationsSinceCompaction: snap.iterationsSinceCompaction,
|
|
133
|
+
blocks,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
getSystemPromptInfo() {
|
|
137
|
+
const { prompt, excluded } = this.buildSystemPrompt();
|
|
138
|
+
const tokenCount = this.deps.llmProvider.countTokens(prompt);
|
|
139
|
+
return { text: prompt, tokenCount, excluded };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Some OpenAI-compatible backends (llama.cpp) omit `usage` from responses,
|
|
143
|
+
* leaving apiPromptTokens/apiCompletionTokens at 0. Fall back to local
|
|
144
|
+
* estimates so JSON results still carry meaningful token metrics.
|
|
145
|
+
*/
|
|
146
|
+
resolveUsageTokens(apiPromptTokens, apiCompletionTokens, estimatedPromptTokens, completionChars) {
|
|
147
|
+
if (apiPromptTokens > 0 || apiCompletionTokens > 0) {
|
|
148
|
+
return {
|
|
149
|
+
prompt: apiPromptTokens,
|
|
150
|
+
completion: apiCompletionTokens,
|
|
151
|
+
total: apiPromptTokens + apiCompletionTokens,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// ~4 chars per token is a reasonable heuristic when the backend gives
|
|
155
|
+
// us nothing (matches the pre-tiktoken fallback elsewhere in the code).
|
|
156
|
+
const prompt = Math.max(1, estimatedPromptTokens);
|
|
157
|
+
const completion = Math.max(0, Math.ceil(completionChars / 4));
|
|
158
|
+
return { prompt, completion, total: prompt + completion };
|
|
159
|
+
}
|
|
160
|
+
refreshSystemPrompt() {
|
|
161
|
+
const { prompt } = this.buildSystemPrompt();
|
|
162
|
+
const current = this.deps.contextManager.getActiveHistory().find((m) => m.role === "system");
|
|
163
|
+
if (!current || current.content !== prompt) {
|
|
164
|
+
this.deps.contextManager.updateSystemPrompt?.(prompt);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
truncateToolOutput(output, budget, currentTokens, bounded) {
|
|
168
|
+
const remainingBudget = Math.max(0, budget.history - currentTokens);
|
|
169
|
+
const maxChars = toolOutputCharLimit(remainingBudget, budget.history, bounded);
|
|
170
|
+
if (output.length <= maxChars)
|
|
171
|
+
return output;
|
|
172
|
+
const truncated = output.slice(0, maxChars);
|
|
173
|
+
const removedChars = output.length - maxChars;
|
|
174
|
+
return truncated + `\n\n${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
|
|
175
|
+
}
|
|
176
|
+
emitPhase(iteration, phase, onPhase) {
|
|
177
|
+
this.deps.pluginManager.runOnPhase?.({
|
|
178
|
+
iteration,
|
|
179
|
+
logger: this.deps.logger,
|
|
180
|
+
contextManager: this.deps.contextManager,
|
|
181
|
+
}, phase);
|
|
182
|
+
onPhase?.(phase);
|
|
183
|
+
}
|
|
184
|
+
async run(input, onChunk, onMeta, onTool, onPhase) {
|
|
185
|
+
// Reset shutdown flag from previous interrupt
|
|
186
|
+
this.shutdownRequested = false;
|
|
187
|
+
this.setScope();
|
|
188
|
+
const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
|
|
189
|
+
const slog = new SessionLogger(sessionManager, logger, () => this.callerProvenance());
|
|
190
|
+
if (sessionManager && !sessionManager.getActive()) {
|
|
191
|
+
sessionManager.create();
|
|
192
|
+
logger.debug(`Session started: ${sessionManager.getActive()}`);
|
|
193
|
+
}
|
|
194
|
+
if (!this.systemPromptAdded &&
|
|
195
|
+
!contextManager.getActiveHistory().some((m) => m.role === "system")) {
|
|
196
|
+
// Log session startup diagnostics (environment, plugins, skills) as the
|
|
197
|
+
// first entry in session.jsonl so the file is self-describing for
|
|
198
|
+
// post-mortem analysis. LSP probe is logged separately from repl.ts
|
|
199
|
+
// since it may complete after this point. Dedup: logSessionStart
|
|
200
|
+
// checks for existing session_start entry to avoid double-logging.
|
|
201
|
+
slog.logSessionStart({
|
|
202
|
+
environment: this.deps.envReport,
|
|
203
|
+
});
|
|
204
|
+
const lazy = this.deps.lazyPromptBlocks ? await this.deps.lazyPromptBlocks() : [];
|
|
205
|
+
if (lazy.length > 0) {
|
|
206
|
+
this.deps.promptBlocks.push(...lazy);
|
|
207
|
+
}
|
|
208
|
+
const { prompt: systemPrompt, excluded } = this.buildSystemPrompt();
|
|
209
|
+
contextManager.addMessage({ role: "system", content: systemPrompt });
|
|
210
|
+
this.systemPromptAdded = true;
|
|
211
|
+
slog.logSystem(systemPrompt.slice(0, 2000));
|
|
212
|
+
if (excluded.length > 0) {
|
|
213
|
+
slog.logSystem(`[Excluded prompt blocks: ${excluded.length}]`);
|
|
214
|
+
}
|
|
215
|
+
pluginManager.runOnSessionStart({
|
|
216
|
+
logger,
|
|
217
|
+
sessionManager: sessionManager?.getActiveMeta(),
|
|
218
|
+
contextManager,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// A new user message starts a fresh compaction interval — iterations
|
|
222
|
+
// from the previous turn must not trigger a compaction on iteration 1
|
|
223
|
+
// of this one (observed: turn 2 compacted at iteration 8, deleting the
|
|
224
|
+
// just-sent task along with 40+ old turns).
|
|
225
|
+
if (typeof contextManager.resetUserTurn === "function") {
|
|
226
|
+
contextManager.resetUserTurn();
|
|
227
|
+
}
|
|
228
|
+
contextManager.addMessage({ role: "user", content: input });
|
|
229
|
+
slog.logUser(input);
|
|
230
|
+
// Turn lifecycle for plugins: count tool events without touching the loop,
|
|
231
|
+
// and dispatch onTurnEnd exactly once — success, error or interrupt.
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
let toolCalls = 0;
|
|
234
|
+
const countTool = (ev) => {
|
|
235
|
+
if (ev.type === "end")
|
|
236
|
+
toolCalls++;
|
|
237
|
+
onTool?.(ev);
|
|
238
|
+
};
|
|
239
|
+
const emitTurnEnd = (result, err) => {
|
|
240
|
+
const interrupted = (err instanceof Error && err.name === "AbortError") || this.shutdownRequested;
|
|
241
|
+
pluginManager.runOnTurnEnd?.({
|
|
242
|
+
logger,
|
|
243
|
+
sessionManager: sessionManager?.getActiveMeta(),
|
|
244
|
+
contextManager,
|
|
245
|
+
}, {
|
|
246
|
+
success: !!result && result.success !== false && !err,
|
|
247
|
+
durationMs: Date.now() - startedAt,
|
|
248
|
+
textLength: (result?.text || "").length,
|
|
249
|
+
toolCalls,
|
|
250
|
+
interrupted: interrupted || undefined,
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
const result = config.moe?.enabled === true
|
|
255
|
+
? await runWithMoE({
|
|
256
|
+
config,
|
|
257
|
+
llmProvider,
|
|
258
|
+
toolExecutor,
|
|
259
|
+
logger,
|
|
260
|
+
baseDir: this.deps.baseDir,
|
|
261
|
+
}, input, () => this.executeSingleAgentLoop(input, onChunk, onMeta, countTool, onPhase), { onMeta, onTool: countTool, onPhase })
|
|
262
|
+
: await this.executeSingleAgentLoop(input, onChunk, onMeta, countTool, onPhase);
|
|
263
|
+
emitTurnEnd(result);
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
emitTurnEnd(null, err);
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async executeSingleAgentLoop(input, onChunk, onMeta, onTool, onPhase) {
|
|
272
|
+
const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, sessionManager, baseDir, } = this.deps;
|
|
273
|
+
const slog = new SessionLogger(sessionManager, logger, () => this.callerProvenance());
|
|
274
|
+
this.abortController = new AbortController();
|
|
275
|
+
let iteration = 0;
|
|
276
|
+
let lastText = "";
|
|
277
|
+
// The actual most-recent model output (tool commentary, retried answers
|
|
278
|
+
// included). Repetition is compared against THIS, not the last *accepted*
|
|
279
|
+
// text — a frozen accepted answer made consecutive retries compare
|
|
280
|
+
// against a stale baseline and flag every re-answer as repetitive.
|
|
281
|
+
let lastModelText = "";
|
|
282
|
+
let lastForcedCompactionIteration = -FORCED_COMPACTION_COOLDOWN;
|
|
283
|
+
let hallucinationRetries = 0;
|
|
284
|
+
let lastToolSignature = "";
|
|
285
|
+
let apiPromptTokens = 0;
|
|
286
|
+
let apiCompletionTokens = 0;
|
|
287
|
+
let apiCompletionChars = 0;
|
|
288
|
+
const MAX_HALLUCINATION_RETRIES = 3;
|
|
289
|
+
let consecutiveToolFailures = 0;
|
|
290
|
+
const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
291
|
+
// Per-tool failure counts and which tools already produced a memory rule.
|
|
292
|
+
const toolFailureCounts = new Map();
|
|
293
|
+
const memoryRuleRecorded = new Set();
|
|
294
|
+
const MIN_REPEATED_TOOL_FAILURES = 3;
|
|
295
|
+
let auditRetries = 0;
|
|
296
|
+
const MAX_AUDIT_RETRIES = 3;
|
|
297
|
+
let emptyResponseRetries = 0;
|
|
298
|
+
const MAX_EMPTY_RESPONSE_RETRIES = 2;
|
|
299
|
+
let emptyResponseExhausted = false;
|
|
300
|
+
let auditFailed = false;
|
|
301
|
+
let lastAuditSummary = "";
|
|
302
|
+
// Set when the audit gate rejects a final answer and re-prompts: the next
|
|
303
|
+
// non-tool response is then a re-answer of an already-completed task, so
|
|
304
|
+
// a repetition verdict is expected and must not burn a hallucination retry.
|
|
305
|
+
let suppressRepetitionRetry = false;
|
|
306
|
+
let repeatedToolCount = 0;
|
|
307
|
+
const MAX_REPEATED_TOOL_CALLS = 2;
|
|
308
|
+
let llmErrorRetries = 0;
|
|
309
|
+
// Account for tool definitions in context budget (they're sent via body.tools, not messages)
|
|
310
|
+
// Tool definitions are recomputed each iteration so `enable_tools`
|
|
311
|
+
// (which mutates the shared activeToolTags array) can grow the
|
|
312
|
+
// LLM-visible tool set mid-run. Bound per-iteration to keep the budget
|
|
313
|
+
// estimate and boundedOutput set in sync with what is actually sent.
|
|
314
|
+
let allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
|
|
315
|
+
let boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
|
|
316
|
+
let toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
|
|
317
|
+
contextManager.setToolTokens(toolTokenEstimate);
|
|
318
|
+
while (iteration < config.maxToolIterations && !this.shutdownRequested) {
|
|
319
|
+
iteration++;
|
|
320
|
+
contextManager.noteIteration();
|
|
321
|
+
// Re-read the mutable tag set in case enable_tools was called.
|
|
322
|
+
allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
|
|
323
|
+
boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
|
|
324
|
+
toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
|
|
325
|
+
contextManager.setToolTokens(toolTokenEstimate);
|
|
326
|
+
pluginManager.runOnBeforeThink({
|
|
327
|
+
iteration,
|
|
328
|
+
logger,
|
|
329
|
+
lastUserMessage: input,
|
|
330
|
+
contextManager,
|
|
331
|
+
onMeta,
|
|
332
|
+
sessionLog: {
|
|
333
|
+
plan: (event, detail, iter) => slog.logPlan(event, detail, iter),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
if (contextManager.needsCompaction()) {
|
|
337
|
+
const result = contextManager.compact();
|
|
338
|
+
if (result) {
|
|
339
|
+
logger.debug("Context compacted");
|
|
340
|
+
slog.logCompaction({ reason: "interval", iteration, ...result });
|
|
341
|
+
// Seed hallucination checker with files from compaction summary
|
|
342
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const currentTokens = contextManager.getEstimatedTokens();
|
|
346
|
+
const budget = contextManager.getBudget();
|
|
347
|
+
const quality = contextManager.getQuality();
|
|
348
|
+
if (quality < QUALITY_TRIGGER_THRESHOLD &&
|
|
349
|
+
contextManager.getCompactionCount() > 0 &&
|
|
350
|
+
iteration - lastForcedCompactionIteration >= FORCED_COMPACTION_COOLDOWN) {
|
|
351
|
+
lastForcedCompactionIteration = iteration;
|
|
352
|
+
const result = contextManager.compact();
|
|
353
|
+
logger.warn(`Low context quality (${quality}%) — forced compaction`);
|
|
354
|
+
if (result) {
|
|
355
|
+
slog.logCompaction({
|
|
356
|
+
reason: `quality-triggered (${quality}% < ${QUALITY_TRIGGER_THRESHOLD}%)`,
|
|
357
|
+
iteration,
|
|
358
|
+
...result,
|
|
359
|
+
});
|
|
360
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (currentTokens > budget.history) {
|
|
364
|
+
const result = contextManager.compact();
|
|
365
|
+
logger.warn(`Context overflow (${currentTokens} > ${budget.history}), forced compaction`);
|
|
366
|
+
if (result) {
|
|
367
|
+
slog.logCompaction({
|
|
368
|
+
reason: `overflow (${currentTokens} > ${budget.history})`,
|
|
369
|
+
iteration,
|
|
370
|
+
...result,
|
|
371
|
+
});
|
|
372
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
this.refreshSystemPrompt();
|
|
376
|
+
const history = contextManager.getActiveHistory();
|
|
377
|
+
slog.logToolDefs(allToolsForBudget.length, allToolsForBudget.map((t) => t.name), iteration);
|
|
378
|
+
if (iteration === 1) {
|
|
379
|
+
const { blocks } = this.buildSystemPrompt();
|
|
380
|
+
this.logContextStat("start", iteration, slog, blocks);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
this.logContextStat("iteration", iteration, slog);
|
|
384
|
+
}
|
|
385
|
+
let textContent = "";
|
|
386
|
+
let reasoningContent = "";
|
|
387
|
+
const toolCalls = [];
|
|
388
|
+
let sawToolCall = false;
|
|
389
|
+
let emittedReasoning = false;
|
|
390
|
+
const textChunks = [];
|
|
391
|
+
this.emitPhase(iteration, "thinking", onPhase);
|
|
392
|
+
const llmStart = Date.now();
|
|
393
|
+
const promptBefore = apiPromptTokens;
|
|
394
|
+
const completionBefore = apiCompletionTokens;
|
|
395
|
+
logger.logLLMRequest(config.model, history.length, input, "agent");
|
|
396
|
+
try {
|
|
397
|
+
for await (const chunk of llmProvider.chat(history, allToolsForBudget, this.abortController?.signal)) {
|
|
398
|
+
if (this.shutdownRequested)
|
|
399
|
+
break;
|
|
400
|
+
if (chunk.type === "text" && chunk.content) {
|
|
401
|
+
if (emittedReasoning && !textContent) {
|
|
402
|
+
onMeta?.("\n\n");
|
|
403
|
+
}
|
|
404
|
+
textContent += chunk.content;
|
|
405
|
+
textChunks.push(chunk.content);
|
|
406
|
+
}
|
|
407
|
+
if (chunk.type === "reasoning" && chunk.content) {
|
|
408
|
+
reasoningContent += chunk.content;
|
|
409
|
+
if (config.showReasoning) {
|
|
410
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, chunk.content);
|
|
411
|
+
if (metaOut) {
|
|
412
|
+
onMeta?.(pc.dim(metaOut));
|
|
413
|
+
}
|
|
414
|
+
emittedReasoning = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
418
|
+
sawToolCall = true;
|
|
419
|
+
let parsedArgs;
|
|
420
|
+
try {
|
|
421
|
+
parsedArgs = JSON.parse(chunk.toolCall.arguments);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
parsedArgs = {};
|
|
425
|
+
}
|
|
426
|
+
toolCalls.push({
|
|
427
|
+
id: chunk.toolCall.id,
|
|
428
|
+
name: chunk.toolCall.name,
|
|
429
|
+
arguments: parsedArgs,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (chunk.type === "done" && chunk.usage) {
|
|
433
|
+
apiPromptTokens += chunk.usage.promptTokens;
|
|
434
|
+
apiCompletionTokens += chunk.usage.completionTokens;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
if (this.shutdownRequested || err?.name === "AbortError") {
|
|
440
|
+
logger.info("LLM call aborted (interrupt)");
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
// Recoverable LLM errors (e.g. the completion hit the token limit
|
|
444
|
+
// mid-tool_call): feed the localized reason back into context so the
|
|
445
|
+
// model can adapt (split the write into smaller chunks) instead of
|
|
446
|
+
// the whole session dying. Bounded to avoid infinite loops.
|
|
447
|
+
if (err?.recoverableLlm && llmErrorRetries < MAX_LLM_ERROR_RETRIES) {
|
|
448
|
+
llmErrorRetries++;
|
|
449
|
+
logger.warn(`Recoverable LLM error, feeding back (${llmErrorRetries}/${MAX_LLM_ERROR_RETRIES}): ${err.message}`);
|
|
450
|
+
slog.logError(err.message);
|
|
451
|
+
pluginManager.runOnError({ iteration, logger, contextManager }, err);
|
|
452
|
+
contextManager.addMessage({
|
|
453
|
+
role: "user",
|
|
454
|
+
content: `<system-summary>${err.message}</system-summary>`,
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
logger.logLLMResponse(config.model, textContent.length, Date.now() - llmStart, err.message, "agent");
|
|
459
|
+
logger.error(`LLM call failed: ${err.message}`);
|
|
460
|
+
slog.logError(err.message);
|
|
461
|
+
pluginManager.runOnError({ iteration, logger, contextManager }, err);
|
|
462
|
+
return {
|
|
463
|
+
success: false,
|
|
464
|
+
text: lastText,
|
|
465
|
+
error: t("error.llm", { message: err.message }),
|
|
466
|
+
iterationCount: iteration,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
this.emitPhase(iteration, "done", onPhase);
|
|
471
|
+
}
|
|
472
|
+
// Track response length so token metrics stay meaningful even when
|
|
473
|
+
// the backend omits `usage` from the response.
|
|
474
|
+
apiCompletionChars += (textContent || reasoningContent).length;
|
|
475
|
+
logger.logLLMResponse(config.model, (textContent || reasoningContent).length, Date.now() - llmStart, undefined, "agent");
|
|
476
|
+
// Log per-call token usage. llama.cpp streaming often omits `usage`,
|
|
477
|
+
// so fall back to local estimates (context tokens + chars/4) and
|
|
478
|
+
// mark the source — the log must distinguish real API numbers from
|
|
479
|
+
// heuristics.
|
|
480
|
+
{
|
|
481
|
+
const usagePrompt = apiPromptTokens - promptBefore;
|
|
482
|
+
const usageCompletion = apiCompletionTokens - completionBefore;
|
|
483
|
+
const source = usagePrompt > 0 || usageCompletion > 0 ? "api" : "estimate";
|
|
484
|
+
const prompt = source === "api" ? usagePrompt : contextManager.getEstimatedTokens();
|
|
485
|
+
const completion = source === "api"
|
|
486
|
+
? usageCompletion
|
|
487
|
+
: Math.ceil((textContent || reasoningContent).length / 4);
|
|
488
|
+
slog.logLlmUsage(iteration, {
|
|
489
|
+
promptTokens: prompt,
|
|
490
|
+
completionTokens: completion,
|
|
491
|
+
totalTokens: prompt + completion,
|
|
492
|
+
source,
|
|
493
|
+
durationMs: Date.now() - llmStart,
|
|
494
|
+
});
|
|
495
|
+
this.costTracker.record(prompt, completion, this.callerProvenance().provider);
|
|
496
|
+
}
|
|
497
|
+
if (this.shutdownRequested) {
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
// Show the model's commentary text. When a tool call accompanies the
|
|
501
|
+
// response, keep the text too (opencode-like narration), unless it is
|
|
502
|
+
// a raw JSON payload that small models sometimes emit instead of
|
|
503
|
+
// describing the call. `toolComments: false` restores the old behavior
|
|
504
|
+
// of suppressing text next to a tool call.
|
|
505
|
+
const toolComments = this.deps.config.ui?.toolComments ?? true;
|
|
506
|
+
const showText = textChunks.length > 0 && (!sawToolCall || (toolComments && !isToolCallJson(textContent)));
|
|
507
|
+
if (showText) {
|
|
508
|
+
for (const chunk of textChunks) {
|
|
509
|
+
const textOut = pluginManager.runOnText({ iteration, logger, contextManager }, chunk);
|
|
510
|
+
onChunk?.(textOut);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
let llmResponse = null;
|
|
514
|
+
if (sawToolCall) {
|
|
515
|
+
llmResponse = { type: "tool_call", calls: toolCalls };
|
|
516
|
+
}
|
|
517
|
+
else if (textContent) {
|
|
518
|
+
llmResponse = { type: "text", content: textContent };
|
|
519
|
+
}
|
|
520
|
+
else if (reasoningContent) {
|
|
521
|
+
llmResponse = { type: "reasoning", content: reasoningContent };
|
|
522
|
+
}
|
|
523
|
+
pluginManager.runOnAfterThink({ iteration, logger, contextManager }, llmResponse);
|
|
524
|
+
if (this.deps.exitOnComplete && sawToolCall) {
|
|
525
|
+
const signature = toolCalls
|
|
526
|
+
.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`)
|
|
527
|
+
.join("|");
|
|
528
|
+
if (signature && signature === lastToolSignature) {
|
|
529
|
+
// A repeated identical tool call is often the model re-running
|
|
530
|
+
// a command after a confusing result. Give it one more chance
|
|
531
|
+
// to produce a final text answer instead of stopping with
|
|
532
|
+
// text: "" (observed on 08-r4: bash re-run → empty result).
|
|
533
|
+
repeatedToolCount++;
|
|
534
|
+
if (repeatedToolCount >= MAX_REPEATED_TOOL_CALLS) {
|
|
535
|
+
logger.debug("Exit-on-complete: repeated identical tool call, stopping");
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
contextManager.addMessage({
|
|
539
|
+
role: "user",
|
|
540
|
+
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>`,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
lastToolSignature = signature;
|
|
544
|
+
}
|
|
545
|
+
if (sawToolCall) {
|
|
546
|
+
slog.logAssistant(textContent || "", reasoningContent, toolCalls, iteration);
|
|
547
|
+
}
|
|
548
|
+
if (sawToolCall) {
|
|
549
|
+
contextManager.addMessage({
|
|
550
|
+
role: "assistant",
|
|
551
|
+
content: textContent || "",
|
|
552
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
553
|
+
id: tc.id,
|
|
554
|
+
type: "function",
|
|
555
|
+
function: {
|
|
556
|
+
name: tc.name,
|
|
557
|
+
arguments: JSON.stringify(tc.arguments),
|
|
558
|
+
},
|
|
559
|
+
})),
|
|
560
|
+
});
|
|
561
|
+
const summaries = [];
|
|
562
|
+
let anyToolFailed = false;
|
|
563
|
+
// Informational read-only tools whose "failure" is expected behavior
|
|
564
|
+
// (e.g. file_info "Not found" after delete_file) — excluded from
|
|
565
|
+
// per-tool failure counting and consecutive failure tracking.
|
|
566
|
+
const infoTools = new Set(["file_info"]);
|
|
567
|
+
for (const call of toolCalls) {
|
|
568
|
+
this.setScope();
|
|
569
|
+
const startTime = Date.now();
|
|
570
|
+
pluginManager.runOnToolCall({
|
|
571
|
+
toolName: call.name,
|
|
572
|
+
args: call.arguments,
|
|
573
|
+
});
|
|
574
|
+
pluginManager.runOnToolStart({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments });
|
|
575
|
+
const toolIcon = this.deps.toolExecutor.getRegistry().get(call.name)?.icon;
|
|
576
|
+
onTool?.({ type: "start", tool: call.name, args: call.arguments, icon: toolIcon });
|
|
577
|
+
slog.logToolCall(call, iteration);
|
|
578
|
+
const tokensBeforeTool = contextManager.getEstimatedTokens();
|
|
579
|
+
const result = await toolExecutor.execute(call, this.abortController?.signal);
|
|
580
|
+
// Interrupt during the tool call: the session is already shutting
|
|
581
|
+
// down (Esc/Ctrl+C), so stop immediately — no rendering, no session
|
|
582
|
+
// log writes, no context update for a result nobody will see. This
|
|
583
|
+
// fixes the "Replaced in ..." lines appearing AFTER "Session ended".
|
|
584
|
+
if (this.shutdownRequested)
|
|
585
|
+
break;
|
|
586
|
+
const duration = Date.now() - startTime;
|
|
587
|
+
if (!result.success && !infoTools.has(call.name))
|
|
588
|
+
anyToolFailed = true;
|
|
589
|
+
if (result.success && call.arguments.path) {
|
|
590
|
+
const filePath = String(call.arguments.path);
|
|
591
|
+
if (call.name === "write_file" || call.name === "edit_file") {
|
|
592
|
+
hallucinationDetector.getConsistencyCheck().trackCreatedFile(filePath);
|
|
593
|
+
}
|
|
594
|
+
else if (call.name === "delete_file") {
|
|
595
|
+
hallucinationDetector.getConsistencyCheck().trackDeletedFile(filePath);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
pluginManager.runOnToolEnd({ iteration, logger, contextManager }, { id: call.id, name: call.name, arguments: call.arguments }, result, duration);
|
|
599
|
+
// Presence check, not truthiness: read_file returns display ""
|
|
600
|
+
// on a full read to suppress output entirely (the tool header
|
|
601
|
+
// already shows the path) — falling back on empty string would
|
|
602
|
+
// dump the whole file content into the REPL.
|
|
603
|
+
if (result.display !== undefined) {
|
|
604
|
+
if (result.display.length > 0) {
|
|
605
|
+
onMeta?.("\n" + result.display + "\n");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const metaOut = pluginManager.runOnMeta({ iteration, logger, contextManager }, result.output);
|
|
610
|
+
onMeta?.("\n" + pc.dim(metaOut) + "\n");
|
|
611
|
+
}
|
|
612
|
+
if (result.diff) {
|
|
613
|
+
onMeta?.("\n" + result.diff + "\n");
|
|
614
|
+
}
|
|
615
|
+
const currentTokens = contextManager.getEstimatedTokens();
|
|
616
|
+
const budget = contextManager.getBudget();
|
|
617
|
+
const truncatedOutput = this.truncateToolOutput(result.output, budget, currentTokens, boundedToolNames.has(call.name));
|
|
618
|
+
contextManager.addMessage({
|
|
619
|
+
role: "tool",
|
|
620
|
+
content: truncatedOutput,
|
|
621
|
+
name: call.name,
|
|
622
|
+
tool_call_id: call.id,
|
|
623
|
+
success: result.success,
|
|
624
|
+
arguments: call.arguments,
|
|
625
|
+
});
|
|
626
|
+
const tokensAfterTool = contextManager.getEstimatedTokens();
|
|
627
|
+
onTool?.({
|
|
628
|
+
type: "end",
|
|
629
|
+
tool: call.name,
|
|
630
|
+
args: call.arguments,
|
|
631
|
+
duration,
|
|
632
|
+
error: !result.success,
|
|
633
|
+
ctxDelta: tokensAfterTool - tokensBeforeTool,
|
|
634
|
+
costUsd: this.costTracker.total,
|
|
635
|
+
});
|
|
636
|
+
summaries.push(`[Tool: ${call.name} (${JSON.stringify(call.arguments)}) → ${truncatedOutput.slice(0, 200)}]`);
|
|
637
|
+
if (config.session.autoSave) {
|
|
638
|
+
slog.logToolResult(call, result, duration, iteration);
|
|
639
|
+
}
|
|
640
|
+
if (contextManager.needsCompaction()) {
|
|
641
|
+
const result = contextManager.compact();
|
|
642
|
+
if (result) {
|
|
643
|
+
logger.debug("Context compacted after tool result");
|
|
644
|
+
slog.logCompaction({
|
|
645
|
+
reason: "after_tool",
|
|
646
|
+
iteration,
|
|
647
|
+
...result,
|
|
648
|
+
});
|
|
649
|
+
hallucinationDetector.addKnownFiles(contextManager.getKnownFiles());
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Track per-tool failure counts (NOT just consecutive) so a tool
|
|
653
|
+
// that keeps failing while other tools succeed between attempts
|
|
654
|
+
// is still learned from (observed: LSP spawn npx ENOENT failed
|
|
655
|
+
// 8x in one session, never consecutively, so it never reached
|
|
656
|
+
// memory via the 5-consecutive-failures path).
|
|
657
|
+
if (!result.success && !infoTools.has(call.name)) {
|
|
658
|
+
const key = call.name;
|
|
659
|
+
const prev = toolFailureCounts.get(key) ?? { count: 0, error: "" };
|
|
660
|
+
prev.count++;
|
|
661
|
+
prev.error = String(result.output ?? "").slice(0, 200);
|
|
662
|
+
toolFailureCounts.set(key, prev);
|
|
663
|
+
if (prev.count >= MIN_REPEATED_TOOL_FAILURES && !memoryRuleRecorded.has(key)) {
|
|
664
|
+
memoryRuleRecorded.add(key);
|
|
665
|
+
const memStore = this.deps.memoryStore;
|
|
666
|
+
if (memStore) {
|
|
667
|
+
memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.error})`, `Repeated ${key} failures suggest a systemic problem (config, environment, or a broken tool), not a one-off`, "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool");
|
|
668
|
+
logger.warn(`Recorded repeated ${key} failures to memory (${prev.count}x)`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (this.shutdownRequested)
|
|
674
|
+
break;
|
|
675
|
+
if (anyToolFailed) {
|
|
676
|
+
consecutiveToolFailures++;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
consecutiveToolFailures = 0;
|
|
680
|
+
}
|
|
681
|
+
if (consecutiveToolFailures >= MAX_CONSECUTIVE_TOOL_FAILURES) {
|
|
682
|
+
const recoveryMsg = t("exec.consecutive_failures_recovery", {
|
|
683
|
+
count: consecutiveToolFailures,
|
|
684
|
+
});
|
|
685
|
+
logger.warn(`Consecutive tool failures: ${consecutiveToolFailures}`);
|
|
686
|
+
const taskSnippet = input.length > 200 ? input.slice(0, 200) + "..." : input;
|
|
687
|
+
const taskReminder = t("exec.task_reminder", { task: taskSnippet });
|
|
688
|
+
contextManager.addMessage({
|
|
689
|
+
role: "user",
|
|
690
|
+
content: `<system-summary>${recoveryMsg}\n${taskReminder}</system-summary>`,
|
|
691
|
+
});
|
|
692
|
+
const memStore = this.deps.memoryStore;
|
|
693
|
+
if (memStore) {
|
|
694
|
+
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");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
contextManager.addMessage({
|
|
698
|
+
role: "user",
|
|
699
|
+
content: `<system-summary>${summaries.join("\n")}</system-summary>`,
|
|
700
|
+
});
|
|
701
|
+
const ui = this.deps.config.ui;
|
|
702
|
+
if (ui?.showContextStats) {
|
|
703
|
+
const ctxTokens = contextManager.getEstimatedTokens();
|
|
704
|
+
const ctxBudget = contextManager.getBudget();
|
|
705
|
+
const ctxPct = Math.min(100, Math.round((ctxTokens / ctxBudget.history) * 100));
|
|
706
|
+
const barLen = 10;
|
|
707
|
+
const filled = Math.round((ctxPct / 100) * barLen);
|
|
708
|
+
const ctxBar = pc.green("█".repeat(filled)) + pc.dim("░".repeat(barLen - filled));
|
|
709
|
+
const pctColor = ctxPct >= 75 ? pc.yellow : pc.dim;
|
|
710
|
+
const compCount = contextManager.getCompactionCount();
|
|
711
|
+
const quality = contextManager.getQuality();
|
|
712
|
+
const qualityColor = quality >= 70 ? pc.green : quality >= 40 ? pc.yellow : pc.red;
|
|
713
|
+
onMeta?.(`\n ${ctxBar} ${pctColor(`${ctxPct}%`)} ${pc.dim(`ctx: ${ctxTokens}/${ctxBudget.history}`)} ${pc.dim(`compactions: ${compCount}`)} ${qualityColor(`quality: ${quality}%`)}\n`);
|
|
714
|
+
}
|
|
715
|
+
else if (ui?.showCompaction) {
|
|
716
|
+
const compCount = contextManager.getCompactionCount();
|
|
717
|
+
if (compCount > this.lastCompactionShown) {
|
|
718
|
+
this.lastCompactionShown = compCount;
|
|
719
|
+
onMeta?.(pc.dim(`\n ⟳ Context compacted (${compCount})\n`));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// A tool call means the model kept working instead of just
|
|
723
|
+
// re-answering — the audit-re-answer leniency no longer applies.
|
|
724
|
+
suppressRepetitionRetry = false;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
hallucinationDetector.getConfidenceCheck().setPreviousResponse(lastModelText);
|
|
728
|
+
// Update AFTER setPreviousResponse so the comparison uses the
|
|
729
|
+
// previous iteration's output, not this one (which would otherwise
|
|
730
|
+
// always overlap with itself).
|
|
731
|
+
if (textContent)
|
|
732
|
+
lastModelText = textContent;
|
|
733
|
+
const hallucinationResult = await hallucinationDetector.validate(textContent);
|
|
734
|
+
if (hallucinationResult.status === "block") {
|
|
735
|
+
logger.warn(`Response blocked: ${hallucinationResult.reason}`);
|
|
736
|
+
return {
|
|
737
|
+
success: false,
|
|
738
|
+
text: lastText,
|
|
739
|
+
error: t("error.response_blocked", {
|
|
740
|
+
reason: hallucinationResult.reason || "",
|
|
741
|
+
}),
|
|
742
|
+
iterationCount: iteration,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
if (hallucinationResult.status === "warn") {
|
|
746
|
+
logger.warn(`Hallucination warning: ${hallucinationResult.reason}`);
|
|
747
|
+
const warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
|
|
748
|
+
if (onMeta) {
|
|
749
|
+
onMeta(`\n${pc.yellow(warnLine)}\n`);
|
|
750
|
+
}
|
|
751
|
+
else if (onChunk) {
|
|
752
|
+
onChunk(`\n${warnLine}\n`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (hallucinationResult.status === "retry") {
|
|
756
|
+
if (this.deps.exitOnComplete && textContent?.trim()) {
|
|
757
|
+
logger.debug("Exit-on-complete: stopping on first response");
|
|
758
|
+
// Save the response BEFORE breaking — lastText is still the
|
|
759
|
+
// previous (tool-only) iteration's text, so without this the
|
|
760
|
+
// final answer is lost (reported text: "").
|
|
761
|
+
lastText = textContent;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
if (suppressRepetitionRetry && hallucinationResult.kind === "repetition") {
|
|
765
|
+
// The audit gate rejected the previous final answer and
|
|
766
|
+
// re-prompted the model. Its re-answer restating the completed
|
|
767
|
+
// task is naturally "repetitive" — that is expected, not
|
|
768
|
+
// degeneration. Downgrade to a warning and let the response
|
|
769
|
+
// flow through to the final audit gate again (which is itself
|
|
770
|
+
// bounded by MAX_AUDIT_RETRIES).
|
|
771
|
+
suppressRepetitionRetry = false;
|
|
772
|
+
logger.warn(`Audit-triggered re-answer repetition — not counted as hallucination retry`);
|
|
773
|
+
const warnLine = `${t("hall.uncertainty_prefix").trim()} ${hallucinationResult.reason ?? ""}`;
|
|
774
|
+
if (onMeta) {
|
|
775
|
+
onMeta(`\n${pc.yellow(warnLine)}\n`);
|
|
776
|
+
}
|
|
777
|
+
else if (onChunk) {
|
|
778
|
+
onChunk(`\n${warnLine}\n`);
|
|
779
|
+
}
|
|
780
|
+
// fall through to the acceptance path below (assistant message,
|
|
781
|
+
// lastText, final audit gate).
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// NOTE: with exitOnComplete and an EMPTY text we deliberately
|
|
785
|
+
// do NOT break — the model produced no usable answer yet (same
|
|
786
|
+
// case as the empty-response guard below). Falling through to
|
|
787
|
+
// the retry path keeps us from finishing with text: "".
|
|
788
|
+
if (hallucinationRetries >= MAX_HALLUCINATION_RETRIES) {
|
|
789
|
+
logger.warn(`Hallucination retries exhausted (${MAX_HALLUCINATION_RETRIES}), returning error`);
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
text: lastText,
|
|
793
|
+
error: t("error.response_blocked", {
|
|
794
|
+
reason: t("hall.max_retries_exhausted"),
|
|
795
|
+
}),
|
|
796
|
+
iterationCount: iteration,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
hallucinationRetries++;
|
|
800
|
+
logger.warn(`Hallucination retry (${hallucinationRetries}/${MAX_HALLUCINATION_RETRIES}): ${hallucinationResult.reason}`);
|
|
801
|
+
if (textContent) {
|
|
802
|
+
contextManager.addMessage({
|
|
803
|
+
role: "assistant",
|
|
804
|
+
content: textContent,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
contextManager.addMessage({
|
|
808
|
+
role: "user",
|
|
809
|
+
content: `<system-summary>[Retry context: ${hallucinationResult.reason}. Original task: "${input}". You must either call a needed tool or provide a substantive response. Empty replies are not allowed.]</system-summary>`,
|
|
810
|
+
});
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (textContent) {
|
|
815
|
+
contextManager.addMessage({ role: "assistant", content: textContent });
|
|
816
|
+
if (config.session.autoSave) {
|
|
817
|
+
slog.saveAssistantMessage(textContent);
|
|
818
|
+
}
|
|
819
|
+
slog.logAssistant(textContent, reasoningContent, undefined, iteration);
|
|
820
|
+
}
|
|
821
|
+
lastText = textContent;
|
|
822
|
+
{
|
|
823
|
+
const decisionPatterns = [
|
|
824
|
+
...textContent.matchAll(/(?:plan|decided|decision|решено|план|решение):\s*(.+?)(?:\n|$)/gi),
|
|
825
|
+
];
|
|
826
|
+
for (const match of decisionPatterns) {
|
|
827
|
+
hallucinationDetector
|
|
828
|
+
.getConsistencyCheck()
|
|
829
|
+
.trackDecision(match[1].trim(), "agent_response");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (!sawToolCall) {
|
|
833
|
+
// Guard: the model returned an EMPTY final response (no text, no
|
|
834
|
+
// tool calls — often just reasoning content after context
|
|
835
|
+
// compaction). Nudge it to produce a real answer instead of
|
|
836
|
+
// silently finishing with text: "".
|
|
837
|
+
if (!textContent?.trim()) {
|
|
838
|
+
if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
|
|
839
|
+
emptyResponseRetries++;
|
|
840
|
+
logger.warn(`Empty response on iteration ${iteration} (retry ${emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
|
|
841
|
+
contextManager.addMessage({
|
|
842
|
+
role: "user",
|
|
843
|
+
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>`,
|
|
844
|
+
});
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
emptyResponseExhausted = true;
|
|
848
|
+
logger.warn(`Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
|
|
849
|
+
}
|
|
850
|
+
if (this.deps.finalAudit) {
|
|
851
|
+
const audit = await this.deps.finalAudit();
|
|
852
|
+
if (audit && !audit.passed) {
|
|
853
|
+
logger.warn(`Final audit incomplete: ${audit.summary}`);
|
|
854
|
+
const steps = audit.pendingSteps.slice(0, 5).join("; ") || "—";
|
|
855
|
+
contextManager.addMessage({
|
|
856
|
+
role: "user",
|
|
857
|
+
content: `<system-summary>${t("exec.audit_incomplete", {
|
|
858
|
+
summary: audit.summary,
|
|
859
|
+
steps,
|
|
860
|
+
})}</system-summary>`,
|
|
861
|
+
});
|
|
862
|
+
slog.logAudit(audit.summary, iteration);
|
|
863
|
+
auditRetries++;
|
|
864
|
+
lastAuditSummary = audit.summary;
|
|
865
|
+
if (auditRetries >= MAX_AUDIT_RETRIES || iteration >= config.maxToolIterations - 1) {
|
|
866
|
+
logger.warn(`Final audit still incomplete after ${auditRetries} retries — reporting failure`);
|
|
867
|
+
auditFailed = true;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
// The next non-tool response is a forced re-answer of an
|
|
871
|
+
// already-completed task — allow one repetition without
|
|
872
|
+
// burning the hallucination budget (see retry branch).
|
|
873
|
+
suppressRepetitionRetry = true;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const tokensUsed = contextManager.getEstimatedTokens();
|
|
881
|
+
const budget = contextManager.getBudget();
|
|
882
|
+
const usageTokens = this.resolveUsageTokens(apiPromptTokens, apiCompletionTokens, tokensUsed, apiCompletionChars);
|
|
883
|
+
if (iteration >= config.maxToolIterations) {
|
|
884
|
+
return {
|
|
885
|
+
success: false,
|
|
886
|
+
text: lastText,
|
|
887
|
+
error: t("error.max_iters", { max: config.maxToolIterations }),
|
|
888
|
+
iterationCount: iteration,
|
|
889
|
+
contextUsed: tokensUsed,
|
|
890
|
+
contextLimit: budget.history,
|
|
891
|
+
promptTokens: usageTokens.prompt,
|
|
892
|
+
completionTokens: usageTokens.completion,
|
|
893
|
+
totalTokens: usageTokens.total,
|
|
894
|
+
totalCost: this.costTracker.total,
|
|
895
|
+
costBreakdown: this.costTracker.breakdown(),
|
|
896
|
+
compactionCount: contextManager.getCompactionCount(),
|
|
897
|
+
contextQuality: contextManager.getQuality(),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
success: emptyResponseExhausted || auditFailed ? false : true,
|
|
902
|
+
text: lastText,
|
|
903
|
+
error: emptyResponseExhausted
|
|
904
|
+
? t("error.empty_response")
|
|
905
|
+
: auditFailed
|
|
906
|
+
? t("error.audit_failed", { summary: lastAuditSummary })
|
|
907
|
+
: undefined,
|
|
908
|
+
iterationCount: iteration,
|
|
909
|
+
contextUsed: tokensUsed,
|
|
910
|
+
contextLimit: budget.history,
|
|
911
|
+
promptTokens: usageTokens.prompt,
|
|
912
|
+
completionTokens: usageTokens.completion,
|
|
913
|
+
totalTokens: usageTokens.total,
|
|
914
|
+
totalCost: this.costTracker.total,
|
|
915
|
+
costBreakdown: this.costTracker.breakdown(),
|
|
916
|
+
compactionCount: contextManager.getCompactionCount(),
|
|
917
|
+
contextQuality: contextManager.getQuality(),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
clearContext() {
|
|
921
|
+
this.deps.contextManager.clear();
|
|
922
|
+
this.systemPromptAdded = false;
|
|
923
|
+
}
|
|
924
|
+
async reconfigure(config) {
|
|
925
|
+
const { OpenAICompatProvider } = await import("../llm/openai-compat");
|
|
926
|
+
const { TokenCounter } = await import("../llm/token-counter");
|
|
927
|
+
const newProvider = new OpenAICompatProvider({
|
|
928
|
+
model: config.model,
|
|
929
|
+
baseUrl: config.provider.baseUrl,
|
|
930
|
+
apiKey: config.provider.apiKey,
|
|
931
|
+
contextWindow: config.contextWindow,
|
|
932
|
+
retry: config.retry,
|
|
933
|
+
rateLimits: config.security?.rateLimits,
|
|
934
|
+
maxCompletionTokens: config.provider.entries?.find((e) => e.label === config.provider.active)?.maxCompletionTokens ??
|
|
935
|
+
config.provider.maxCompletionTokens,
|
|
936
|
+
});
|
|
937
|
+
this.deps.llmProvider = newProvider;
|
|
938
|
+
this.deps.toolExecutor.updateProvider(newProvider);
|
|
939
|
+
const newTokenCounter = new TokenCounter(config.model);
|
|
940
|
+
this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
|
|
941
|
+
this.deps.config = config;
|
|
942
|
+
this.costTracker.setModel(config.model);
|
|
943
|
+
}
|
|
944
|
+
setProvider(name, model) {
|
|
945
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
946
|
+
contextWindow: this.deps.config.contextWindow,
|
|
947
|
+
retry: this.deps.config.retry,
|
|
948
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
949
|
+
});
|
|
950
|
+
manager.switch(name, model);
|
|
951
|
+
const providerCfg = manager.toConfig();
|
|
952
|
+
this.deps.config.provider = providerCfg;
|
|
953
|
+
this.deps.llmProvider = manager.active;
|
|
954
|
+
this.deps.toolExecutor.updateProvider(manager.active);
|
|
955
|
+
this.deps.toolExecutor.ctx.llmProvider = manager.active;
|
|
956
|
+
}
|
|
957
|
+
listProviders() {
|
|
958
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
959
|
+
contextWindow: this.deps.config.contextWindow,
|
|
960
|
+
retry: this.deps.config.retry,
|
|
961
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
962
|
+
});
|
|
963
|
+
const activeName = this.deps.config.provider.active;
|
|
964
|
+
return manager.list().map((e) => ({
|
|
965
|
+
...e,
|
|
966
|
+
active: e.label === activeName || e.type === activeName,
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
969
|
+
/** Build (or get cached) the provider for a named entry — health checks, routing. */
|
|
970
|
+
getProviderFor(name) {
|
|
971
|
+
try {
|
|
972
|
+
const manager = new ProviderManager(this.deps.config.provider, {
|
|
973
|
+
contextWindow: this.deps.config.contextWindow,
|
|
974
|
+
retry: this.deps.config.retry,
|
|
975
|
+
rateLimits: this.deps.config.security?.rateLimits,
|
|
976
|
+
});
|
|
977
|
+
manager.setModel(this.deps.config.model);
|
|
978
|
+
manager.switch(name);
|
|
979
|
+
return manager.active;
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
setContext(messages) {
|
|
986
|
+
const { contextManager } = this.deps;
|
|
987
|
+
contextManager.clear();
|
|
988
|
+
const { prompt: systemPrompt } = this.buildSystemPrompt();
|
|
989
|
+
contextManager.addMessage({ role: "system", content: systemPrompt });
|
|
990
|
+
this.systemPromptAdded = true;
|
|
991
|
+
for (const msg of messages) {
|
|
992
|
+
if (msg.role === "system")
|
|
993
|
+
continue;
|
|
994
|
+
contextManager.addMessage({
|
|
995
|
+
role: msg.role,
|
|
996
|
+
content: msg.content,
|
|
997
|
+
name: msg.name,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
shutdown() {
|
|
1002
|
+
this.shutdownRequested = true;
|
|
1003
|
+
this.abortController?.abort();
|
|
1004
|
+
const { pluginManager, logger, sessionManager, contextManager } = this.deps;
|
|
1005
|
+
contextManager.onCompact = null;
|
|
1006
|
+
const killed = processRegistry.killAll();
|
|
1007
|
+
if (killed > 0) {
|
|
1008
|
+
logger.info(`Killed ${killed} background process(es) on shutdown`);
|
|
1009
|
+
}
|
|
1010
|
+
// Note: do NOT close session log here — the session is still active.
|
|
1011
|
+
// Session log is closed only when the session actually ends (exit, session delete).
|
|
1012
|
+
pluginManager.runOnSessionEnd({
|
|
1013
|
+
logger,
|
|
1014
|
+
sessionManager: sessionManager?.getActiveMeta(),
|
|
1015
|
+
contextManager,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|