@mrdrmccoy/xerj-opencode-plugin 0.0.0-stage → 0.4.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/LICENSE +201 -0
- package/README.md +284 -2
- package/package.json +47 -4
- package/src/binary/detect.ts +55 -0
- package/src/binary/manager.ts +348 -0
- package/src/binary/release.ts +64 -0
- package/src/config.ts +381 -0
- package/src/hooks/session.ts +165 -0
- package/src/index/brain.ts +225 -0
- package/src/index/corpus.ts +164 -0
- package/src/index/hidden.ts +318 -0
- package/src/index/watcher.ts +333 -0
- package/src/index.ts +723 -0
- package/src/log.ts +88 -0
- package/src/memory/capture.ts +72 -0
- package/src/memory/store.ts +116 -0
- package/src/node/client.ts +361 -0
- package/src/node/supervisor.ts +267 -0
- package/src/paths.ts +202 -0
- package/src/platform.ts +88 -0
- package/src/remote/shell.ts +338 -0
- package/src/remote/spool.ts +217 -0
- package/src/rpc.ts +250 -0
- package/src/search/brains.ts +306 -0
- package/src/search/rank.ts +61 -0
- package/src/search/service.ts +589 -0
- package/src/semver.ts +117 -0
- package/src/stats/counters.ts +55 -0
- package/src/stats/ledger.ts +344 -0
- package/src/telemetry/anomaly.ts +211 -0
- package/src/tools/search.ts +384 -0
- package/src/tui.tsx +362 -0
- package/src/ui/format.ts +234 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import type { Libc } from "./platform.ts";
|
|
2
|
+
|
|
3
|
+
// Plugin options. Every field has a default, unknown keys are ignored, and a
|
|
4
|
+
// malformed value falls back to the default with a warning instead of failing
|
|
5
|
+
// plugin load. A search plugin that refuses to start because one option is a
|
|
6
|
+
// string instead of a number is worse than one that starts with defaults.
|
|
7
|
+
|
|
8
|
+
export interface BinaryOptions {
|
|
9
|
+
/** "auto" tracks the latest release; anything else pins a version. */
|
|
10
|
+
readonly version: string;
|
|
11
|
+
/** Explicit binary path; skips download and upgrade entirely. */
|
|
12
|
+
readonly path: string | null;
|
|
13
|
+
/** Explicit release triple; overrides host detection. Mostly for CI. */
|
|
14
|
+
readonly target: string | null;
|
|
15
|
+
/** Linux C library flavour of the downloaded build. */
|
|
16
|
+
readonly libc: Libc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NodeOptions {
|
|
20
|
+
/** Attach to an existing node instead of spawning one. */
|
|
21
|
+
readonly url: string | null;
|
|
22
|
+
/** Preferred ES-compatible port; the supervisor may advance it. */
|
|
23
|
+
readonly port: number;
|
|
24
|
+
readonly dataDir: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IndexOptions {
|
|
28
|
+
readonly auto: boolean;
|
|
29
|
+
/** Keep a resident `autoindex --watch` process instead of polling. */
|
|
30
|
+
readonly watch: boolean;
|
|
31
|
+
readonly debounceMs: number;
|
|
32
|
+
readonly periodicMs: number;
|
|
33
|
+
readonly workers: number | null;
|
|
34
|
+
readonly brain: ReadonlyArray<string>;
|
|
35
|
+
/** Hidden folders indexed as plain documents, e.g. `.agents`. */
|
|
36
|
+
readonly hidden: ReadonlyArray<string>;
|
|
37
|
+
readonly hiddenMaxBytes: number;
|
|
38
|
+
readonly semantic: boolean;
|
|
39
|
+
readonly maxBytes: number;
|
|
40
|
+
/** Upper bound on the measured first-index estimate before asking. */
|
|
41
|
+
readonly maxMinutes: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RemoteOptions {
|
|
45
|
+
readonly enabled: boolean;
|
|
46
|
+
readonly maxBytes: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ContextOptions {
|
|
50
|
+
readonly enabled: boolean;
|
|
51
|
+
readonly maxTokens: number;
|
|
52
|
+
readonly memoryTokens: number;
|
|
53
|
+
readonly maxPassages: number;
|
|
54
|
+
readonly timeoutMs: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MemoryOptions {
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface StatsOptions {
|
|
62
|
+
readonly enabled: boolean;
|
|
63
|
+
readonly bytesPerToken: number;
|
|
64
|
+
/** Baseline credited per cited file; a lockfile should not count as 700 KB. */
|
|
65
|
+
readonly maxBaselineBytes: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TelemetryOptions {
|
|
69
|
+
readonly anomaly: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Config {
|
|
73
|
+
readonly binary: BinaryOptions;
|
|
74
|
+
readonly node: NodeOptions;
|
|
75
|
+
readonly index: IndexOptions;
|
|
76
|
+
readonly remote: RemoteOptions;
|
|
77
|
+
readonly context: ContextOptions;
|
|
78
|
+
readonly memory: MemoryOptions;
|
|
79
|
+
readonly stats: StatsOptions;
|
|
80
|
+
readonly telemetry: TelemetryOptions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface LoadedConfig {
|
|
84
|
+
readonly config: Config;
|
|
85
|
+
readonly warnings: ReadonlyArray<string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const DEFAULT_CONFIG: Config = {
|
|
89
|
+
binary: { version: "auto", path: null, target: null, libc: "musl" },
|
|
90
|
+
node: { url: null, port: 9200, dataDir: null },
|
|
91
|
+
index: {
|
|
92
|
+
auto: true,
|
|
93
|
+
watch: true,
|
|
94
|
+
debounceMs: 800,
|
|
95
|
+
periodicMs: 900_000,
|
|
96
|
+
workers: null,
|
|
97
|
+
brain: [],
|
|
98
|
+
hidden: [],
|
|
99
|
+
hiddenMaxBytes: 256 * 1024,
|
|
100
|
+
semantic: true,
|
|
101
|
+
maxBytes: 2 * 1024 ** 3,
|
|
102
|
+
maxMinutes: 10,
|
|
103
|
+
},
|
|
104
|
+
remote: { enabled: true, maxBytes: 2_000_000 },
|
|
105
|
+
context: {
|
|
106
|
+
enabled: true,
|
|
107
|
+
maxTokens: 1500,
|
|
108
|
+
memoryTokens: 400,
|
|
109
|
+
maxPassages: 4,
|
|
110
|
+
timeoutMs: 400,
|
|
111
|
+
},
|
|
112
|
+
memory: { enabled: true },
|
|
113
|
+
stats: { enabled: true, bytesPerToken: 4, maxBaselineBytes: 128 * 1024 },
|
|
114
|
+
telemetry: { anomaly: false },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
type Raw = Record<string, unknown>;
|
|
118
|
+
|
|
119
|
+
export function loadConfig(
|
|
120
|
+
raw: unknown,
|
|
121
|
+
env: Readonly<Record<string, string | undefined>> = {},
|
|
122
|
+
): LoadedConfig {
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
const root = asObject(raw);
|
|
125
|
+
|
|
126
|
+
const binary = asObject(root.binary);
|
|
127
|
+
const node = asObject(root.node);
|
|
128
|
+
const index = asObject(root.index);
|
|
129
|
+
const remote = asObject(root.remote);
|
|
130
|
+
const context = asObject(root.context);
|
|
131
|
+
const memory = asObject(root.memory);
|
|
132
|
+
const stats = asObject(root.stats);
|
|
133
|
+
const telemetry = asObject(root.telemetry);
|
|
134
|
+
|
|
135
|
+
const envUrl = env.XERJ_URL?.trim();
|
|
136
|
+
|
|
137
|
+
const config: Config = {
|
|
138
|
+
binary: {
|
|
139
|
+
version: readVersion(binary.version, warnings),
|
|
140
|
+
path: readString(binary.path, null),
|
|
141
|
+
target: readTarget(binary.target, warnings),
|
|
142
|
+
libc: readLibc(binary.libc, warnings),
|
|
143
|
+
},
|
|
144
|
+
node: {
|
|
145
|
+
url: readString(node.url, null) ?? (envUrl ? envUrl : null),
|
|
146
|
+
port: readInteger(node.port, DEFAULT_CONFIG.node.port, 1, 65_535, "node.port", warnings),
|
|
147
|
+
dataDir: readString(node.dataDir, null),
|
|
148
|
+
},
|
|
149
|
+
index: {
|
|
150
|
+
auto: readBoolean(index.auto, DEFAULT_CONFIG.index.auto, "index.auto", warnings),
|
|
151
|
+
watch: readBoolean(index.watch, DEFAULT_CONFIG.index.watch, "index.watch", warnings),
|
|
152
|
+
debounceMs: readInteger(
|
|
153
|
+
index.debounceMs,
|
|
154
|
+
DEFAULT_CONFIG.index.debounceMs,
|
|
155
|
+
200,
|
|
156
|
+
60_000,
|
|
157
|
+
"index.debounceMs",
|
|
158
|
+
warnings,
|
|
159
|
+
),
|
|
160
|
+
periodicMs: readInteger(
|
|
161
|
+
index.periodicMs,
|
|
162
|
+
DEFAULT_CONFIG.index.periodicMs,
|
|
163
|
+
10_000,
|
|
164
|
+
86_400_000,
|
|
165
|
+
"index.periodicMs",
|
|
166
|
+
warnings,
|
|
167
|
+
),
|
|
168
|
+
workers: readOptionalInteger(index.workers, 1, 1024, "index.workers", warnings),
|
|
169
|
+
brain: readStringArray(index.brain, "index.brain", warnings),
|
|
170
|
+
hidden: readStringArray(index.hidden, "index.hidden", warnings),
|
|
171
|
+
hiddenMaxBytes: readInteger(
|
|
172
|
+
index.hiddenMaxBytes,
|
|
173
|
+
DEFAULT_CONFIG.index.hiddenMaxBytes,
|
|
174
|
+
1_024,
|
|
175
|
+
Number.MAX_SAFE_INTEGER,
|
|
176
|
+
"index.hiddenMaxBytes",
|
|
177
|
+
warnings,
|
|
178
|
+
),
|
|
179
|
+
semantic: readBoolean(
|
|
180
|
+
index.semantic,
|
|
181
|
+
DEFAULT_CONFIG.index.semantic,
|
|
182
|
+
"index.semantic",
|
|
183
|
+
warnings,
|
|
184
|
+
),
|
|
185
|
+
maxBytes: readInteger(
|
|
186
|
+
index.maxBytes,
|
|
187
|
+
DEFAULT_CONFIG.index.maxBytes,
|
|
188
|
+
0,
|
|
189
|
+
Number.MAX_SAFE_INTEGER,
|
|
190
|
+
"index.maxBytes",
|
|
191
|
+
warnings,
|
|
192
|
+
),
|
|
193
|
+
maxMinutes: readInteger(
|
|
194
|
+
index.maxMinutes,
|
|
195
|
+
DEFAULT_CONFIG.index.maxMinutes,
|
|
196
|
+
1,
|
|
197
|
+
10_080,
|
|
198
|
+
"index.maxMinutes",
|
|
199
|
+
warnings,
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
remote: {
|
|
203
|
+
enabled: readBoolean(remote.enabled, DEFAULT_CONFIG.remote.enabled, "remote.enabled", warnings),
|
|
204
|
+
maxBytes: readInteger(
|
|
205
|
+
remote.maxBytes,
|
|
206
|
+
DEFAULT_CONFIG.remote.maxBytes,
|
|
207
|
+
0,
|
|
208
|
+
Number.MAX_SAFE_INTEGER,
|
|
209
|
+
"remote.maxBytes",
|
|
210
|
+
warnings,
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
context: {
|
|
214
|
+
enabled: readBoolean(context.enabled, DEFAULT_CONFIG.context.enabled, "context.enabled", warnings),
|
|
215
|
+
maxTokens: readInteger(
|
|
216
|
+
context.maxTokens,
|
|
217
|
+
DEFAULT_CONFIG.context.maxTokens,
|
|
218
|
+
100,
|
|
219
|
+
100_000,
|
|
220
|
+
"context.maxTokens",
|
|
221
|
+
warnings,
|
|
222
|
+
),
|
|
223
|
+
memoryTokens: readInteger(
|
|
224
|
+
context.memoryTokens,
|
|
225
|
+
DEFAULT_CONFIG.context.memoryTokens,
|
|
226
|
+
0,
|
|
227
|
+
100_000,
|
|
228
|
+
"context.memoryTokens",
|
|
229
|
+
warnings,
|
|
230
|
+
),
|
|
231
|
+
maxPassages: readInteger(
|
|
232
|
+
context.maxPassages,
|
|
233
|
+
DEFAULT_CONFIG.context.maxPassages,
|
|
234
|
+
1,
|
|
235
|
+
20,
|
|
236
|
+
"context.maxPassages",
|
|
237
|
+
warnings,
|
|
238
|
+
),
|
|
239
|
+
timeoutMs: readInteger(
|
|
240
|
+
context.timeoutMs,
|
|
241
|
+
DEFAULT_CONFIG.context.timeoutMs,
|
|
242
|
+
50,
|
|
243
|
+
30_000,
|
|
244
|
+
"context.timeoutMs",
|
|
245
|
+
warnings,
|
|
246
|
+
),
|
|
247
|
+
},
|
|
248
|
+
memory: {
|
|
249
|
+
enabled: readBoolean(memory.enabled, DEFAULT_CONFIG.memory.enabled, "memory.enabled", warnings),
|
|
250
|
+
},
|
|
251
|
+
stats: {
|
|
252
|
+
enabled: readBoolean(stats.enabled, DEFAULT_CONFIG.stats.enabled, "stats.enabled", warnings),
|
|
253
|
+
bytesPerToken: readInteger(
|
|
254
|
+
stats.bytesPerToken,
|
|
255
|
+
DEFAULT_CONFIG.stats.bytesPerToken,
|
|
256
|
+
1,
|
|
257
|
+
100,
|
|
258
|
+
"stats.bytesPerToken",
|
|
259
|
+
warnings,
|
|
260
|
+
),
|
|
261
|
+
maxBaselineBytes: readInteger(
|
|
262
|
+
stats.maxBaselineBytes,
|
|
263
|
+
DEFAULT_CONFIG.stats.maxBaselineBytes,
|
|
264
|
+
0,
|
|
265
|
+
Number.MAX_SAFE_INTEGER,
|
|
266
|
+
"stats.maxBaselineBytes",
|
|
267
|
+
warnings,
|
|
268
|
+
),
|
|
269
|
+
},
|
|
270
|
+
telemetry: {
|
|
271
|
+
anomaly: readBoolean(
|
|
272
|
+
telemetry.anomaly,
|
|
273
|
+
DEFAULT_CONFIG.telemetry.anomaly,
|
|
274
|
+
"telemetry.anomaly",
|
|
275
|
+
warnings,
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return { config, warnings };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function asObject(value: unknown): Raw {
|
|
284
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
285
|
+
return value as Raw;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readVersion(value: unknown, warnings: string[]): string {
|
|
289
|
+
if (value === undefined) return DEFAULT_CONFIG.binary.version;
|
|
290
|
+
if (typeof value !== "string") {
|
|
291
|
+
warnings.push("binary.version must be a string; using auto");
|
|
292
|
+
return DEFAULT_CONFIG.binary.version;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const trimmed = value.trim();
|
|
296
|
+
if (trimmed === "" || trimmed === "auto") return "auto";
|
|
297
|
+
if (!/^v?\d+\.\d+\.\d+/.test(trimmed)) {
|
|
298
|
+
warnings.push(`binary.version "${trimmed}" is not a version; using auto`);
|
|
299
|
+
return "auto";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return trimmed.replace(/^v/, "");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A release triple, e.g. `aarch64-apple-darwin`. Invalid values fall back to
|
|
306
|
+
* host detection rather than failing plugin load. */
|
|
307
|
+
function readTarget(value: unknown, warnings: string[]): string | null {
|
|
308
|
+
const target = readString(value, null);
|
|
309
|
+
if (target === null) return null;
|
|
310
|
+
if (!/^(x86_64|aarch64)-[a-z0-9-]+$/.test(target)) {
|
|
311
|
+
warnings.push(`binary.target "${target}" is not a release triple; using host detection`);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return target;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readLibc(value: unknown, warnings: string[]): Libc {
|
|
318
|
+
if (value === undefined) return DEFAULT_CONFIG.binary.libc;
|
|
319
|
+
if (value === "musl" || value === "gnu") return value;
|
|
320
|
+
warnings.push(`binary.libc must be "musl" or "gnu"; using ${DEFAULT_CONFIG.binary.libc}`);
|
|
321
|
+
return DEFAULT_CONFIG.binary.libc;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function readBoolean(value: unknown, fallback: boolean, key: string, warnings: string[]): boolean {
|
|
325
|
+
if (value === undefined) return fallback;
|
|
326
|
+
if (typeof value === "boolean") return value;
|
|
327
|
+
warnings.push(`${key} must be a boolean; using ${fallback}`);
|
|
328
|
+
return fallback;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function readString(value: unknown, fallback: string | null): string | null {
|
|
332
|
+
if (typeof value !== "string") return fallback;
|
|
333
|
+
const trimmed = value.trim();
|
|
334
|
+
return trimmed === "" ? fallback : trimmed;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function readInteger(
|
|
338
|
+
value: unknown,
|
|
339
|
+
fallback: number,
|
|
340
|
+
min: number,
|
|
341
|
+
max: number,
|
|
342
|
+
key: string,
|
|
343
|
+
warnings: string[],
|
|
344
|
+
): number {
|
|
345
|
+
if (value === undefined) return fallback;
|
|
346
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
|
|
347
|
+
warnings.push(`${key} must be an integer; using ${fallback}`);
|
|
348
|
+
return fallback;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (value < min || value > max) {
|
|
352
|
+
const clamped = Math.min(max, Math.max(min, value));
|
|
353
|
+
warnings.push(`${key} must be between ${min} and ${max}; using ${clamped}`);
|
|
354
|
+
return clamped;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readOptionalInteger(
|
|
361
|
+
value: unknown,
|
|
362
|
+
min: number,
|
|
363
|
+
max: number,
|
|
364
|
+
key: string,
|
|
365
|
+
warnings: string[],
|
|
366
|
+
): number | null {
|
|
367
|
+
if (value === undefined || value === null) return null;
|
|
368
|
+
return readInteger(value, min, min, max, key, warnings);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readStringArray(value: unknown, key: string, warnings: string[]): ReadonlyArray<string> {
|
|
372
|
+
if (value === undefined) return [];
|
|
373
|
+
if (!Array.isArray(value)) {
|
|
374
|
+
warnings.push(`${key} must be an array of strings; using []`);
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const entries = value.filter((entry): entry is string => typeof entry === "string");
|
|
379
|
+
if (entries.length !== value.length) warnings.push(`${key} contains non-string entries; ignoring them`);
|
|
380
|
+
return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
381
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Prompt-time retrieval and context injection.
|
|
2
|
+
//
|
|
3
|
+
// The prompt hook runs at admission and does the retrieval once, with a hard
|
|
4
|
+
// deadline. The context hook runs before every model call; it injects that one
|
|
5
|
+
// prepared block exactly once and forgets it, so tool-driven continuations do
|
|
6
|
+
// not repeat the same passages into the window.
|
|
7
|
+
|
|
8
|
+
import type { Plugin } from "@opencode/plugin";
|
|
9
|
+
import type { Config } from "../config.ts";
|
|
10
|
+
import type { Logger } from "../log.ts";
|
|
11
|
+
import type { MemoryStore } from "../memory/store.ts";
|
|
12
|
+
import { formatMemories } from "../memory/store.ts";
|
|
13
|
+
import type { SearchService } from "../search/service.ts";
|
|
14
|
+
|
|
15
|
+
export interface SessionHookOptions {
|
|
16
|
+
readonly config: Config;
|
|
17
|
+
readonly logger: Logger;
|
|
18
|
+
readonly getSearch: () => SearchService | null;
|
|
19
|
+
readonly getMemory: () => MemoryStore | null;
|
|
20
|
+
readonly summarize: () => Promise<string>;
|
|
21
|
+
readonly onInject?: () => void;
|
|
22
|
+
readonly recordQuery: (
|
|
23
|
+
sessionID: string,
|
|
24
|
+
paths: ReadonlyArray<string>,
|
|
25
|
+
bytes: number,
|
|
26
|
+
hits: number,
|
|
27
|
+
) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Prepared {
|
|
31
|
+
readonly messageID: string;
|
|
32
|
+
readonly block: string;
|
|
33
|
+
readonly memory: string;
|
|
34
|
+
readonly hits: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MIN_QUERY_LENGTH = 12;
|
|
38
|
+
|
|
39
|
+
/** Slash commands and one-word replies are not worth a retrieval pass. */
|
|
40
|
+
export function shouldRetrieve(text: string): boolean {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
if (trimmed.startsWith("/")) return false;
|
|
43
|
+
return trimmed.length >= MIN_QUERY_LENGTH;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatContextBlock(text: string): string {
|
|
47
|
+
return [
|
|
48
|
+
"<xerj-context>",
|
|
49
|
+
"Retrieved from the local XERJ index. Cite path:line when you use it.",
|
|
50
|
+
"",
|
|
51
|
+
text,
|
|
52
|
+
"</xerj-context>",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatCorpusBlock(summary: string): string {
|
|
57
|
+
return `<xerj-corpus>\n${summary}\n</xerj-corpus>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function registerSessionHooks(
|
|
61
|
+
ctx: Plugin.Context,
|
|
62
|
+
options: SessionHookOptions,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
if (!options.config.context.enabled) return;
|
|
65
|
+
|
|
66
|
+
const prepared = new Map<string, Prepared>();
|
|
67
|
+
const injected = new Set<string>();
|
|
68
|
+
const injectedMemories = new Set<string>();
|
|
69
|
+
const summarized = new Set<string>();
|
|
70
|
+
|
|
71
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
72
|
+
const text = event.prompt.text.trim();
|
|
73
|
+
if (!shouldRetrieve(text)) return;
|
|
74
|
+
|
|
75
|
+
const search = options.getSearch();
|
|
76
|
+
if (!search) return;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await retrieve(search, text, options);
|
|
80
|
+
const paths = result.hits.flatMap((hit) => (hit.path ? [hit.path] : []));
|
|
81
|
+
await options.recordQuery(event.sessionID, paths, result.bytes, result.hits.length);
|
|
82
|
+
|
|
83
|
+
const memory = await recallMemories(text, options, injectedMemories);
|
|
84
|
+
|
|
85
|
+
if (result.hits.length === 0 && memory.length === 0) return;
|
|
86
|
+
prepared.set(event.sessionID, {
|
|
87
|
+
messageID: event.messageID,
|
|
88
|
+
block: result.text.trim().length === 0 ? "" : formatContextBlock(result.text),
|
|
89
|
+
memory,
|
|
90
|
+
hits: result.hits.length,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
options.logger.debug("prompt retrieval skipped", String(error));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await ctx.session.hook("context", async (event) => {
|
|
98
|
+
if (!summarized.has(event.sessionID)) {
|
|
99
|
+
summarized.add(event.sessionID);
|
|
100
|
+
try {
|
|
101
|
+
const summary = await options.summarize();
|
|
102
|
+
if (summary.length > 0) {
|
|
103
|
+
event.system.push({ type: "text", text: formatCorpusBlock(summary) });
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
options.logger.debug("could not summarize the corpus", String(error));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const entry = prepared.get(event.sessionID);
|
|
111
|
+
if (!entry) return;
|
|
112
|
+
|
|
113
|
+
const key = `${event.sessionID}:${entry.messageID}`;
|
|
114
|
+
if (injected.has(key)) return;
|
|
115
|
+
injected.add(key);
|
|
116
|
+
prepared.delete(event.sessionID);
|
|
117
|
+
|
|
118
|
+
if (entry.memory.length > 0) {
|
|
119
|
+
event.system.push({ type: "text", text: entry.memory });
|
|
120
|
+
}
|
|
121
|
+
if (entry.block.length > 0) {
|
|
122
|
+
event.system.push({ type: "text", text: entry.block });
|
|
123
|
+
}
|
|
124
|
+
if (entry.memory.length > 0 || entry.block.length > 0) {
|
|
125
|
+
options.onInject?.();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function recallMemories(
|
|
131
|
+
text: string,
|
|
132
|
+
options: SessionHookOptions,
|
|
133
|
+
injected: Set<string>,
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const memory = options.getMemory();
|
|
136
|
+
if (!memory) return "";
|
|
137
|
+
|
|
138
|
+
const hits = (await memory.recall(text, 3)).filter((hit) => !injected.has(hit.id));
|
|
139
|
+
if (hits.length === 0) return "";
|
|
140
|
+
|
|
141
|
+
const block = formatMemories(hits, options.config.context.memoryTokens * 4);
|
|
142
|
+
if (block.length === 0) return "";
|
|
143
|
+
|
|
144
|
+
for (const hit of hits) injected.add(hit.id);
|
|
145
|
+
return block;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function retrieve(
|
|
149
|
+
search: SearchService,
|
|
150
|
+
text: string,
|
|
151
|
+
options: SessionHookOptions,
|
|
152
|
+
): Promise<Awaited<ReturnType<SearchService["search"]>>> {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timer = setTimeout(() => controller.abort(), options.config.context.timeoutMs);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
return await search.search(text, {
|
|
158
|
+
k: options.config.context.maxPassages,
|
|
159
|
+
maxTokens: options.config.context.maxTokens,
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
}
|