@remnic/core 10.0.5 → 10.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +5 -5
- package/dist/access-authorization-probe.js +6 -6
- package/dist/access-boundary.js +5 -5
- package/dist/access-cli.js +21 -21
- package/dist/access-extraction-force-flush.js +5 -5
- package/dist/access-http-briefing.js +6 -6
- package/dist/access-http-query.js +5 -5
- package/dist/access-http.js +17 -17
- package/dist/access-identity-continuity-surface.js +5 -5
- package/dist/access-lcm-surface.js +5 -5
- package/dist/access-mcp.js +12 -12
- package/dist/access-namespace-preflight.js +5 -5
- package/dist/access-observe-write-surface.js +5 -5
- package/dist/access-operations-batch.js +6 -6
- package/dist/access-operations.js +11 -11
- package/dist/access-recall-concurrency.js +5 -5
- package/dist/access-recall-response.js +5 -5
- package/dist/access-recall-surface.js +5 -5
- package/dist/access-service.js +5 -5
- package/dist/access-surface-catalog.js +7 -7
- package/dist/calibration.d.ts +31 -3
- package/dist/calibration.d.ts.map +1 -1
- package/dist/calibration.js +67 -16
- package/dist/calibration.js.map +1 -1
- package/dist/{chunk-WRICYGYG.js → chunk-2UF42WXV.js} +8 -8
- package/dist/{chunk-5LNPW7NE.js → chunk-4JNTGPBR.js} +2 -2
- package/dist/{chunk-SUAX5JWY.js → chunk-6C76FZRM.js} +2 -2
- package/dist/{chunk-HPAOMYKM.js → chunk-7CZIKCJR.js} +8 -8
- package/dist/{chunk-4CWUZBPF.js → chunk-CCMXCZLT.js} +3 -3
- package/dist/{chunk-NBQIFCJE.js → chunk-D6M3VKMY.js} +3 -9
- package/dist/chunk-D6M3VKMY.js.map +1 -0
- package/dist/{chunk-GVUW5XK5.js → chunk-DTXN4NVD.js} +4 -4
- package/dist/{chunk-5CQO6LVM.js → chunk-KCCZFDJD.js} +2 -2
- package/dist/{chunk-I25RINZK.js → chunk-KQT4DRKF.js} +2 -2
- package/dist/{chunk-AXC7UTLM.js → chunk-MH2DXDN6.js} +2 -2
- package/dist/{chunk-FRH2UC7Q.js → chunk-Q562GF7J.js} +2 -2
- package/dist/{chunk-NHSVW7UT.js → chunk-QEPL7KIZ.js} +2 -2
- package/dist/{chunk-5BY3IA2U.js → chunk-QGLEM46C.js} +2 -2
- package/dist/{chunk-V32NHP2R.js → chunk-TL4PM7PC.js} +2 -2
- package/dist/{chunk-52CQKST6.js → chunk-TRBLJU6Y.js} +4 -4
- package/dist/{chunk-M47Q6566.js → chunk-VUEG3TZ4.js} +2 -2
- package/dist/{chunk-DST7HRSG.js → chunk-YGPXV546.js} +2 -2
- package/dist/cli.js +18 -18
- package/dist/compounding/engine.d.ts.map +1 -1
- package/dist/compounding/engine.js +1 -1
- package/dist/external-wiki-access.js +6 -6
- package/dist/extraction.js +4 -4
- package/dist/index.js +21 -21
- package/dist/memory-browse-operations.js +6 -6
- package/dist/orchestration/maintenance.js +2 -2
- package/dist/orchestrator.js +21 -21
- package/dist/recall-navigation-operations.js +6 -6
- package/dist/recall-navigation.js +5 -5
- package/dist/support-passport/index.js +8 -8
- package/package.json +2 -2
- package/src/calibration-local-llm.test.ts +361 -0
- package/src/calibration.ts +109 -26
- package/src/compounding/engine.ts +2 -8
- package/dist/chunk-NBQIFCJE.js.map +0 -1
- /package/dist/{chunk-WRICYGYG.js.map → chunk-2UF42WXV.js.map} +0 -0
- /package/dist/{chunk-5LNPW7NE.js.map → chunk-4JNTGPBR.js.map} +0 -0
- /package/dist/{chunk-SUAX5JWY.js.map → chunk-6C76FZRM.js.map} +0 -0
- /package/dist/{chunk-HPAOMYKM.js.map → chunk-7CZIKCJR.js.map} +0 -0
- /package/dist/{chunk-4CWUZBPF.js.map → chunk-CCMXCZLT.js.map} +0 -0
- /package/dist/{chunk-GVUW5XK5.js.map → chunk-DTXN4NVD.js.map} +0 -0
- /package/dist/{chunk-5CQO6LVM.js.map → chunk-KCCZFDJD.js.map} +0 -0
- /package/dist/{chunk-I25RINZK.js.map → chunk-KQT4DRKF.js.map} +0 -0
- /package/dist/{chunk-AXC7UTLM.js.map → chunk-MH2DXDN6.js.map} +0 -0
- /package/dist/{chunk-FRH2UC7Q.js.map → chunk-Q562GF7J.js.map} +0 -0
- /package/dist/{chunk-NHSVW7UT.js.map → chunk-QEPL7KIZ.js.map} +0 -0
- /package/dist/{chunk-5BY3IA2U.js.map → chunk-QGLEM46C.js.map} +0 -0
- /package/dist/{chunk-V32NHP2R.js.map → chunk-TL4PM7PC.js.map} +0 -0
- /package/dist/{chunk-52CQKST6.js.map → chunk-TRBLJU6Y.js.map} +0 -0
- /package/dist/{chunk-M47Q6566.js.map → chunk-VUEG3TZ4.js.map} +0 -0
- /package/dist/{chunk-DST7HRSG.js.map → chunk-YGPXV546.js.map} +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import type { AddressInfo } from "node:net";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
import { calibrationConsolidationInput, readCalibrationIndex, runCalibrationConsolidation } from "./calibration.js";
|
|
10
|
+
import { parseConfig } from "./config.js";
|
|
11
|
+
import { type LoggerBackend, initLogger, resetLogger } from "./logger.js";
|
|
12
|
+
import type { GatewayConfig } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gateway-less calibration (standalone daemon): no gateway model chain, so
|
|
16
|
+
* consolidation must call the configured local LLM and surface skips above
|
|
17
|
+
* debug. These tests drive the real LocalLlmClient against a loopback stub.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface CapturedRequest {
|
|
21
|
+
method: string;
|
|
22
|
+
path: string;
|
|
23
|
+
body: string;
|
|
24
|
+
headers: http.IncomingHttpHeaders;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface StubServer {
|
|
28
|
+
url: string;
|
|
29
|
+
requests: CapturedRequest[];
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function startServer(handle: (req: CapturedRequest, res: http.ServerResponse) => void): Promise<StubServer> {
|
|
34
|
+
const requests: CapturedRequest[] = [];
|
|
35
|
+
const server = http.createServer((req, res) => {
|
|
36
|
+
const chunks: Buffer[] = [];
|
|
37
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
38
|
+
req.on("end", () => {
|
|
39
|
+
const captured: CapturedRequest = {
|
|
40
|
+
method: req.method ?? "",
|
|
41
|
+
path: req.url ?? "",
|
|
42
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
43
|
+
headers: req.headers,
|
|
44
|
+
};
|
|
45
|
+
requests.push(captured);
|
|
46
|
+
handle(captured, res);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
server.listen(0, "127.0.0.1", () => {
|
|
51
|
+
const { port } = server.address() as AddressInfo;
|
|
52
|
+
resolve({
|
|
53
|
+
url: `http://127.0.0.1:${port}/v1`,
|
|
54
|
+
requests,
|
|
55
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function json(res: http.ServerResponse, body: unknown): void {
|
|
62
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
63
|
+
res.end(JSON.stringify(body));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ruleJson(condition: string): string {
|
|
67
|
+
return JSON.stringify({
|
|
68
|
+
rules: [
|
|
69
|
+
{
|
|
70
|
+
ruleType: "scope_boundary",
|
|
71
|
+
condition,
|
|
72
|
+
modelTendency: "It expands the task",
|
|
73
|
+
userExpectation: "Stay inside the request",
|
|
74
|
+
calibration: "Ask before widening the task.",
|
|
75
|
+
confidence: 0.8,
|
|
76
|
+
evidenceIds: ["correction-1", "correction-2"],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function chatCompletionBody(content: string): unknown {
|
|
83
|
+
return { choices: [{ message: { content } }] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function writeCorrections(memoryDir: string, count: number): Promise<void> {
|
|
87
|
+
const dir = path.join(memoryDir, "corrections");
|
|
88
|
+
await mkdir(dir, { recursive: true });
|
|
89
|
+
for (let i = 1; i <= count; i += 1) {
|
|
90
|
+
const body = [
|
|
91
|
+
"---",
|
|
92
|
+
`id: correction-${i}`,
|
|
93
|
+
"confidence: 0.9",
|
|
94
|
+
"---",
|
|
95
|
+
"",
|
|
96
|
+
`Correction ${i}: the model assumed a broader scope than the user stated for task ${i}.`,
|
|
97
|
+
"",
|
|
98
|
+
].join("\n");
|
|
99
|
+
await writeFile(path.join(dir, `correction-${i}.md`), body, "utf8");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function captureLogs(debug = true): { lines: Array<{ level: string; msg: string }> } {
|
|
104
|
+
const lines: Array<{ level: string; msg: string }> = [];
|
|
105
|
+
const backend: LoggerBackend = {
|
|
106
|
+
info(msg) {
|
|
107
|
+
lines.push({ level: "info", msg: String(msg) });
|
|
108
|
+
},
|
|
109
|
+
warn(msg) {
|
|
110
|
+
lines.push({ level: "warn", msg: String(msg) });
|
|
111
|
+
},
|
|
112
|
+
error(msg) {
|
|
113
|
+
lines.push({ level: "error", msg: String(msg) });
|
|
114
|
+
},
|
|
115
|
+
debug(msg) {
|
|
116
|
+
lines.push({ level: "debug", msg: String(msg) });
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
initLogger(backend, debug);
|
|
120
|
+
return { lines };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function gatewayConfig(baseUrl: string): GatewayConfig {
|
|
124
|
+
return {
|
|
125
|
+
agents: { defaults: { model: { primary: "stub/calibration-gateway" } } },
|
|
126
|
+
models: {
|
|
127
|
+
providers: {
|
|
128
|
+
stub: {
|
|
129
|
+
baseUrl,
|
|
130
|
+
api: "openai-completions",
|
|
131
|
+
apiKey: "gateway-test-key",
|
|
132
|
+
models: [],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
test.describe("gateway-less calibration", { concurrency: false }, () => {
|
|
140
|
+
test("plugin mode supplies localLlm settings and leaves the gateway chain unset", () => {
|
|
141
|
+
const config = parseConfig({
|
|
142
|
+
memoryDir: "/tmp/remnic-cal-plugin",
|
|
143
|
+
modelSource: "plugin",
|
|
144
|
+
localLlmEnabled: true,
|
|
145
|
+
localLlmModel: "daemon-calibration-model",
|
|
146
|
+
gatewayAgentId: "ignored-in-plugin-mode",
|
|
147
|
+
taskModelChain: { primary: "stub/ignored" },
|
|
148
|
+
});
|
|
149
|
+
const input = calibrationConsolidationInput(config);
|
|
150
|
+
assert.equal(input.pluginConfig, config);
|
|
151
|
+
assert.equal(input.pluginConfig.localLlmModel, "daemon-calibration-model");
|
|
152
|
+
assert.equal(input.pluginConfig.localLlmEnabled, true);
|
|
153
|
+
assert.equal(input.gatewayAgentId, undefined);
|
|
154
|
+
assert.equal(input.modelChain, undefined);
|
|
155
|
+
assert.equal(input.gatewayConfig, undefined);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("gateway mode keeps the agent and task chain on the calibration input", () => {
|
|
159
|
+
const config = parseConfig({
|
|
160
|
+
memoryDir: "/tmp/remnic-cal-gateway-input",
|
|
161
|
+
modelSource: "gateway",
|
|
162
|
+
gatewayAgentId: "agent-a",
|
|
163
|
+
taskModelChain: { primary: "stub/calibration-gateway" },
|
|
164
|
+
});
|
|
165
|
+
const input = calibrationConsolidationInput(config);
|
|
166
|
+
assert.equal(input.gatewayAgentId, "agent-a");
|
|
167
|
+
assert.deepEqual(input.modelChain, { primary: "stub/calibration-gateway" });
|
|
168
|
+
assert.equal(input.pluginConfig, config);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("uses the daemon localLlm* settings when no gateway model chain exists", async () => {
|
|
172
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-cal-local-"));
|
|
173
|
+
const logs = captureLogs();
|
|
174
|
+
const server = await startServer((req, res) => {
|
|
175
|
+
if (req.method === "GET" && req.path === "/") {
|
|
176
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
177
|
+
res.end("Ollama is running");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (req.method === "POST" && req.path === "/v1/chat/completions") {
|
|
181
|
+
json(res, chatCompletionBody(ruleJson("When the daemon has no gateway chain")));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.writeHead(404);
|
|
185
|
+
res.end();
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
await writeCorrections(memoryDir, 3);
|
|
189
|
+
const pluginConfig = parseConfig({
|
|
190
|
+
memoryDir,
|
|
191
|
+
modelSource: "plugin",
|
|
192
|
+
localLlmEnabled: true,
|
|
193
|
+
localLlmUrl: server.url,
|
|
194
|
+
localLlmModel: "daemon-calibration-model",
|
|
195
|
+
localLlmHeaders: { "X-Remnic-Local": "calibration" },
|
|
196
|
+
localLlmApiKey: "local-test-key",
|
|
197
|
+
localLlmAuthHeader: true,
|
|
198
|
+
});
|
|
199
|
+
const rules = await runCalibrationConsolidation({
|
|
200
|
+
memoryDir,
|
|
201
|
+
workspaceDir: memoryDir,
|
|
202
|
+
pluginConfig,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
assert.equal(rules.length, 1);
|
|
206
|
+
assert.equal(rules[0]?.condition, "When the daemon has no gateway chain");
|
|
207
|
+
const stored = await readCalibrationIndex(memoryDir);
|
|
208
|
+
assert.equal(stored.rules.length, 1);
|
|
209
|
+
assert.equal(stored.totalCorrectionsAnalyzed, 3);
|
|
210
|
+
|
|
211
|
+
const completion = server.requests.find((req) => req.method === "POST" && req.path === "/v1/chat/completions");
|
|
212
|
+
assert.ok(completion, "local LLM chat completion was not called");
|
|
213
|
+
const body = JSON.parse(completion.body) as { model?: string };
|
|
214
|
+
assert.equal(body.model, "daemon-calibration-model");
|
|
215
|
+
assert.equal(completion.headers["x-remnic-local"], "calibration");
|
|
216
|
+
assert.equal(completion.headers.authorization, "Bearer local-test-key");
|
|
217
|
+
assert.ok(
|
|
218
|
+
logs.lines.some((line) => line.level === "info" && line.msg.includes("model=daemon-calibration-model"))
|
|
219
|
+
);
|
|
220
|
+
assert.equal(
|
|
221
|
+
logs.lines.some((line) => line.msg.includes("no LLM available")),
|
|
222
|
+
false
|
|
223
|
+
);
|
|
224
|
+
} finally {
|
|
225
|
+
resetLogger();
|
|
226
|
+
await server.close();
|
|
227
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("warns and skips when neither a gateway chain nor a local LLM is configured", async () => {
|
|
232
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-cal-skip-"));
|
|
233
|
+
const logs = captureLogs(true);
|
|
234
|
+
try {
|
|
235
|
+
await writeCorrections(memoryDir, 3);
|
|
236
|
+
const pluginConfig = parseConfig({
|
|
237
|
+
memoryDir,
|
|
238
|
+
modelSource: "plugin",
|
|
239
|
+
localLlmEnabled: false,
|
|
240
|
+
});
|
|
241
|
+
const rules = await runCalibrationConsolidation({
|
|
242
|
+
memoryDir,
|
|
243
|
+
workspaceDir: memoryDir,
|
|
244
|
+
pluginConfig,
|
|
245
|
+
});
|
|
246
|
+
assert.deepEqual(rules, []);
|
|
247
|
+
const stored = await readCalibrationIndex(memoryDir);
|
|
248
|
+
assert.equal(stored.rules.length, 0);
|
|
249
|
+
assert.ok(logs.lines.some((line) => line.level === "warn" && line.msg.includes("no LLM available")));
|
|
250
|
+
assert.equal(
|
|
251
|
+
logs.lines.some((line) => line.level === "debug" && line.msg.includes("no LLM available")),
|
|
252
|
+
false
|
|
253
|
+
);
|
|
254
|
+
} finally {
|
|
255
|
+
resetLogger();
|
|
256
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("logs a short correction set at info", async () => {
|
|
261
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-cal-few-"));
|
|
262
|
+
const logs = captureLogs(true);
|
|
263
|
+
const server = await startServer((req, res) => {
|
|
264
|
+
if (req.method === "GET" && req.path === "/") {
|
|
265
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
266
|
+
res.end("Ollama is running");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
res.writeHead(500);
|
|
270
|
+
res.end();
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
await writeCorrections(memoryDir, 1);
|
|
274
|
+
const pluginConfig = parseConfig({
|
|
275
|
+
memoryDir,
|
|
276
|
+
modelSource: "plugin",
|
|
277
|
+
localLlmEnabled: true,
|
|
278
|
+
localLlmUrl: server.url,
|
|
279
|
+
localLlmModel: "daemon-calibration-model",
|
|
280
|
+
});
|
|
281
|
+
const rules = await runCalibrationConsolidation({
|
|
282
|
+
memoryDir,
|
|
283
|
+
workspaceDir: memoryDir,
|
|
284
|
+
pluginConfig,
|
|
285
|
+
});
|
|
286
|
+
assert.deepEqual(rules, []);
|
|
287
|
+
assert.equal(
|
|
288
|
+
server.requests.some((req) => req.method === "POST"),
|
|
289
|
+
false,
|
|
290
|
+
"synthesis must not call the LLM before the correction minimum"
|
|
291
|
+
);
|
|
292
|
+
assert.ok(logs.lines.some((line) => line.level === "info" && line.msg.includes("need at least 3")));
|
|
293
|
+
assert.equal(
|
|
294
|
+
logs.lines.some((line) => line.level === "debug" && line.msg.includes("need at least 3")),
|
|
295
|
+
false
|
|
296
|
+
);
|
|
297
|
+
} finally {
|
|
298
|
+
resetLogger();
|
|
299
|
+
await server.close();
|
|
300
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("keeps the gateway chain when one is configured", async () => {
|
|
305
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-cal-gateway-"));
|
|
306
|
+
const logs = captureLogs();
|
|
307
|
+
const local = await startServer((req, res) => {
|
|
308
|
+
if (req.method === "GET" && req.path === "/") {
|
|
309
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
310
|
+
res.end("Ollama is running");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (req.method === "POST") {
|
|
314
|
+
json(res, chatCompletionBody(ruleJson("When the local model was used by mistake")));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
res.writeHead(404);
|
|
318
|
+
res.end();
|
|
319
|
+
});
|
|
320
|
+
const gateway = await startServer((req, res) => {
|
|
321
|
+
if (req.method === "POST" && req.path === "/v1/chat/completions") {
|
|
322
|
+
json(res, chatCompletionBody(ruleJson("When the gateway chain is present")));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
res.writeHead(404);
|
|
326
|
+
res.end();
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
await writeCorrections(memoryDir, 3);
|
|
330
|
+
const pluginConfig = parseConfig({
|
|
331
|
+
memoryDir,
|
|
332
|
+
modelSource: "plugin",
|
|
333
|
+
localLlmEnabled: true,
|
|
334
|
+
localLlmUrl: local.url,
|
|
335
|
+
localLlmModel: "daemon-calibration-model",
|
|
336
|
+
});
|
|
337
|
+
const rules = await runCalibrationConsolidation({
|
|
338
|
+
memoryDir,
|
|
339
|
+
workspaceDir: memoryDir,
|
|
340
|
+
gatewayConfig: gatewayConfig(gateway.url),
|
|
341
|
+
pluginConfig,
|
|
342
|
+
});
|
|
343
|
+
assert.equal(rules.length, 1);
|
|
344
|
+
assert.equal(rules[0]?.condition, "When the gateway chain is present");
|
|
345
|
+
assert.equal(local.requests.length, 0);
|
|
346
|
+
assert.equal(
|
|
347
|
+
gateway.requests.filter((req) => req.method === "POST" && req.path === "/v1/chat/completions").length,
|
|
348
|
+
1
|
|
349
|
+
);
|
|
350
|
+
assert.equal(
|
|
351
|
+
logs.lines.some((line) => line.msg.includes("using configured local LLM")),
|
|
352
|
+
false
|
|
353
|
+
);
|
|
354
|
+
} finally {
|
|
355
|
+
resetLogger();
|
|
356
|
+
await local.close();
|
|
357
|
+
await gateway.close();
|
|
358
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
package/src/calibration.ts
CHANGED
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import path from "node:path";
|
|
17
16
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { resolveLocalLlmCapabilities } from "./capabilities.js";
|
|
19
|
+
import { OUTPUT_LANGUAGE_POLICY } from "./extraction-prompt.js";
|
|
18
20
|
import { FallbackLlmClient } from "./fallback-llm.js";
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import { isRecord } from "./store-contract.js";
|
|
21
|
+
import { listJsonFiles } from "./json-store.js";
|
|
22
|
+
import { LocalLlmClient } from "./local-llm.js";
|
|
22
23
|
import { log } from "./logger.js";
|
|
23
|
-
import {
|
|
24
|
+
import type { AgentPersonaModelConfig, GatewayConfig, PluginConfig } from "./types.js";
|
|
24
25
|
|
|
25
26
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
26
27
|
|
|
@@ -223,12 +224,57 @@ Output valid JSON only:
|
|
|
223
224
|
]
|
|
224
225
|
}`;
|
|
225
226
|
|
|
227
|
+
interface CalibrationChatClient {
|
|
228
|
+
chatCompletion(
|
|
229
|
+
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>,
|
|
230
|
+
options?: {
|
|
231
|
+
temperature?: number;
|
|
232
|
+
maxTokens?: number;
|
|
233
|
+
agentId?: string;
|
|
234
|
+
modelChain?: AgentPersonaModelConfig;
|
|
235
|
+
}
|
|
236
|
+
): Promise<{ content?: string | null } | null>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface CalibrationLlmOptions {
|
|
240
|
+
gatewayConfig?: GatewayConfig;
|
|
241
|
+
gatewayAgentId?: string;
|
|
242
|
+
modelChain?: AgentPersonaModelConfig;
|
|
243
|
+
workspaceDir?: string;
|
|
244
|
+
/** Plugin config. Supplies `localLlm*` settings when the gateway chain is empty. */
|
|
245
|
+
pluginConfig?: PluginConfig;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resolveCalibrationChatClient(options: CalibrationLlmOptions): CalibrationChatClient | null {
|
|
249
|
+
const gateway = new FallbackLlmClient(options.gatewayConfig, {
|
|
250
|
+
workspaceDir: options.workspaceDir,
|
|
251
|
+
});
|
|
252
|
+
if (gateway.isAvailable({ agentId: options.gatewayAgentId, modelChain: options.modelChain })) {
|
|
253
|
+
return gateway;
|
|
254
|
+
}
|
|
255
|
+
const pluginConfig = options.pluginConfig;
|
|
256
|
+
if (pluginConfig && resolveLocalLlmCapabilities(pluginConfig).localLlm) {
|
|
257
|
+
log.info(`[calibration] using configured local LLM (model=${pluginConfig.localLlmModel})`);
|
|
258
|
+
const local = new LocalLlmClient(pluginConfig);
|
|
259
|
+
return {
|
|
260
|
+
chatCompletion(messages, chatOptions) {
|
|
261
|
+
return local.chatCompletion(messages, {
|
|
262
|
+
temperature: chatOptions?.temperature,
|
|
263
|
+
maxTokens: chatOptions?.maxTokens,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
log.warn("[calibration] no LLM available — skipping consolidation");
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
226
272
|
export async function synthesizeCalibrationRules(
|
|
227
273
|
corrections: CorrectionMemory[],
|
|
228
|
-
llm:
|
|
274
|
+
llm: CalibrationChatClient,
|
|
229
275
|
existingRules: CalibrationRule[],
|
|
230
276
|
agentId?: string,
|
|
231
|
-
modelChain?: AgentPersonaModelConfig
|
|
277
|
+
modelChain?: AgentPersonaModelConfig
|
|
232
278
|
): Promise<CalibrationRule[]> {
|
|
233
279
|
if (corrections.length < 2) return [];
|
|
234
280
|
|
|
@@ -238,16 +284,20 @@ export async function synthesizeCalibrationRules(
|
|
|
238
284
|
.map((c, i) => `[${c.id}] ${c.content}`)
|
|
239
285
|
.join("\n\n");
|
|
240
286
|
|
|
241
|
-
const existingRulesText =
|
|
242
|
-
|
|
243
|
-
|
|
287
|
+
const existingRulesText =
|
|
288
|
+
existingRules.length > 0
|
|
289
|
+
? `\n\nExisting calibration rules (avoid duplicating these):\n${existingRules.map((r) => `- ${r.condition}: ${r.calibration}`).join("\n")}`
|
|
290
|
+
: "";
|
|
244
291
|
|
|
245
292
|
const response = await llm.chatCompletion(
|
|
246
293
|
[
|
|
247
294
|
{ role: "system", content: CLUSTER_PROMPT },
|
|
248
|
-
{
|
|
295
|
+
{
|
|
296
|
+
role: "user",
|
|
297
|
+
content: `Here are ${corrections.length} corrections from this user:\n\n${correctionText}${existingRulesText}`,
|
|
298
|
+
},
|
|
249
299
|
],
|
|
250
|
-
{ temperature: 0.3, maxTokens: 3000, agentId, modelChain }
|
|
300
|
+
{ temperature: 0.3, maxTokens: 3000, agentId, modelChain }
|
|
251
301
|
);
|
|
252
302
|
|
|
253
303
|
if (!response?.content) return [];
|
|
@@ -283,7 +333,10 @@ export async function synthesizeCalibrationRules(
|
|
|
283
333
|
}
|
|
284
334
|
const evidenceCorrectionIds = parseEvidenceIds(raw.evidenceIds);
|
|
285
335
|
return {
|
|
286
|
-
id: `cal-${createHash("sha256")
|
|
336
|
+
id: `cal-${createHash("sha256")
|
|
337
|
+
.update(condition + calibration)
|
|
338
|
+
.digest("hex")
|
|
339
|
+
.slice(0, 12)}`,
|
|
287
340
|
ruleType: ruleType as CalibrationRule["ruleType"],
|
|
288
341
|
condition,
|
|
289
342
|
modelTendency,
|
|
@@ -292,8 +345,8 @@ export async function synthesizeCalibrationRules(
|
|
|
292
345
|
confidence,
|
|
293
346
|
evidenceCount: evidenceCorrectionIds.length,
|
|
294
347
|
evidenceCorrectionIds,
|
|
295
|
-
|
|
296
|
-
|
|
348
|
+
createdAt: now,
|
|
349
|
+
lastReinforcedAt: now,
|
|
297
350
|
};
|
|
298
351
|
})
|
|
299
352
|
.filter((rule): rule is CalibrationRule => !!rule);
|
|
@@ -341,6 +394,30 @@ export function buildCalibrationRecallSection(
|
|
|
341
394
|
|
|
342
395
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
343
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Options weekly compounding passes into calibration. Gateway agent and task
|
|
399
|
+
* chain fields stay limited to `modelSource: "gateway"`. `pluginConfig` carries
|
|
400
|
+
* the `localLlm*` settings used when that chain is empty.
|
|
401
|
+
*/
|
|
402
|
+
export function calibrationConsolidationInput(config: PluginConfig): {
|
|
403
|
+
memoryDir: string;
|
|
404
|
+
gatewayConfig?: GatewayConfig;
|
|
405
|
+
gatewayAgentId?: string;
|
|
406
|
+
modelChain?: AgentPersonaModelConfig;
|
|
407
|
+
workspaceDir?: string;
|
|
408
|
+
pluginConfig: PluginConfig;
|
|
409
|
+
} {
|
|
410
|
+
const gateway = config.modelSource === "gateway";
|
|
411
|
+
return {
|
|
412
|
+
memoryDir: config.memoryDir,
|
|
413
|
+
gatewayConfig: config.gatewayConfig,
|
|
414
|
+
gatewayAgentId: gateway ? (config.gatewayAgentId || undefined) : undefined,
|
|
415
|
+
modelChain: gateway ? config.taskModelChain : undefined,
|
|
416
|
+
workspaceDir: config.workspaceDir,
|
|
417
|
+
pluginConfig: config,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
344
421
|
/**
|
|
345
422
|
* Run the full calibration pipeline:
|
|
346
423
|
* 1. Read all corrections
|
|
@@ -354,27 +431,29 @@ export async function runCalibrationConsolidation(options: {
|
|
|
354
431
|
gatewayAgentId?: string;
|
|
355
432
|
modelChain?: AgentPersonaModelConfig;
|
|
356
433
|
workspaceDir?: string;
|
|
434
|
+
pluginConfig?: PluginConfig;
|
|
357
435
|
}): Promise<CalibrationRule[]> {
|
|
358
436
|
try {
|
|
359
|
-
const llm =
|
|
360
|
-
|
|
361
|
-
});
|
|
362
|
-
if (!llm.isAvailable({ agentId: options.gatewayAgentId, modelChain: options.modelChain })) {
|
|
363
|
-
log.debug("[calibration] no LLM available — skipping consolidation");
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
437
|
+
const llm = resolveCalibrationChatClient(options);
|
|
438
|
+
if (!llm) return [];
|
|
366
439
|
|
|
367
440
|
const corrections = await readCorrections(options.memoryDir);
|
|
368
441
|
if (corrections.length < 3) {
|
|
369
|
-
log.
|
|
442
|
+
log.info(`[calibration] only ${corrections.length} corrections — need at least 3`);
|
|
370
443
|
return [];
|
|
371
444
|
}
|
|
372
445
|
|
|
373
446
|
const existingIndex = await readCalibrationIndex(options.memoryDir);
|
|
374
447
|
|
|
375
|
-
const newRules = await synthesizeCalibrationRules(
|
|
448
|
+
const newRules = await synthesizeCalibrationRules(
|
|
449
|
+
corrections,
|
|
450
|
+
llm,
|
|
451
|
+
existingIndex.rules,
|
|
452
|
+
options.gatewayAgentId,
|
|
453
|
+
options.modelChain
|
|
454
|
+
);
|
|
376
455
|
if (newRules.length === 0) {
|
|
377
|
-
log.
|
|
456
|
+
log.info("[calibration] no new calibration rules synthesized");
|
|
378
457
|
return existingIndex.rules;
|
|
379
458
|
}
|
|
380
459
|
|
|
@@ -402,7 +481,9 @@ export async function runCalibrationConsolidation(options: {
|
|
|
402
481
|
log.debug(`[calibration] synthesized ${newRules.length} new rule(s), ${allRules.length} total`);
|
|
403
482
|
return allRules;
|
|
404
483
|
} catch (error) {
|
|
405
|
-
log.warn(
|
|
484
|
+
log.warn(
|
|
485
|
+
`[calibration] consolidation failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`
|
|
486
|
+
);
|
|
406
487
|
return [];
|
|
407
488
|
}
|
|
408
489
|
}
|
|
@@ -421,6 +502,7 @@ export async function runCalibrationIfEnabled(options: {
|
|
|
421
502
|
gatewayAgentId?: string;
|
|
422
503
|
modelChain?: AgentPersonaModelConfig;
|
|
423
504
|
workspaceDir?: string;
|
|
505
|
+
pluginConfig?: PluginConfig;
|
|
424
506
|
}): Promise<CalibrationRule[]> {
|
|
425
507
|
if (!options.calibrationEnabled) {
|
|
426
508
|
return [];
|
|
@@ -431,6 +513,7 @@ export async function runCalibrationIfEnabled(options: {
|
|
|
431
513
|
gatewayAgentId: options.gatewayAgentId,
|
|
432
514
|
modelChain: options.modelChain,
|
|
433
515
|
workspaceDir: options.workspaceDir,
|
|
516
|
+
pluginConfig: options.pluginConfig,
|
|
434
517
|
});
|
|
435
518
|
}
|
|
436
519
|
|
|
@@ -552,14 +552,8 @@ export class CompoundingEngine {
|
|
|
552
552
|
// PEDC: Run calibration consolidation during weekly synthesis
|
|
553
553
|
if (resolveConsolidationCapabilities(this.config).calibration) {
|
|
554
554
|
try {
|
|
555
|
-
const { runCalibrationConsolidation } = await import("../calibration.js");
|
|
556
|
-
const calRules = await runCalibrationConsolidation(
|
|
557
|
-
memoryDir: this.config.memoryDir,
|
|
558
|
-
gatewayConfig: this.config.gatewayConfig,
|
|
559
|
-
gatewayAgentId: this.config.modelSource === "gateway" ? (this.config.gatewayAgentId || undefined) : undefined,
|
|
560
|
-
modelChain: this.config.modelSource === "gateway" ? this.config.taskModelChain : undefined,
|
|
561
|
-
workspaceDir: this.config.workspaceDir,
|
|
562
|
-
});
|
|
555
|
+
const { calibrationConsolidationInput, runCalibrationConsolidation } = await import("../calibration.js");
|
|
556
|
+
const calRules = await runCalibrationConsolidation(calibrationConsolidationInput(this.config));
|
|
563
557
|
log.debug(`[calibration] weekly synthesis produced ${calRules.length} calibration rule(s)`);
|
|
564
558
|
} catch (error) {
|
|
565
559
|
log.warn(`[calibration] weekly consolidation failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|