@fonlan/dsh-gitmemo 0.4.0 → 0.5.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 +157 -4
- package/README.zh.md +83 -3
- package/lib/client.js +721 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +266 -15
- package/lib/index.js +377 -10
- package/lib/mem.d.ts +37 -1
- package/lib/mem.js +74 -10
- package/lib/systemone.d.ts +250 -0
- package/lib/systemone.js +359 -0
- package/package.json +37 -5
package/lib/mem.js
CHANGED
|
@@ -34,9 +34,20 @@ export const LOCK_FILE_NAME = ".mem.gitmemo.lock";
|
|
|
34
34
|
export const MIGRATION_JOURNAL_NAME = ".mem.gitmemo-migration.json";
|
|
35
35
|
/** Max entry content size. */
|
|
36
36
|
export const MAX_CONTENT_BYTES = 1024 * 1024;
|
|
37
|
+
/** Upper bound on stored entry keywords (mem_write / mem_replace). */
|
|
38
|
+
export const MAX_WRITE_KEYWORDS = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Upper bound on query keywords for one mem_search call. Deliberately higher
|
|
41
|
+
* than the write bound: recall is a fixed-string OR over the query terms, so a
|
|
42
|
+
* wider (still human-curated) query only widens recall, while every matched
|
|
43
|
+
* term still contributes to the score and the write side keeps entries tight.
|
|
44
|
+
*/
|
|
45
|
+
export const MAX_SEARCH_KEYWORDS = 15;
|
|
37
46
|
/** Error type thrown by every gitmemo operation. */
|
|
38
47
|
export class GitMemoError extends Error {
|
|
39
48
|
}
|
|
49
|
+
/** Accepted `searchScoring` values, for config validation and tool schemas. */
|
|
50
|
+
export const SEARCH_SCORING_MODES = ["count", "weighted"];
|
|
40
51
|
const UNSAFE_CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/;
|
|
41
52
|
const TRAILER_INJECT_RE = /^\s*GitMemo-[A-Za-z0-9-]+:/im;
|
|
42
53
|
const COMMIT_RE = /^[0-9a-f]{40}$/;
|
|
@@ -256,8 +267,8 @@ export function validateWriteInput(input, legacy = false) {
|
|
|
256
267
|
throw new GitMemoError("gitmemo: keywords must be an array");
|
|
257
268
|
const rawKeywords = input.keywords.map((k) => (typeof k === "string" ? k.trim() : "")).filter((k) => k.length > 0);
|
|
258
269
|
const minKeywords = legacy ? 0 : 2;
|
|
259
|
-
if (rawKeywords.length < minKeywords || rawKeywords.length >
|
|
260
|
-
throw new GitMemoError(`gitmemo: keywords must contain ${minKeywords === 0 ? "0
|
|
270
|
+
if (rawKeywords.length < minKeywords || rawKeywords.length > MAX_WRITE_KEYWORDS) {
|
|
271
|
+
throw new GitMemoError(`gitmemo: keywords must contain ${minKeywords === 0 ? "0" : "2"}-${MAX_WRITE_KEYWORDS} entries, got ${rawKeywords.length}`);
|
|
261
272
|
}
|
|
262
273
|
const keywords = [];
|
|
263
274
|
const seen = new Set();
|
|
@@ -271,7 +282,7 @@ export function validateWriteInput(input, legacy = false) {
|
|
|
271
282
|
keywords.push(kw);
|
|
272
283
|
}
|
|
273
284
|
if (keywords.length < minKeywords) {
|
|
274
|
-
throw new GitMemoError(`gitmemo: keywords must contain ${minKeywords === 0 ? "0
|
|
285
|
+
throw new GitMemoError(`gitmemo: keywords must contain ${minKeywords === 0 ? "0" : "2"}-${MAX_WRITE_KEYWORDS} distinct entries after normalization, got ${keywords.length}`);
|
|
275
286
|
}
|
|
276
287
|
let related_branches = [];
|
|
277
288
|
if (input.related_branches !== undefined) {
|
|
@@ -495,6 +506,23 @@ export function recencyBonus(committerTime, nowMs = Date.now()) {
|
|
|
495
506
|
return 0;
|
|
496
507
|
return Math.round(((0.5 * (180 - ageDays)) / 150) * 100) / 100;
|
|
497
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Rarity weight of a keyword matched by `df` of `n` candidates, in the
|
|
511
|
+
* BM25-style form already used across this project. A term matched by every
|
|
512
|
+
* candidate is worth almost nothing; a term matched by one candidate out of
|
|
513
|
+
* many is worth the most.
|
|
514
|
+
*
|
|
515
|
+
* Used by `searchScoring: "weighted"` so a distinctive match can outrank a
|
|
516
|
+
* pile of common ones. The weight is computed over the *recall page*, which
|
|
517
|
+
* costs no extra git call (the candidate commit messages are already in hand)
|
|
518
|
+
* and is the pool the ordering actually has to get right.
|
|
519
|
+
*/
|
|
520
|
+
export function keywordRarity(df, n) {
|
|
521
|
+
if (n <= 0)
|
|
522
|
+
return 0;
|
|
523
|
+
const present = Math.min(Math.max(df, 0), n);
|
|
524
|
+
return Math.log(1 + (n - present + 0.5) / (present + 0.5));
|
|
525
|
+
}
|
|
498
526
|
/** Commit trailer emitted only for topic pages (absence means "task"). */
|
|
499
527
|
const KIND_TRAILER_TOPIC = "GitMemo-Kind: topic";
|
|
500
528
|
/** ADD commit message (plan 4.1). */
|
|
@@ -1248,10 +1276,15 @@ export class GitMemo {
|
|
|
1248
1276
|
}
|
|
1249
1277
|
return value;
|
|
1250
1278
|
};
|
|
1279
|
+
const scoring = config.searchScoring ?? "weighted";
|
|
1280
|
+
if (!SEARCH_SCORING_MODES.includes(scoring)) {
|
|
1281
|
+
throw new GitMemoError(`gitmemo: searchScoring must be one of ${SEARCH_SCORING_MODES.join(" | ")}, got ${JSON.stringify(config.searchScoring)}`);
|
|
1282
|
+
}
|
|
1251
1283
|
this.config = {
|
|
1252
1284
|
searchLimit: positiveInt(config.searchLimit, 20, "searchLimit"),
|
|
1253
1285
|
gitTimeoutMs: positiveInt(config.gitTimeoutMs, 60000, "gitTimeoutMs"),
|
|
1254
|
-
lockTimeoutMs: positiveInt(config.lockTimeoutMs, 30000, "lockTimeoutMs")
|
|
1286
|
+
lockTimeoutMs: positiveInt(config.lockTimeoutMs, 30000, "lockTimeoutMs"),
|
|
1287
|
+
searchScoring: scoring
|
|
1255
1288
|
};
|
|
1256
1289
|
this.memDir = join(this.root, ".mem");
|
|
1257
1290
|
}
|
|
@@ -1299,7 +1332,7 @@ export class GitMemo {
|
|
|
1299
1332
|
* messages only, active-entry filtering, field-level matching and scoring,
|
|
1300
1333
|
* snapshot-stable pagination, in-process LRU.
|
|
1301
1334
|
*
|
|
1302
|
-
* @param keywords - 1-
|
|
1335
|
+
* @param keywords - 1-15 keywords (legacy callers may pass a comma-separated string).
|
|
1303
1336
|
* @param options.skip - pagination offset.
|
|
1304
1337
|
* @param options.snapshot - pass back the snapshot from the first page.
|
|
1305
1338
|
*/
|
|
@@ -1312,7 +1345,7 @@ export class GitMemo {
|
|
|
1312
1345
|
.map((k) => k.trim())
|
|
1313
1346
|
.filter((k) => k.length > 0)
|
|
1314
1347
|
.map((k) => normalizeKeyword(k));
|
|
1315
|
-
adapterWarning =
|
|
1348
|
+
adapterWarning = `gitmemo: deprecation — mem_search.keywords as a comma-separated string; pass an array of 1-${MAX_SEARCH_KEYWORDS} keywords`;
|
|
1316
1349
|
}
|
|
1317
1350
|
else {
|
|
1318
1351
|
if (!Array.isArray(keywords))
|
|
@@ -1324,8 +1357,8 @@ export class GitMemo {
|
|
|
1324
1357
|
adapterWarning = (adapterWarning ? adapterWarning + " " : "") +
|
|
1325
1358
|
"gitmemo: deprecation — mem_search.mode is gone; recall is always OR";
|
|
1326
1359
|
}
|
|
1327
|
-
if (normalized.length < 1 || normalized.length >
|
|
1328
|
-
throw new GitMemoError(
|
|
1360
|
+
if (normalized.length < 1 || normalized.length > MAX_SEARCH_KEYWORDS) {
|
|
1361
|
+
throw new GitMemoError(`gitmemo: search requires 1-${MAX_SEARCH_KEYWORDS} non-empty keywords`);
|
|
1329
1362
|
}
|
|
1330
1363
|
const skip = options.skip ?? 0;
|
|
1331
1364
|
if (!Number.isInteger(skip) || skip < 0)
|
|
@@ -1348,7 +1381,11 @@ export class GitMemo {
|
|
|
1348
1381
|
: undefined;
|
|
1349
1382
|
return { ...resolved, blockedNote: note };
|
|
1350
1383
|
});
|
|
1351
|
-
|
|
1384
|
+
// The scoring mode is part of the key: two handles on the SAME repo with
|
|
1385
|
+
// different `searchScoring` must not share a cached ranking. (Omitting it
|
|
1386
|
+
// made an A/B of the two modes return identical results — the second
|
|
1387
|
+
// handle simply read the first one's cached computation.)
|
|
1388
|
+
const cacheKey = this.memDir + "\x00" + snapshot + "\x00" + this.config.searchScoring + "\x00" + normalized.join("\x00");
|
|
1352
1389
|
let computation = searchCache.get(cacheKey);
|
|
1353
1390
|
if (computation === undefined) {
|
|
1354
1391
|
computation = await this.computeSearch(snapshot, normalized, legacy);
|
|
@@ -1383,6 +1420,15 @@ export class GitMemo {
|
|
|
1383
1420
|
const diagnostics = [...(legacySnapshot?.diagnostics ?? [])];
|
|
1384
1421
|
const seenFiles = new Set();
|
|
1385
1422
|
const nowMs = Date.now();
|
|
1423
|
+
// Two passes: collect the matching candidates first, then (in weighted
|
|
1424
|
+
// mode) score each match by how rare its keyword is *within this recall
|
|
1425
|
+
// page*. Saturation-free counting treats a distinctive term like
|
|
1426
|
+
// "biscuit" exactly like a ubiquitous one, which is what the LongMemEval
|
|
1427
|
+
// measurements pinned as the engine's ranking defect: evidence was almost
|
|
1428
|
+
// always reachable (recall_all@50 = 0.998) but ranked poorly
|
|
1429
|
+
// (ndcg_any@5 = 0.686). Rarity re-weights the same matches without
|
|
1430
|
+
// changing which entries match at all, so recall is untouched by design.
|
|
1431
|
+
const scored = [];
|
|
1386
1432
|
for (const record of candidates) {
|
|
1387
1433
|
if (record.added.length !== 1) {
|
|
1388
1434
|
if (record.added.length > 1)
|
|
@@ -1409,6 +1455,24 @@ export class GitMemo {
|
|
|
1409
1455
|
}
|
|
1410
1456
|
if (matched.length === 0)
|
|
1411
1457
|
continue;
|
|
1458
|
+
scored.push({ record, matched });
|
|
1459
|
+
}
|
|
1460
|
+
let weights;
|
|
1461
|
+
if (this.config.searchScoring === "weighted" && scored.length > 0) {
|
|
1462
|
+
weights = new Map();
|
|
1463
|
+
for (const kw of kws) {
|
|
1464
|
+
let df = 0;
|
|
1465
|
+
for (const entry of scored) {
|
|
1466
|
+
if (entry.matched.includes(kw))
|
|
1467
|
+
df += 1;
|
|
1468
|
+
}
|
|
1469
|
+
weights.set(kw, keywordRarity(df, scored.length));
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
for (const { record, matched } of scored) {
|
|
1473
|
+
const base = weights === undefined
|
|
1474
|
+
? matched.length
|
|
1475
|
+
: matched.reduce((sum, kw) => sum + (weights.get(kw) ?? 0), 0);
|
|
1412
1476
|
hits.push({
|
|
1413
1477
|
hash: record.hash,
|
|
1414
1478
|
title: record.message.subject,
|
|
@@ -1416,7 +1480,7 @@ export class GitMemo {
|
|
|
1416
1480
|
summary: record.message.summary,
|
|
1417
1481
|
keywords: record.message.keywords,
|
|
1418
1482
|
kind: record.message.kind,
|
|
1419
|
-
score: Math.round((
|
|
1483
|
+
score: Math.round((base + recencyBonus(record.committerTime, nowMs)) * 100) / 100,
|
|
1420
1484
|
matched_keywords: matched
|
|
1421
1485
|
});
|
|
1422
1486
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-gitmemo — System-one recall gate.
|
|
3
|
+
*
|
|
4
|
+
* Optional narrowing stage for `mem_search`. After the engine produces a page
|
|
5
|
+
* of lexical candidates, a System-one model (TypeSafe Jev by default, or any
|
|
6
|
+
* endpoint speaking the same request/answer contract) is asked one typed
|
|
7
|
+
* question per candidate, in a single request. Candidates the model judges
|
|
8
|
+
* irrelevant are dropped from the page the root agent sees, which cuts the
|
|
9
|
+
* tokens injected into the conversation.
|
|
10
|
+
*
|
|
11
|
+
* Design rules (deliberate, see README):
|
|
12
|
+
* - FAIL-OPEN: the gate may only ever REMOVE noise. Any transport, HTTP,
|
|
13
|
+
* timeout, or parse failure keeps every candidate and reports `degraded`,
|
|
14
|
+
* so a broken or unconfigured endpoint can never hide a memory.
|
|
15
|
+
* - NEVER EMPTY: when the gate would drop every candidate on a page, the
|
|
16
|
+
* `minKeep` best-ranked candidates are retained. A filter that silently
|
|
17
|
+
* returns nothing is indistinguishable from "no memory exists", which is
|
|
18
|
+
* the one failure mode a recall path must not have.
|
|
19
|
+
* - AUDITABLE: per-candidate values and the keep/drop verdict are returned in
|
|
20
|
+
* the tool output's `gated` block, so the decision can be inspected rather
|
|
21
|
+
* than trusted.
|
|
22
|
+
*
|
|
23
|
+
* This module imports nothing from the harness and performs no I/O of its own
|
|
24
|
+
* beyond `fetch`, so it is unit-testable with a fake fetch implementation.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-gitmemo/systemone
|
|
27
|
+
*/
|
|
28
|
+
/** Question types this gate can drive. */
|
|
29
|
+
export type SystemOneMode = "noul" | "score";
|
|
30
|
+
/** One candidate memory offered to the gate. */
|
|
31
|
+
export interface GateCandidate {
|
|
32
|
+
/** Commit hash — the identity used to keep/drop the candidate. */
|
|
33
|
+
hash: string;
|
|
34
|
+
/** Entry title (commit subject). */
|
|
35
|
+
title: string;
|
|
36
|
+
/** Entry summary (commit body). */
|
|
37
|
+
summary: string;
|
|
38
|
+
/** Structured keywords stored with the entry. */
|
|
39
|
+
keywords: string[];
|
|
40
|
+
/** Entry kind: `task` or `topic`. */
|
|
41
|
+
kind: string;
|
|
42
|
+
}
|
|
43
|
+
/** Resolved gate settings for one search. */
|
|
44
|
+
export interface GateOptions {
|
|
45
|
+
/** Absolute endpoint URL accepting the System-one request contract. */
|
|
46
|
+
endpoint: string;
|
|
47
|
+
/** Model identifier sent in the request body (e.g. `jev-latest`). */
|
|
48
|
+
model: string;
|
|
49
|
+
/** Question type used to judge each candidate. */
|
|
50
|
+
mode: SystemOneMode;
|
|
51
|
+
/** noul mode: keep a candidate when its probability is >= this value (0..1). */
|
|
52
|
+
threshold: number;
|
|
53
|
+
/** score mode: keep a candidate when its score is >= this value. */
|
|
54
|
+
scoreMin: number;
|
|
55
|
+
/** Retain at least this many candidates when the model rejects the whole page. */
|
|
56
|
+
minKeep: number;
|
|
57
|
+
/** What the caller does with the values; see {@link GatePolicy}. */
|
|
58
|
+
policy?: GatePolicy;
|
|
59
|
+
/**
|
|
60
|
+
* `filter` policy only: never drop more than this fraction of the judged page,
|
|
61
|
+
* lowest-scoring first. Bounds the damage a mis-calibrated threshold can do —
|
|
62
|
+
* an offline sweep kept recall_all@5 at 0.8875 with a 20 % cap, against 0.6875
|
|
63
|
+
* uncapped. Default 0.25.
|
|
64
|
+
*/
|
|
65
|
+
maxDropFraction?: number;
|
|
66
|
+
/** Never send more than this many candidates in one request. */
|
|
67
|
+
maxCandidates: number;
|
|
68
|
+
/** Truncate the task text to this many characters. */
|
|
69
|
+
maxTaskChars: number;
|
|
70
|
+
/** End-to-end request deadline in milliseconds. */
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
/** Literal API key, when one is configured in the settings section. */
|
|
73
|
+
apiKey?: string;
|
|
74
|
+
/** Late-bound credential lookup, used when no literal key is configured. */
|
|
75
|
+
resolveApiKey?: () => Promise<string | undefined>;
|
|
76
|
+
/** Injectable fetch, for tests. */
|
|
77
|
+
fetchImpl?: typeof fetch;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One candidate's verdict, retained for auditability.
|
|
81
|
+
*
|
|
82
|
+
* `value` is `null` when no readable answer came back for that candidate;
|
|
83
|
+
* `null` rather than `NaN` because the harness rejects non-finite numbers in
|
|
84
|
+
* session events and tool output must stay losslessly JSON-serializable.
|
|
85
|
+
*/
|
|
86
|
+
export interface GateVerdict {
|
|
87
|
+
hash: string;
|
|
88
|
+
/** Probability (noul) or score (score) returned for this candidate, or null. */
|
|
89
|
+
value: number | null;
|
|
90
|
+
/** Whether the candidate survived the gate. */
|
|
91
|
+
keep: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** Token accounting reported by the endpoint, when it reports any. */
|
|
94
|
+
export interface GateUsage {
|
|
95
|
+
/** Input tokens billed for the request (Jev bills input only). */
|
|
96
|
+
inputTokens?: number;
|
|
97
|
+
/** Output tokens; free on Jev, but reported. */
|
|
98
|
+
outputTokens?: number;
|
|
99
|
+
}
|
|
100
|
+
/** Outcome of one gate evaluation. */
|
|
101
|
+
export interface GateDecision {
|
|
102
|
+
/** Hashes retained, in their original order. */
|
|
103
|
+
kept: string[];
|
|
104
|
+
/** Hashes removed, in their original order. */
|
|
105
|
+
dropped: string[];
|
|
106
|
+
/** Per-candidate verdicts, in candidate order. */
|
|
107
|
+
verdicts: GateVerdict[];
|
|
108
|
+
/** Policy actually applied; see {@link GatePolicy}. */
|
|
109
|
+
policy: GatePolicy;
|
|
110
|
+
/** True when `maxDropFraction` restored candidates a threshold would have removed. */
|
|
111
|
+
bounded: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Candidate hashes ordered by the judge's value, best first (nulls last and
|
|
114
|
+
* kept stable). This is what the `rerank` policy applies; in `filter` mode it
|
|
115
|
+
* is still reported so a caller can log or inspect the judge's ordering.
|
|
116
|
+
*/
|
|
117
|
+
ordered: string[];
|
|
118
|
+
/** Question type actually used. */
|
|
119
|
+
mode: SystemOneMode;
|
|
120
|
+
/** Model identifier the request named. */
|
|
121
|
+
model: string;
|
|
122
|
+
/** True when the gate failed open (every candidate retained as a result). */
|
|
123
|
+
degraded: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Why the gate degraded — or, on a `false` {@link degraded} page, that it
|
|
126
|
+
* floored: no candidate cleared the threshold and `minKeep` was applied.
|
|
127
|
+
*/
|
|
128
|
+
reason?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Candidates actually sent to the endpoint, i.e. `min(maxCandidates, page
|
|
131
|
+
* size)`. Reported separately from the page size so a truncated judgement is
|
|
132
|
+
* never mistaken for a full one.
|
|
133
|
+
*/
|
|
134
|
+
judged: number;
|
|
135
|
+
/**
|
|
136
|
+
* Candidates past `maxCandidates`, which were never evaluated and therefore
|
|
137
|
+
* can never be dropped. Always `0` unless the page exceeded the cap.
|
|
138
|
+
*/
|
|
139
|
+
untouched: number;
|
|
140
|
+
/** Endpoint-reported token usage, so the gate's own cost is observable. */
|
|
141
|
+
usage?: GateUsage;
|
|
142
|
+
}
|
|
143
|
+
/** Score levels offered to the model in `score` mode (ordered low to high). */
|
|
144
|
+
export declare const SCORE_LEVELS: readonly ["unrelated to the task", "same general area but not actionable for this task", "partially relevant background", "substantially relevant and worth reusing", "directly reusable conclusions for this exact task"];
|
|
145
|
+
/**
|
|
146
|
+
* The judgement asked of the model, verbatim. Kept in English on purpose:
|
|
147
|
+
* Jev's primary training language is English and the vendor documents lower
|
|
148
|
+
* accuracy for other scripts, so only the (possibly CJK) candidate text and
|
|
149
|
+
* task text are non-English.
|
|
150
|
+
*/
|
|
151
|
+
export declare const NOUL_INSTRUCTION = "Does the `candidate` memory record contain conclusions, decisions, constraints, or reusable findings that are directly relevant to the `task`? Answer yes only if reading that memory would inform or change how the task is carried out. Answer no when it is merely on a related topic, is superseded, or concerns a different area.";
|
|
152
|
+
/**
|
|
153
|
+
* Clarifies what yes and no mean for {@link NOUL_INSTRUCTION}.
|
|
154
|
+
*
|
|
155
|
+
* A noul `criteria` is an object with exactly `true` and `false` keys — a bare
|
|
156
|
+
* string is rejected by the endpoint's validation, so this shape is required.
|
|
157
|
+
*/
|
|
158
|
+
export declare const NOUL_CRITERIA: {
|
|
159
|
+
readonly true: "yes — the memory is directly reusable for this task";
|
|
160
|
+
readonly false: "no — unrelated, superseded, or only loosely related to this task";
|
|
161
|
+
};
|
|
162
|
+
/** The judgement asked of the model in `score` mode. */
|
|
163
|
+
export declare const SCORE_INSTRUCTION = "How relevant is the `candidate` memory record to the `task`? Rate how reusable its conclusions, decisions, constraints, or findings are for this exact task.";
|
|
164
|
+
/**
|
|
165
|
+
* What the caller does with the judge's values.
|
|
166
|
+
*
|
|
167
|
+
* - `rerank` (default): keep every judged candidate and let the values reorder
|
|
168
|
+
* the page. Cannot lose a memory by construction.
|
|
169
|
+
* - `filter`: drop candidates below the threshold, then apply `maxDropFraction`.
|
|
170
|
+
*
|
|
171
|
+
* Measured on LongMemEval `_S` (500 questions, identical ingested memories): at
|
|
172
|
+
* threshold 0.5 the filter held 343 gold memories out of the prompt across 254
|
|
173
|
+
* instances and took LLM-as-Judge from 0.7840 to 0.3800, while the same values
|
|
174
|
+
* used as a reranker scored 0.8020 on the same context. The judge's AUC is only
|
|
175
|
+
* ~0.71-0.75, so no threshold can make a hard drop safe; the same signal is a
|
|
176
|
+
* fine ordering feature. See evals/longmemeval/README.md.
|
|
177
|
+
*/
|
|
178
|
+
export type GatePolicy = "rerank" | "filter";
|
|
179
|
+
/** Accepted `policy` values, for config validation and tool schemas. */
|
|
180
|
+
export declare const GATE_POLICIES: readonly GatePolicy[];
|
|
181
|
+
/**
|
|
182
|
+
* Build the System-one request body: the task is the shared `state`, and each
|
|
183
|
+
* candidate travels inside its own question's structured `instructions`, so
|
|
184
|
+
* every candidate is judged against the same task in one request and no
|
|
185
|
+
* candidate payload is repeated across questions.
|
|
186
|
+
*
|
|
187
|
+
* @param task - the agent's current task, used as the shared state.
|
|
188
|
+
* @param candidates - candidates to judge, already capped by the caller.
|
|
189
|
+
* @param options - resolved gate settings.
|
|
190
|
+
* @returns a JSON-serializable request body.
|
|
191
|
+
*/
|
|
192
|
+
export declare function buildRequest(task: string, candidates: readonly GateCandidate[], options: Pick<GateOptions, "mode" | "model" | "maxTaskChars">): {
|
|
193
|
+
state: {
|
|
194
|
+
task: string;
|
|
195
|
+
};
|
|
196
|
+
model: string;
|
|
197
|
+
questions: Record<string, unknown>;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Extract one numeric answer per question id from a System-one response.
|
|
201
|
+
*
|
|
202
|
+
* Tolerates both documented envelope shapes (a top-level map of answers, or
|
|
203
|
+
* the same map nested under `answers`) and both `noul`/`score` field spellings,
|
|
204
|
+
* so an endpoint that is merely contract-compatible still works.
|
|
205
|
+
*
|
|
206
|
+
* @param payload - parsed response JSON.
|
|
207
|
+
* @param mode - the question type that was asked.
|
|
208
|
+
* @param count - number of questions sent (ids are `m0`..`m<count-1>`).
|
|
209
|
+
* @returns the value per question index, or undefined where absent/unreadable.
|
|
210
|
+
*/
|
|
211
|
+
export declare function parseAnswers(payload: unknown, mode: SystemOneMode, count: number): Array<number | undefined>;
|
|
212
|
+
/**
|
|
213
|
+
* Read the endpoint's token accounting.
|
|
214
|
+
*
|
|
215
|
+
* @param payload - parsed response JSON.
|
|
216
|
+
* @returns camel-cased usage, or undefined when the endpoint reported none.
|
|
217
|
+
*/
|
|
218
|
+
export declare function parseUsage(payload: unknown): GateUsage | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* Turn per-candidate values into a keep/drop decision.
|
|
221
|
+
*
|
|
222
|
+
* Applies the configured threshold, then the `minKeep` floor that guarantees a
|
|
223
|
+
* non-empty page whenever candidates existed. Candidates with a missing or
|
|
224
|
+
* unreadable answer are KEPT (fail-open at the per-candidate level).
|
|
225
|
+
*
|
|
226
|
+
* @param candidates - candidates in page order.
|
|
227
|
+
* @param values - value per candidate index (undefined when unreadable).
|
|
228
|
+
* @param options - resolved gate settings.
|
|
229
|
+
* @returns kept/dropped hash lists and per-candidate verdicts.
|
|
230
|
+
*/
|
|
231
|
+
export declare function decide(candidates: readonly GateCandidate[], values: ReadonlyArray<number | undefined>, options: Pick<GateOptions, "mode" | "threshold" | "scoreMin" | "minKeep" | "policy" | "maxDropFraction">): {
|
|
232
|
+
kept: string[];
|
|
233
|
+
dropped: string[];
|
|
234
|
+
verdicts: GateVerdict[];
|
|
235
|
+
floored: boolean;
|
|
236
|
+
bounded: boolean;
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Evaluate the gate for one search page.
|
|
240
|
+
*
|
|
241
|
+
* Never throws and never returns an empty `kept` list while candidates exist:
|
|
242
|
+
* every failure path degrades to "keep everything" with a reason.
|
|
243
|
+
*
|
|
244
|
+
* @param task - the agent's current task text (shared state).
|
|
245
|
+
* @param candidates - page candidates; only the first `maxCandidates` are judged.
|
|
246
|
+
* @param options - resolved gate settings.
|
|
247
|
+
* @param signal - caller cancellation.
|
|
248
|
+
* @returns the decision to apply to the page.
|
|
249
|
+
*/
|
|
250
|
+
export declare function evaluateGate(task: string, candidates: readonly GateCandidate[], options: GateOptions, signal?: AbortSignal): Promise<GateDecision>;
|