micro-models-agent 0.63.3 → 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 +148 -1
- 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 +7 -4
- package/dist/i18n/ru.json +7 -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 +1606 -800
- 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/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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MCPClient } from "./client";
|
|
2
2
|
import { MCPRegistry } from "./registry";
|
|
3
|
+
import { Logger } from "../../logger/app-logger";
|
|
4
|
+
const logger = new Logger("warn", "mcp");
|
|
3
5
|
function sanitizeToolName(server, tool) {
|
|
4
6
|
const safeServer = server.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
5
7
|
const safeTool = tool.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -16,16 +18,27 @@ function convertInputSchema(schema) {
|
|
|
16
18
|
}
|
|
17
19
|
return { type: "object", properties: {} };
|
|
18
20
|
}
|
|
21
|
+
/** Timeout for the initial tool discovery call (avoids hanging startup). */
|
|
22
|
+
const DISCOVERY_TIMEOUT_MS = 5_000;
|
|
19
23
|
export class MCPModule {
|
|
20
24
|
config;
|
|
21
25
|
name = "mcp";
|
|
22
26
|
connections = new Map();
|
|
27
|
+
/** Server configs registered at init, not yet connected. */
|
|
28
|
+
serverConfigs = new Map();
|
|
23
29
|
discovered = [];
|
|
24
30
|
initialized = false;
|
|
25
31
|
initError = null;
|
|
32
|
+
/** Servers that failed initial discovery — will not be retried. */
|
|
33
|
+
failedServers = new Set();
|
|
26
34
|
constructor(config) {
|
|
27
35
|
this.config = config;
|
|
28
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Register MCP server configs WITHOUT connecting. Actual connection and tool
|
|
39
|
+
* discovery happens lazily on first tool call — keeps startup fast and
|
|
40
|
+
* offline-safe.
|
|
41
|
+
*/
|
|
29
42
|
async initialize() {
|
|
30
43
|
if (this.initialized)
|
|
31
44
|
return;
|
|
@@ -48,14 +61,76 @@ export class MCPModule {
|
|
|
48
61
|
};
|
|
49
62
|
registry.register(serverConfig);
|
|
50
63
|
}
|
|
64
|
+
// Store configs for lazy connection — no network calls here.
|
|
51
65
|
for (const serverName of registry.list()) {
|
|
52
|
-
|
|
66
|
+
this.serverConfigs.set(serverName, registry.get(serverName));
|
|
67
|
+
}
|
|
68
|
+
// Attempt fast discovery in background (best-effort, timeout-bounded).
|
|
69
|
+
// If it fails, tools are still registered — they'll connect on first call.
|
|
70
|
+
this.discoverAllTools();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Try to connect and discover tools for all registered servers.
|
|
74
|
+
* Runs in background, never blocks the caller. Failures are silent —
|
|
75
|
+
* tools still get registered and will retry connection on first call.
|
|
76
|
+
*/
|
|
77
|
+
async discoverAllTools() {
|
|
78
|
+
for (const [serverName, serverConfig] of this.serverConfigs) {
|
|
79
|
+
if (this.failedServers.has(serverName))
|
|
80
|
+
continue;
|
|
53
81
|
const client = new MCPClient(serverConfig);
|
|
54
82
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
// Race discovery against timeout — never let a dead server block startup.
|
|
84
|
+
await Promise.race([
|
|
85
|
+
(async () => {
|
|
86
|
+
await client.connect();
|
|
87
|
+
const tools = await client.listTools();
|
|
88
|
+
this.connections.set(serverName, client);
|
|
89
|
+
for (const tool of tools) {
|
|
90
|
+
this.discovered.push({
|
|
91
|
+
serverName,
|
|
92
|
+
toolName: tool.name,
|
|
93
|
+
description: tool.description || `Tool on MCP server "${serverName}"`,
|
|
94
|
+
inputSchema: tool.inputSchema || {},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
})(),
|
|
98
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("discovery timeout")), DISCOVERY_TIMEOUT_MS)),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
this.failedServers.add(serverName);
|
|
103
|
+
this.initError = `MCP server "${serverName}" init failed: ${e.message}`;
|
|
104
|
+
await client.disconnect().catch((err) => {
|
|
105
|
+
logger.warn(`cleanup disconnect for "${serverName}" failed`, { error: String(err) });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ensure a specific server is connected and its tools are discovered.
|
|
112
|
+
* Called lazily on first tool invocation for that server.
|
|
113
|
+
*/
|
|
114
|
+
async ensureConnected(serverName) {
|
|
115
|
+
// Already connected?
|
|
116
|
+
const existing = this.connections.get(serverName);
|
|
117
|
+
if (existing?.isConnected())
|
|
118
|
+
return existing;
|
|
119
|
+
// Failed during init — don't retry.
|
|
120
|
+
if (this.failedServers.has(serverName))
|
|
121
|
+
return null;
|
|
122
|
+
const serverConfig = this.serverConfigs.get(serverName);
|
|
123
|
+
if (!serverConfig)
|
|
124
|
+
return null;
|
|
125
|
+
const client = new MCPClient(serverConfig);
|
|
126
|
+
try {
|
|
127
|
+
await client.connect();
|
|
128
|
+
const tools = await client.listTools();
|
|
129
|
+
this.connections.set(serverName, client);
|
|
130
|
+
// Add newly discovered tools (if not already present).
|
|
131
|
+
for (const tool of tools) {
|
|
132
|
+
const alreadyDiscovered = this.discovered.some((d) => d.serverName === serverName && d.toolName === tool.name);
|
|
133
|
+
if (!alreadyDiscovered) {
|
|
59
134
|
this.discovered.push({
|
|
60
135
|
serverName,
|
|
61
136
|
toolName: tool.name,
|
|
@@ -64,14 +139,18 @@ export class MCPModule {
|
|
|
64
139
|
});
|
|
65
140
|
}
|
|
66
141
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
142
|
+
return client;
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
this.failedServers.add(serverName);
|
|
146
|
+
logger.warn(`MCP connect failed for "${serverName}": ${e.message}`);
|
|
147
|
+
await client.disconnect().catch(() => { });
|
|
148
|
+
return null;
|
|
71
149
|
}
|
|
72
150
|
}
|
|
73
151
|
getSystemPromptBlock() {
|
|
74
|
-
|
|
152
|
+
const serverNames = [...this.serverConfigs.keys()];
|
|
153
|
+
if (serverNames.length === 0)
|
|
75
154
|
return null;
|
|
76
155
|
const lines = ["---", "MCP servers available:"];
|
|
77
156
|
const byServer = new Map();
|
|
@@ -80,11 +159,17 @@ export class MCPModule {
|
|
|
80
159
|
list.push(d);
|
|
81
160
|
byServer.set(d.serverName, list);
|
|
82
161
|
}
|
|
83
|
-
for (const
|
|
84
|
-
lines.push(` [${
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
162
|
+
for (const serverName of serverNames) {
|
|
163
|
+
lines.push(` [${serverName}]`);
|
|
164
|
+
const tools = byServer.get(serverName);
|
|
165
|
+
if (tools && tools.length > 0) {
|
|
166
|
+
for (const t of tools) {
|
|
167
|
+
const toolRef = sanitizeToolName(serverName, t.toolName);
|
|
168
|
+
lines.push(` - ${toolRef}: ${t.description}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
lines.push(` (not yet connected — use mcp__${serverName}__connect to discover tools)`);
|
|
88
173
|
}
|
|
89
174
|
}
|
|
90
175
|
lines.push("---");
|
|
@@ -96,45 +181,84 @@ export class MCPModule {
|
|
|
96
181
|
};
|
|
97
182
|
}
|
|
98
183
|
getToolDefinitions() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
184
|
+
// Build definitions from discovered tools AND from registered-but-not-yet-discovered
|
|
185
|
+
// servers (so the LLM sees all configured MCP tools even if discovery failed at startup).
|
|
186
|
+
const defs = [];
|
|
187
|
+
// Tools already discovered (connected at startup).
|
|
188
|
+
for (const d of this.discovered) {
|
|
189
|
+
defs.push({
|
|
190
|
+
name: sanitizeToolName(d.serverName, d.toolName),
|
|
191
|
+
description: `${d.description} [MCP server: ${d.serverName}]`,
|
|
192
|
+
parameters: convertInputSchema(d.inputSchema),
|
|
193
|
+
tags: ["code", "research"],
|
|
194
|
+
handler: async (_ctx, args) => {
|
|
195
|
+
const client = this.connections.get(d.serverName);
|
|
196
|
+
if (!client?.isConnected()) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
output: `MCP server "${d.serverName}" not connected`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const result = await client.callTool(d.toolName, args);
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
output: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
207
|
+
};
|
|
115
208
|
}
|
|
116
|
-
|
|
209
|
+
catch (e) {
|
|
210
|
+
return { success: false, output: `MCP call failed: ${e.message}` };
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Servers that haven't been discovered yet — register placeholder tools
|
|
216
|
+
// that will connect lazily on first call.
|
|
217
|
+
for (const [serverName] of this.serverConfigs) {
|
|
218
|
+
if (this.failedServers.has(serverName))
|
|
219
|
+
continue;
|
|
220
|
+
const alreadyDiscovered = this.discovered.some((d) => d.serverName === serverName);
|
|
221
|
+
if (alreadyDiscovered)
|
|
222
|
+
continue;
|
|
223
|
+
// Register a single "connect" tool so the LLM can trigger discovery.
|
|
224
|
+
defs.push({
|
|
225
|
+
name: sanitizeToolName(serverName, "connect"),
|
|
226
|
+
description: `Connect to MCP server "${serverName}" and discover its tools`,
|
|
227
|
+
parameters: { type: "object", properties: {} },
|
|
228
|
+
tags: ["code", "research"],
|
|
229
|
+
handler: async (_ctx, _args) => {
|
|
230
|
+
const client = await this.ensureConnected(serverName);
|
|
231
|
+
if (!client) {
|
|
232
|
+
return { success: false, output: `MCP server "${serverName}" connection failed` };
|
|
233
|
+
}
|
|
234
|
+
const tools = this.discovered
|
|
235
|
+
.filter((d) => d.serverName === serverName)
|
|
236
|
+
.map((d) => ` - ${sanitizeToolName(serverName, d.toolName)}: ${d.description}`);
|
|
117
237
|
return {
|
|
118
238
|
success: true,
|
|
119
|
-
output:
|
|
239
|
+
output: tools.length > 0
|
|
240
|
+
? `Connected to "${serverName}". Tools:\n${tools.join("\n")}`
|
|
241
|
+
: `Connected to "${serverName}" (no tools discovered)`,
|
|
120
242
|
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
}));
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return defs;
|
|
127
247
|
}
|
|
128
248
|
getPlugin() {
|
|
129
249
|
return {
|
|
130
250
|
name: "mcp_module",
|
|
131
251
|
priority: 0,
|
|
132
252
|
onSessionEnd: async () => {
|
|
133
|
-
for (const client of this.connections
|
|
253
|
+
for (const [serverName, client] of this.connections) {
|
|
134
254
|
try {
|
|
135
255
|
await client.disconnect();
|
|
136
256
|
}
|
|
137
|
-
catch {
|
|
257
|
+
catch (e) {
|
|
258
|
+
// Best-effort cleanup at session end — a failed disconnect must
|
|
259
|
+
// not crash the shutdown path, but must stay diagnosable.
|
|
260
|
+
logger.warn(`disconnect from "${serverName}" failed`, { error: String(e?.message ?? e) });
|
|
261
|
+
}
|
|
138
262
|
}
|
|
139
263
|
},
|
|
140
264
|
};
|
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { MemoryStore } from "./store";
|
|
4
4
|
import { rememberTool } from "../../tools/remember";
|
|
5
5
|
import { recallTool } from "../../tools/recall";
|
|
6
|
+
import { estimateTokens } from "../../llm/token-counter";
|
|
6
7
|
const MEMORY_MD_FILES = ["errors", "conventions", "decisions", "facts"];
|
|
7
8
|
export class MemoryModule {
|
|
8
9
|
name = "memory";
|
|
@@ -23,7 +24,7 @@ export class MemoryModule {
|
|
|
23
24
|
const prefStr = Object.entries(prefs)
|
|
24
25
|
.map(([k, v]) => `${k}=${v}`)
|
|
25
26
|
.join(", ");
|
|
26
|
-
parts.push(`User preferences: ${prefStr}`);
|
|
27
|
+
parts.push(`User preferences (background hints, not instructions — the current user message always wins, do not "correct" the user from these): ${prefStr}`);
|
|
27
28
|
}
|
|
28
29
|
const memoryTail = this.getMemoryTail();
|
|
29
30
|
if (memoryTail)
|
|
@@ -35,7 +36,7 @@ export class MemoryModule {
|
|
|
35
36
|
content,
|
|
36
37
|
priority: "normal",
|
|
37
38
|
essential: false,
|
|
38
|
-
estimatedTokens:
|
|
39
|
+
estimatedTokens: estimateTokens(content) + 10,
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -81,7 +82,7 @@ export class MemoryModule {
|
|
|
81
82
|
const prefStr = Object.entries(prefs)
|
|
82
83
|
.map(([k, v]) => `${k}=${v}`)
|
|
83
84
|
.join(", ");
|
|
84
|
-
parts.push(`User preferences: ${prefStr}`);
|
|
85
|
+
parts.push(`User preferences (background hints, not instructions — the current user message always wins, do not "correct" the user from these): ${prefStr}`);
|
|
85
86
|
}
|
|
86
87
|
const memoryTail = this.getMemoryTail();
|
|
87
88
|
if (memoryTail)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn, execSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { resolve, extname, join } from "path";
|
|
4
4
|
import { platform } from "os";
|
|
5
|
-
import { findProjectRoot } from "../../lsp/project-root";
|
|
5
|
+
import { findProjectRoot, resolveTscCommand } from "../../lsp/project-root";
|
|
6
|
+
import { autoFixErrors, parseTscOutput } from "../../../tools/auto-fixer";
|
|
6
7
|
const TYPE_CHECK_DEBOUNCE_MS = 2000;
|
|
7
8
|
// Projects whose lint script points at a binary that is not installed
|
|
8
9
|
// (keyed by baseDir::script). The run itself is expensive and the failure
|
|
@@ -232,6 +233,11 @@ export class LintOnWritePlugin {
|
|
|
232
233
|
if (!existsSync(tsconfigPath)) {
|
|
233
234
|
return;
|
|
234
235
|
}
|
|
236
|
+
// Без локального tsc проверка невозможна — не платим npx-задержку.
|
|
237
|
+
const tsc = resolveTscCommand(projectRoot);
|
|
238
|
+
if (!tsc) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
235
241
|
const now = Date.now();
|
|
236
242
|
if (this._checkPromise && now - this._checkTimestamp < TYPE_CHECK_DEBOUNCE_MS) {
|
|
237
243
|
const error = await this._checkPromise;
|
|
@@ -241,15 +247,39 @@ export class LintOnWritePlugin {
|
|
|
241
247
|
return;
|
|
242
248
|
}
|
|
243
249
|
this._checkTimestamp = now;
|
|
244
|
-
this._checkPromise = this.runTscCheck(projectRoot, signal);
|
|
250
|
+
this._checkPromise = this.runTscCheck(tsc, projectRoot, signal);
|
|
245
251
|
const error = await this._checkPromise;
|
|
246
252
|
if (error) {
|
|
247
|
-
|
|
253
|
+
// Try to auto-fix fixable errors
|
|
254
|
+
const { stdout, stderr } = await runAsync(`${tsc} --noEmit --skipLibCheck 2>&1`, projectRoot, 30000, signal).catch(() => ({ stdout: "", stderr: error }));
|
|
255
|
+
const output = stderr || stdout;
|
|
256
|
+
const errors = parseTscOutput(output);
|
|
257
|
+
if (errors.length > 0) {
|
|
258
|
+
const fixResult = autoFixErrors(filePath, readFileSync(filePath, "utf-8"), errors);
|
|
259
|
+
if (fixResult.fixed) {
|
|
260
|
+
// Write the fixed content
|
|
261
|
+
writeFileSync(filePath, fixResult.newContent, "utf-8");
|
|
262
|
+
result.output += `\n\n[Auto-fixed]: ${fixResult.applied.join("; ")}`;
|
|
263
|
+
// If there are remaining errors, show them
|
|
264
|
+
if (fixResult.remaining.length > 0) {
|
|
265
|
+
const remainingMsg = fixResult.remaining
|
|
266
|
+
.map((e) => `TS${e.code}: ${e.message}`)
|
|
267
|
+
.join("\n");
|
|
268
|
+
result.output += `\n\n[Remaining errors]: ${remainingMsg}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
result.output += `\n\n[Project typecheck failed]: ${error}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
result.output += `\n\n[Project typecheck failed]: ${error}`;
|
|
277
|
+
}
|
|
248
278
|
}
|
|
249
279
|
}
|
|
250
|
-
async runTscCheck(baseDir, signal) {
|
|
280
|
+
async runTscCheck(tsc, baseDir, signal) {
|
|
251
281
|
try {
|
|
252
|
-
await runAsync(
|
|
282
|
+
const { stdout, stderr } = await runAsync(`${tsc} --noEmit --skipLibCheck`, baseDir, 30000, signal);
|
|
253
283
|
return null;
|
|
254
284
|
}
|
|
255
285
|
catch (err) {
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { semverGt } from "../updater/checker";
|
|
2
|
+
import { Logger } from "../../logger/app-logger";
|
|
3
|
+
const logger = new Logger("warn", "plugins");
|
|
4
|
+
/**
|
|
5
|
+
* Run a (possibly async) plugin hook with error isolation. Sync throws and
|
|
6
|
+
* async rejections are both logged and swallowed — one broken plugin must
|
|
7
|
+
* never crash the agent or other plugins.
|
|
8
|
+
*/
|
|
9
|
+
function safeInvoke(plugin, hookName, fn) {
|
|
10
|
+
try {
|
|
11
|
+
const out = fn();
|
|
12
|
+
if (out instanceof Promise) {
|
|
13
|
+
// The promise rejects AFTER this try/catch has exited — attach a catch
|
|
14
|
+
// now, otherwise isolation is broken by an unhandledRejection.
|
|
15
|
+
out.catch((e) => {
|
|
16
|
+
logger.warn(`plugin "${plugin.name}" ${hookName} rejected`, { error: String(e) });
|
|
17
|
+
});
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
logger.warn(`plugin "${plugin.name}" ${hookName} threw`, { error: String(e) });
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
2
27
|
export class PluginManager {
|
|
3
28
|
plugins = [];
|
|
4
29
|
hostBridge = null;
|
|
@@ -55,14 +80,7 @@ export class PluginManager {
|
|
|
55
80
|
return this.plugins.find((p) => p.name === name) ?? null;
|
|
56
81
|
}
|
|
57
82
|
runOnBuildPrompt() {
|
|
58
|
-
return this.plugins.map((p) =>
|
|
59
|
-
try {
|
|
60
|
-
return p.onBuildPrompt?.() ?? null;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
83
|
+
return this.plugins.map((p) => safeInvoke(p, "onBuildPrompt", () => p.onBuildPrompt?.() ?? null) ?? null);
|
|
66
84
|
}
|
|
67
85
|
async runOnBeforeTool(rawCtx, call) {
|
|
68
86
|
const ctx = this.withHost(rawCtx);
|
|
@@ -73,8 +91,8 @@ export class PluginManager {
|
|
|
73
91
|
if (result === false)
|
|
74
92
|
return false;
|
|
75
93
|
}
|
|
76
|
-
catch {
|
|
77
|
-
|
|
94
|
+
catch (e) {
|
|
95
|
+
logger.warn(`plugin "${plugin.name}" onBeforeTool failed`, { error: String(e) });
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
}
|
|
@@ -87,8 +105,8 @@ export class PluginManager {
|
|
|
87
105
|
try {
|
|
88
106
|
await plugin.onAfterTool(ctx, call, result);
|
|
89
107
|
}
|
|
90
|
-
catch {
|
|
91
|
-
|
|
108
|
+
catch (e) {
|
|
109
|
+
logger.warn(`plugin "${plugin.name}" onAfterTool failed`, { error: String(e) });
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
112
|
}
|
|
@@ -100,8 +118,8 @@ export class PluginManager {
|
|
|
100
118
|
try {
|
|
101
119
|
await plugin.onError(ctx, error);
|
|
102
120
|
}
|
|
103
|
-
catch {
|
|
104
|
-
|
|
121
|
+
catch (e) {
|
|
122
|
+
logger.warn(`plugin "${plugin.name}" onError failed`, { error: String(e) });
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
}
|
|
@@ -110,12 +128,7 @@ export class PluginManager {
|
|
|
110
128
|
const ctx = this.withHost(rawCtx);
|
|
111
129
|
for (const plugin of this.plugins) {
|
|
112
130
|
if (plugin.onSessionStart) {
|
|
113
|
-
|
|
114
|
-
plugin.onSessionStart(ctx);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
/* error isolation */
|
|
118
|
-
}
|
|
131
|
+
safeInvoke(plugin, "onSessionStart", () => plugin.onSessionStart(ctx));
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
}
|
|
@@ -123,12 +136,7 @@ export class PluginManager {
|
|
|
123
136
|
const ctx = this.withHost(rawCtx);
|
|
124
137
|
for (const plugin of this.plugins) {
|
|
125
138
|
if (plugin.onBeforeThink) {
|
|
126
|
-
|
|
127
|
-
plugin.onBeforeThink(ctx);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
/* error isolation */
|
|
131
|
-
}
|
|
139
|
+
safeInvoke(plugin, "onBeforeThink", () => plugin.onBeforeThink(ctx));
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -136,12 +144,7 @@ export class PluginManager {
|
|
|
136
144
|
const ctx = this.withHost(rawCtx);
|
|
137
145
|
for (const plugin of this.plugins) {
|
|
138
146
|
if (plugin.onAfterThink) {
|
|
139
|
-
|
|
140
|
-
plugin.onAfterThink(ctx, response);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
/* error isolation */
|
|
144
|
-
}
|
|
147
|
+
safeInvoke(plugin, "onAfterThink", () => plugin.onAfterThink(ctx, response));
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
}
|
|
@@ -149,12 +152,7 @@ export class PluginManager {
|
|
|
149
152
|
const ctx = this.withHost(rawCtx);
|
|
150
153
|
for (const plugin of this.plugins) {
|
|
151
154
|
if (plugin.onSessionEnd) {
|
|
152
|
-
|
|
153
|
-
plugin.onSessionEnd(ctx);
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
/* error isolation */
|
|
157
|
-
}
|
|
155
|
+
safeInvoke(plugin, "onSessionEnd", () => plugin.onSessionEnd(ctx));
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
}
|
|
@@ -162,12 +160,7 @@ export class PluginManager {
|
|
|
162
160
|
const ctx = this.withHost(rawCtx);
|
|
163
161
|
for (const plugin of this.plugins) {
|
|
164
162
|
if (plugin.onToolCall) {
|
|
165
|
-
|
|
166
|
-
plugin.onToolCall(ctx);
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
/* error isolation */
|
|
170
|
-
}
|
|
163
|
+
safeInvoke(plugin, "onToolCall", () => plugin.onToolCall(ctx));
|
|
171
164
|
}
|
|
172
165
|
}
|
|
173
166
|
}
|
|
@@ -175,12 +168,7 @@ export class PluginManager {
|
|
|
175
168
|
const ctx = this.withHost(rawCtx);
|
|
176
169
|
for (const plugin of this.plugins) {
|
|
177
170
|
if (plugin.onPhase) {
|
|
178
|
-
|
|
179
|
-
plugin.onPhase(ctx, phase);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
/* error isolation */
|
|
183
|
-
}
|
|
171
|
+
safeInvoke(plugin, "onPhase", () => plugin.onPhase(ctx, phase));
|
|
184
172
|
}
|
|
185
173
|
}
|
|
186
174
|
}
|
|
@@ -188,12 +176,7 @@ export class PluginManager {
|
|
|
188
176
|
const ctx = this.withHost(rawCtx);
|
|
189
177
|
for (const plugin of this.plugins) {
|
|
190
178
|
if (plugin.onToolStart) {
|
|
191
|
-
|
|
192
|
-
plugin.onToolStart(ctx, call);
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
/* error isolation */
|
|
196
|
-
}
|
|
179
|
+
safeInvoke(plugin, "onToolStart", () => plugin.onToolStart(ctx, call));
|
|
197
180
|
}
|
|
198
181
|
}
|
|
199
182
|
}
|
|
@@ -201,12 +184,7 @@ export class PluginManager {
|
|
|
201
184
|
const ctx = this.withHost(rawCtx);
|
|
202
185
|
for (const plugin of this.plugins) {
|
|
203
186
|
if (plugin.onToolEnd) {
|
|
204
|
-
|
|
205
|
-
plugin.onToolEnd(ctx, call, result, durationMs);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
/* error isolation */
|
|
209
|
-
}
|
|
187
|
+
safeInvoke(plugin, "onToolEnd", () => plugin.onToolEnd(ctx, call, result, durationMs));
|
|
210
188
|
}
|
|
211
189
|
}
|
|
212
190
|
}
|
|
@@ -215,14 +193,9 @@ export class PluginManager {
|
|
|
215
193
|
const ctx = this.withHost(rawCtx);
|
|
216
194
|
for (const plugin of this.plugins) {
|
|
217
195
|
if (plugin.onText) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
chunk = out;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
/* error isolation */
|
|
225
|
-
}
|
|
196
|
+
const out = safeInvoke(plugin, "onText", () => plugin.onText(ctx, chunk));
|
|
197
|
+
if (typeof out === "string")
|
|
198
|
+
chunk = out;
|
|
226
199
|
}
|
|
227
200
|
}
|
|
228
201
|
return chunk;
|
|
@@ -232,14 +205,9 @@ export class PluginManager {
|
|
|
232
205
|
const ctx = this.withHost(rawCtx);
|
|
233
206
|
for (const plugin of this.plugins) {
|
|
234
207
|
if (plugin.onMeta) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
chunk = out;
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
/* error isolation */
|
|
242
|
-
}
|
|
208
|
+
const out = safeInvoke(plugin, "onMeta", () => plugin.onMeta(ctx, chunk));
|
|
209
|
+
if (typeof out === "string")
|
|
210
|
+
chunk = out;
|
|
243
211
|
}
|
|
244
212
|
}
|
|
245
213
|
return chunk;
|
|
@@ -249,12 +217,7 @@ export class PluginManager {
|
|
|
249
217
|
const ctx = this.withHost(rawCtx);
|
|
250
218
|
for (const plugin of this.plugins) {
|
|
251
219
|
if (plugin.onTurnEnd) {
|
|
252
|
-
|
|
253
|
-
plugin.onTurnEnd(ctx, summary);
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
/* error isolation */
|
|
257
|
-
}
|
|
220
|
+
safeInvoke(plugin, "onTurnEnd", () => plugin.onTurnEnd(ctx, summary));
|
|
258
221
|
}
|
|
259
222
|
}
|
|
260
223
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { calculateCostDetailed, formatCost } from "./prices";
|
|
2
2
|
/**
|
|
3
3
|
* Accumulates per-call LLM cost for a session, attributed to the provider
|
|
4
4
|
* that served the call. Call `record()` after each LLM call with the
|
|
@@ -10,6 +10,7 @@ export class CostTracker {
|
|
|
10
10
|
config;
|
|
11
11
|
model;
|
|
12
12
|
_total = 0;
|
|
13
|
+
_saved = 0;
|
|
13
14
|
_known = false;
|
|
14
15
|
byProvider = new Map();
|
|
15
16
|
constructor(model, config) {
|
|
@@ -20,26 +21,35 @@ export class CostTracker {
|
|
|
20
21
|
setModel(model) {
|
|
21
22
|
this.model = model;
|
|
22
23
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Записывает usage одного LLM-вызова с атрибуцией провайдеру. `cache`
|
|
26
|
+
* разделяет prompt-токены на кешированные/некешированные и накапливает
|
|
27
|
+
* экономию.
|
|
28
|
+
*/
|
|
29
|
+
record(promptTokens, completionTokens, provider, cache) {
|
|
25
30
|
const key = provider || "unknown";
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
31
|
+
const result = calculateCostDetailed(this.model, promptTokens, completionTokens, this.config, cache);
|
|
32
|
+
if (!result)
|
|
28
33
|
return;
|
|
29
34
|
this._known = true;
|
|
30
|
-
this._total += cost;
|
|
35
|
+
this._total += result.cost;
|
|
36
|
+
this._saved += result.saved;
|
|
31
37
|
let entry = this.byProvider.get(key);
|
|
32
38
|
if (!entry) {
|
|
33
39
|
entry = { total: 0, known: false };
|
|
34
40
|
this.byProvider.set(key, entry);
|
|
35
41
|
}
|
|
36
42
|
entry.known = true;
|
|
37
|
-
entry.total += cost;
|
|
43
|
+
entry.total += result.cost;
|
|
38
44
|
}
|
|
39
45
|
/** Running total cost in USD, or undefined if no model price is known. */
|
|
40
46
|
get total() {
|
|
41
47
|
return this._known ? this._total : undefined;
|
|
42
48
|
}
|
|
49
|
+
/** Накопленная экономия на cache read (USD), undefined без известной цены. */
|
|
50
|
+
get saved() {
|
|
51
|
+
return this._known ? this._saved : undefined;
|
|
52
|
+
}
|
|
43
53
|
/** Total for one provider, or undefined when nothing was priced for it. */
|
|
44
54
|
totalFor(provider) {
|
|
45
55
|
const entry = this.byProvider.get(provider);
|