@aiwayds/dsh-mcp-adapter 0.5.1 → 0.6.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 +49 -3
- package/README.zh.md +50 -4
- package/lib/index.d.ts +275 -22
- package/lib/index.js +612 -70
- package/lib/index.js.map +1 -1
- package/lib/jev/backends.d.ts +35 -0
- package/lib/jev/backends.js +48 -0
- package/lib/jev/backends.js.map +1 -0
- package/lib/jev/client.d.ts +133 -0
- package/lib/jev/client.js +288 -0
- package/lib/jev/client.js.map +1 -0
- package/lib/jev/config.d.ts +82 -0
- package/lib/jev/config.js +55 -0
- package/lib/jev/config.js.map +1 -0
- package/lib/jev/dual.d.ts +42 -0
- package/lib/jev/dual.js +42 -0
- package/lib/jev/dual.js.map +1 -0
- package/lib/jev/gate.d.ts +51 -0
- package/lib/jev/gate.js +141 -0
- package/lib/jev/gate.js.map +1 -0
- package/lib/jev/log.d.ts +93 -0
- package/lib/jev/log.js +138 -0
- package/lib/jev/log.js.map +1 -0
- package/lib/jev/seams.d.ts +118 -0
- package/lib/jev/seams.js +364 -0
- package/lib/jev/seams.js.map +1 -0
- package/lib/jev/thresholds.d.ts +37 -0
- package/lib/jev/thresholds.js +44 -0
- package/lib/jev/thresholds.js.map +1 -0
- package/lib/rank.d.ts +157 -0
- package/lib/rank.js +327 -0
- package/lib/rank.js.map +1 -0
- package/lib/usage.d.ts +232 -0
- package/lib/usage.js +362 -0
- package/lib/usage.js.map +1 -0
- package/package.json +1 -1
package/lib/jev/gate.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound secret gate (design 2026-09-25 §3.3) — ported verbatim from
|
|
3
|
+
* dsh-topics-memory `src/jev/gate.ts`, a self-contained port of the
|
|
4
|
+
* dsh-jev-mcp `server.mjs` mechanism. The COMPLETE serialized request body
|
|
5
|
+
* (model, state, questions — byte-identical to what goes on the wire) is
|
|
6
|
+
* scanned for credential patterns BEFORE anything leaves the process; a hit
|
|
7
|
+
* aborts the call with a structured descriptor that never echoes the secret.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY INVARIANT: hit descriptors carry static metadata only (pattern
|
|
10
|
+
* name, offset, ≤10 printable chars before the hit) — never the matched
|
|
11
|
+
* text, and never text at or after the hit point.
|
|
12
|
+
*
|
|
13
|
+
* @module jev/gate
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
16
|
+
function safeWarn(log, message) {
|
|
17
|
+
try {
|
|
18
|
+
log?.warn?.(message);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// contained — diagnostics never take the feature down
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Prefix-style patterns require a 20+ char continuation so that merely
|
|
25
|
+
// DISCUSSING a prefix (e.g. "set OPENROUTER_API_KEY to an sk-or-v1- key")
|
|
26
|
+
// never trips the gate — only an actual long token does.
|
|
27
|
+
export const BUILTIN_PATTERNS = [
|
|
28
|
+
{ name: 'jwt', re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
29
|
+
{ name: 'openrouter-key', re: /sk-or-v1-[A-Za-z0-9_-]{20,}/ },
|
|
30
|
+
{ name: 'anthropic-key', re: /sk-ant-[A-Za-z0-9_-]{20,}/ },
|
|
31
|
+
{ name: 'openai-project-key', re: /sk-proj-[A-Za-z0-9_-]{20,}/ },
|
|
32
|
+
{ name: 'openai-sk-key', re: /sk-[A-Za-z0-9]{20,}/ },
|
|
33
|
+
{ name: 'github-token', re: /ghp_[A-Za-z0-9]{20,}/ },
|
|
34
|
+
{ name: 'github-pat', re: /github_pat_[A-Za-z0-9_]{20,}/ },
|
|
35
|
+
{ name: 'gitlab-token', re: /glpat-[A-Za-z0-9_-]{20,}/ },
|
|
36
|
+
{ name: 'slack-token', re: /xox[baprs]-[A-Za-z0-9-]{20,}/ },
|
|
37
|
+
{ name: 'aws-access-key-id', re: /AKIA[0-9A-Z]{16}/ },
|
|
38
|
+
{ name: 'aws-temp-key-id', re: /ASIA[0-9A-Z]{16}/ },
|
|
39
|
+
{ name: 'google-api-key', re: /AIza[A-Za-z0-9_-]{20,}/ },
|
|
40
|
+
{ name: 'private-key-block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
41
|
+
];
|
|
42
|
+
const MAX_SECRET_LINE = 512; // longer lines WARN; matching ALWAYS uses the full line
|
|
43
|
+
const MIN_EXTERNAL_SECRET = 8; // shorter entries would cause runaway false hits
|
|
44
|
+
/** Literal secrets from the external list file (JEV_SECRET_FILE semantics:
|
|
45
|
+
* one entry per line, ≥8 chars, `#` comments skipped, matched at FULL
|
|
46
|
+
* length — a >512-char line warns but is never truncated, or everything
|
|
47
|
+
* past char 512 would silently escape the gate). */
|
|
48
|
+
export function loadExternalSecrets(file, log) {
|
|
49
|
+
if (file === '')
|
|
50
|
+
return [];
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = readFileSync(file, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
safeWarn(log, `mcp-adapter jev: cannot read jevSecretFile (${file}): ${err instanceof Error ? err.message : String(err)}; continuing without it.`);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const secrets = [];
|
|
60
|
+
raw.split(/\r?\n/).forEach((line, i) => {
|
|
61
|
+
const s = line.trim();
|
|
62
|
+
if (s === '' || s.startsWith('#'))
|
|
63
|
+
return;
|
|
64
|
+
if (s.length < MIN_EXTERNAL_SECRET) {
|
|
65
|
+
safeWarn(log, `mcp-adapter jev: jevSecretFile line ${i + 1} shorter than ${MIN_EXTERNAL_SECRET} chars, skipped (false-positive guard).`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (s.length > MAX_SECRET_LINE) {
|
|
69
|
+
safeWarn(log, `mcp-adapter jev: jevSecretFile line ${i + 1} is ${s.length} chars (> ${MAX_SECRET_LINE}); matching uses the FULL line (no truncation).`);
|
|
70
|
+
}
|
|
71
|
+
secrets.push(s);
|
|
72
|
+
});
|
|
73
|
+
return secrets;
|
|
74
|
+
}
|
|
75
|
+
// Cached external list, keyed by path, so the file is read once instead of on
|
|
76
|
+
// every outbound payload. The `jevSecretFile` config is VOLATILE — an operator
|
|
77
|
+
// can repoint it AND edit the file in place — and nothing in the plugin calls
|
|
78
|
+
// reloadSecrets on a settings write, so a cache hit is validated against the
|
|
79
|
+
// file's mtime+size (one stat, far cheaper than a re-read) before it is
|
|
80
|
+
// served. Without that, adding a secret to the file by hand would not be gated
|
|
81
|
+
// until the path changed or the process restarted.
|
|
82
|
+
let cache = { file: '', secrets: [], stamp: '' };
|
|
83
|
+
/** mtime+size of the list file, or '' when it cannot be stat'ed. */
|
|
84
|
+
function fileStamp(file) {
|
|
85
|
+
if (file === '')
|
|
86
|
+
return '';
|
|
87
|
+
try {
|
|
88
|
+
const { mtimeMs, size } = statSync(file);
|
|
89
|
+
return `${mtimeMs}:${size}`;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function currentSecrets(file, log) {
|
|
96
|
+
// A new path, or the same path whose file changed under us, re-reads.
|
|
97
|
+
if (cache.file !== file || cache.stamp !== fileStamp(file))
|
|
98
|
+
reloadSecrets(file, log);
|
|
99
|
+
return cache.secrets;
|
|
100
|
+
}
|
|
101
|
+
export function reloadSecrets(file, log) {
|
|
102
|
+
cache = { file, secrets: loadExternalSecrets(file, log), stamp: fileStamp(file) };
|
|
103
|
+
return cache.secrets;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Redacted context: at most 10 printable chars BEFORE the hit plus an
|
|
107
|
+
* explicit [REDACTED:<pattern>] marker. Content at or after the hit point is
|
|
108
|
+
* NEVER shown — when a prefix regex under-matches, the secret tail sits
|
|
109
|
+
* right there. Belt and suspenders: if the matched text or any known
|
|
110
|
+
* external secret still appears in the context, the whole context collapses
|
|
111
|
+
* to '[REDACTED]'.
|
|
112
|
+
*/
|
|
113
|
+
export function describeHit(pattern, offset, matched, payload, externalSecrets = []) {
|
|
114
|
+
const before = payload.slice(Math.max(0, offset - 10), offset).replace(/[^\x20-\x7e]/g, '.'); // keep it printable
|
|
115
|
+
const context = `${before}[REDACTED:${pattern}]`;
|
|
116
|
+
if (context.includes(matched) || externalSecrets.some((s) => s !== '' && context.includes(s))) {
|
|
117
|
+
return { pattern, offset, context: '[REDACTED]' };
|
|
118
|
+
}
|
|
119
|
+
return { pattern, offset, context };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Scan the serialized outbound payload for secrets.
|
|
123
|
+
* @param payload JSON.stringify({model, state, questions}) — the exact wire body
|
|
124
|
+
* @param externalSecrets literal secrets from the external list (cached via currentSecrets)
|
|
125
|
+
* @param externalSource label for hit descriptors (the list file path)
|
|
126
|
+
*/
|
|
127
|
+
export function scanForSecrets(payload, externalSecrets = [], externalSource = '') {
|
|
128
|
+
for (const { name, re } of BUILTIN_PATTERNS) {
|
|
129
|
+
const m = re.exec(payload);
|
|
130
|
+
if (m)
|
|
131
|
+
return describeHit(name, m.index, m[0], payload, externalSecrets);
|
|
132
|
+
}
|
|
133
|
+
for (const secret of externalSecrets) {
|
|
134
|
+
const idx = payload.indexOf(secret);
|
|
135
|
+
if (idx !== -1) {
|
|
136
|
+
return describeHit(externalSource !== '' ? `external(${externalSource})` : 'external', idx, secret, payload, externalSecrets);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/jev/gate.ts"],"names":[],"mappings":"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"}
|
package/lib/jev/log.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decisions.jsonl — System One usage/optimization telemetry (design
|
|
3
|
+
* 2026-09-25 §6), always on while jevEnabled is on. Ported from
|
|
4
|
+
* dsh-topics-memory `src/jev/log.ts` with ONE structural change: the
|
|
5
|
+
* destination directory is no longer resolved from an env/paths module
|
|
6
|
+
* inside the log, it is INJECTED by the caller
|
|
7
|
+
* ({@link createDecisionLog}). Everything else is carried over unchanged:
|
|
8
|
+
* 512KB auto-compaction keeping the most recent quarter, line JSON, and
|
|
9
|
+
* write failures swallowed silently — fail-open extends to the stats itself.
|
|
10
|
+
*
|
|
11
|
+
* The caller owns the directory; the plugin's own gate directory
|
|
12
|
+
* (`<dsh home>/storages/mcp-adapter`, see resolveGateDir in ../index.ts) is
|
|
13
|
+
* the intended one, so decisions.jsonl lands beside gate.json. Nothing here
|
|
14
|
+
* reads the dsh home, which keeps the module testable against a temp dir.
|
|
15
|
+
*
|
|
16
|
+
* REDACTION INVARIANT (§6.1): metadata and probabilities only — reference
|
|
17
|
+
* hash/question type/score/latency/token count. NEVER the state text or
|
|
18
|
+
* conclusion bodies. Writers copy an explicit field whitelist, so extra
|
|
19
|
+
* fields on caller objects cannot leak into the file.
|
|
20
|
+
*
|
|
21
|
+
* @module jev/log
|
|
22
|
+
*/
|
|
23
|
+
import type { JevBand } from './thresholds.ts';
|
|
24
|
+
/** This plugin's three jev seams. A row must be attributable to exactly one
|
|
25
|
+
* of them — the seam identity is what makes the usage/calibration analysis
|
|
26
|
+
* separable per seam. */
|
|
27
|
+
export type JevLane = 'usage-suggest' | 'list-rank' | 'didyoumean';
|
|
28
|
+
export type JevOutcome = 'ok' | 'timeout' | 'network' | 'http_4xx' | 'http_5xx' | 'bad_json' | 'secret_gate_blocked' | 'missing_key'
|
|
29
|
+
/** Local pre-flight refusal (e.g. questions violate the decisions/openrouter
|
|
30
|
+
* wire contract) — nothing was sent. Reserved outcome beyond §6.2's v1 list. */
|
|
31
|
+
| 'invalid_request';
|
|
32
|
+
export interface JevUsage {
|
|
33
|
+
input_tokens: number;
|
|
34
|
+
output_tokens: number;
|
|
35
|
+
}
|
|
36
|
+
/** Call layer (§6.2): one row per HTTP call, failures included — the
|
|
37
|
+
* fallback rate is the health metric, so failed calls must be counted. */
|
|
38
|
+
export interface JevCallRecord {
|
|
39
|
+
at: string;
|
|
40
|
+
lane: JevLane;
|
|
41
|
+
backend: string;
|
|
42
|
+
model: string;
|
|
43
|
+
questionCount: number;
|
|
44
|
+
stateChars: number;
|
|
45
|
+
latencyMs: number;
|
|
46
|
+
usage: JevUsage | null;
|
|
47
|
+
outcome: JevOutcome;
|
|
48
|
+
fallback: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Verdict layer (§6.2): one row per question, batch requests expanded. */
|
|
51
|
+
export interface JevVerdictRecord {
|
|
52
|
+
at: string;
|
|
53
|
+
lane: JevLane;
|
|
54
|
+
questionId: string;
|
|
55
|
+
qtype: 'noul' | 'choice' | 'score';
|
|
56
|
+
/** Candidate reference without content: `tool:xxx` | `server:name`. */
|
|
57
|
+
ref: string;
|
|
58
|
+
/** §6.2 digest — stable key for the repeat-decision-rate analysis. */
|
|
59
|
+
digest: string;
|
|
60
|
+
probability: number;
|
|
61
|
+
band: JevBand;
|
|
62
|
+
agree: 'hit' | 'nearFloor' | 'gate-blocked' | 'wouldBlock' | 'n/a';
|
|
63
|
+
/** Which backend produced this verdict: 'primary' (default — the configured
|
|
64
|
+
* backend) or 'laya' (the local pace-maker). Design §3.4. */
|
|
65
|
+
backend?: string;
|
|
66
|
+
/** True when this row's source DROVE the decision because the primary
|
|
67
|
+
* backend was unavailable (degraded mode). Comparison rows from the
|
|
68
|
+
* pace-maker are degraded=false; laya rows are excluded from ECE either
|
|
69
|
+
* way (uncalibrated probabilities). */
|
|
70
|
+
degraded?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** The log file for one directory. The directory is created lazily on the
|
|
73
|
+
* first append — `createDecisionLog` itself touches nothing on disk. */
|
|
74
|
+
export declare function decisionsFile(logDir: string): string;
|
|
75
|
+
/** digest(state, questionId, instructions): `h1:` + first 16 hex of
|
|
76
|
+
* sha256(state + NUL + questionId + NUL + instructions) — the key the
|
|
77
|
+
* repeat-decision-rate analysis joins on. */
|
|
78
|
+
export declare function digestFor(state: string, questionId: string, instructions: string): string;
|
|
79
|
+
/** One injected log handle. jevAsk takes this as a required argument so the
|
|
80
|
+
* "row on EVERY path" invariant cannot be skipped by a caller. */
|
|
81
|
+
export interface DecisionLog {
|
|
82
|
+
/** Absolute path this log appends to. */
|
|
83
|
+
file(): string;
|
|
84
|
+
/** Call-layer row (field whitelist enforced). */
|
|
85
|
+
logCall(rec: JevCallRecord): Promise<void>;
|
|
86
|
+
/** Verdict-layer rows (batch expanded by the caller). */
|
|
87
|
+
logVerdicts(recs: readonly JevVerdictRecord[]): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a decisions.jsonl log over one directory. Cheap and side-effect
|
|
91
|
+
* free: build it once (apply time), keep the handle, pass it to jevAsk.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createDecisionLog(logDir: string): DecisionLog;
|
package/lib/jev/log.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decisions.jsonl — System One usage/optimization telemetry (design
|
|
3
|
+
* 2026-09-25 §6), always on while jevEnabled is on. Ported from
|
|
4
|
+
* dsh-topics-memory `src/jev/log.ts` with ONE structural change: the
|
|
5
|
+
* destination directory is no longer resolved from an env/paths module
|
|
6
|
+
* inside the log, it is INJECTED by the caller
|
|
7
|
+
* ({@link createDecisionLog}). Everything else is carried over unchanged:
|
|
8
|
+
* 512KB auto-compaction keeping the most recent quarter, line JSON, and
|
|
9
|
+
* write failures swallowed silently — fail-open extends to the stats itself.
|
|
10
|
+
*
|
|
11
|
+
* The caller owns the directory; the plugin's own gate directory
|
|
12
|
+
* (`<dsh home>/storages/mcp-adapter`, see resolveGateDir in ../index.ts) is
|
|
13
|
+
* the intended one, so decisions.jsonl lands beside gate.json. Nothing here
|
|
14
|
+
* reads the dsh home, which keeps the module testable against a temp dir.
|
|
15
|
+
*
|
|
16
|
+
* REDACTION INVARIANT (§6.1): metadata and probabilities only — reference
|
|
17
|
+
* hash/question type/score/latency/token count. NEVER the state text or
|
|
18
|
+
* conclusion bodies. Writers copy an explicit field whitelist, so extra
|
|
19
|
+
* fields on caller objects cannot leak into the file.
|
|
20
|
+
*
|
|
21
|
+
* @module jev/log
|
|
22
|
+
*/
|
|
23
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
24
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
const COMPACT_LIMIT = 512 * 1024; // ~512KB cap, same as the ilog sidecars
|
|
27
|
+
/** The log file for one directory. The directory is created lazily on the
|
|
28
|
+
* first append — `createDecisionLog` itself touches nothing on disk. */
|
|
29
|
+
export function decisionsFile(logDir) {
|
|
30
|
+
return join(logDir, 'decisions.jsonl');
|
|
31
|
+
}
|
|
32
|
+
/** Digest field separator — a NUL byte, spelled this way so the source file
|
|
33
|
+
* itself stays plain text (a raw NUL would make the file binary). */
|
|
34
|
+
const NUL_SEP = String.fromCharCode(0);
|
|
35
|
+
/** digest(state, questionId, instructions): `h1:` + first 16 hex of
|
|
36
|
+
* sha256(state + NUL + questionId + NUL + instructions) — the key the
|
|
37
|
+
* repeat-decision-rate analysis joins on. */
|
|
38
|
+
export function digestFor(state, questionId, instructions) {
|
|
39
|
+
const h = createHash('sha256');
|
|
40
|
+
h.update(state);
|
|
41
|
+
h.update(NUL_SEP);
|
|
42
|
+
h.update(questionId);
|
|
43
|
+
h.update(NUL_SEP);
|
|
44
|
+
h.update(instructions);
|
|
45
|
+
return `h1:${h.digest('hex').slice(0, 16)}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build a decisions.jsonl log over one directory. Cheap and side-effect
|
|
49
|
+
* free: build it once (apply time), keep the handle, pass it to jevAsk.
|
|
50
|
+
*/
|
|
51
|
+
export function createDecisionLog(logDir) {
|
|
52
|
+
const file = decisionsFile(logDir);
|
|
53
|
+
// Serialized like the store's write queue: single-record appends are
|
|
54
|
+
// line-atomic anyway, but the compaction rewrite must not interleave with
|
|
55
|
+
// an append. Per-log, so two logs never block each other.
|
|
56
|
+
let queue = Promise.resolve();
|
|
57
|
+
function enqueue(op) {
|
|
58
|
+
const run = queue.then(op, op);
|
|
59
|
+
queue = run.catch(() => undefined);
|
|
60
|
+
return run;
|
|
61
|
+
}
|
|
62
|
+
/** All write failures die HERE — never upstream. */
|
|
63
|
+
async function appendJsonl(lines) {
|
|
64
|
+
await enqueue(async () => {
|
|
65
|
+
try {
|
|
66
|
+
await mkdir(logDir, { recursive: true });
|
|
67
|
+
await appendFile(file, lines.map((l) => `${l}\n`).join(''), 'utf8');
|
|
68
|
+
await compactIfNeeded(file);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// fail-open: a broken stats sink must never break the feature (§6.1)
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// ~512KB cap; keep the most recent quarter when exceeded (headroom for
|
|
76
|
+
// the next window).
|
|
77
|
+
async function compactIfNeeded(target) {
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = await readFile(target, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (raw.length <= COMPACT_LIMIT)
|
|
86
|
+
return;
|
|
87
|
+
const lines = raw.split('\n').filter((l) => l.trim() !== '');
|
|
88
|
+
const keep = lines.slice(-Math.max(1, Math.floor(lines.length / 4)));
|
|
89
|
+
const tmp = `${target}.tmp-${randomUUID()}`;
|
|
90
|
+
try {
|
|
91
|
+
await writeFile(tmp, keep.map((l) => `${l}\n`).join(''), 'utf8');
|
|
92
|
+
await rename(tmp, target);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// swallow — the next append retries the compaction
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Call-layer row. Field whitelist enforced here: anything extra on the
|
|
99
|
+
* caller's object is dropped, so no state/conclusion text can sneak in. */
|
|
100
|
+
async function logCall(rec) {
|
|
101
|
+
await appendJsonl([
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
at: rec.at,
|
|
104
|
+
lane: rec.lane,
|
|
105
|
+
backend: rec.backend,
|
|
106
|
+
model: rec.model,
|
|
107
|
+
questionCount: rec.questionCount,
|
|
108
|
+
stateChars: rec.stateChars,
|
|
109
|
+
latencyMs: rec.latencyMs,
|
|
110
|
+
usage: rec.usage === null
|
|
111
|
+
? null
|
|
112
|
+
: { input_tokens: rec.usage.input_tokens, output_tokens: rec.usage.output_tokens },
|
|
113
|
+
outcome: rec.outcome,
|
|
114
|
+
fallback: rec.fallback,
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
/** Verdict-layer rows (batch expanded by the caller). */
|
|
119
|
+
async function logVerdicts(recs) {
|
|
120
|
+
if (recs.length === 0)
|
|
121
|
+
return;
|
|
122
|
+
await appendJsonl(recs.map((rec) => JSON.stringify({
|
|
123
|
+
at: rec.at,
|
|
124
|
+
lane: rec.lane,
|
|
125
|
+
questionId: rec.questionId,
|
|
126
|
+
qtype: rec.qtype,
|
|
127
|
+
ref: rec.ref,
|
|
128
|
+
digest: rec.digest,
|
|
129
|
+
probability: rec.probability,
|
|
130
|
+
band: rec.band,
|
|
131
|
+
agree: rec.agree,
|
|
132
|
+
backend: rec.backend ?? 'primary',
|
|
133
|
+
degraded: rec.degraded === true,
|
|
134
|
+
})));
|
|
135
|
+
}
|
|
136
|
+
return { file: () => file, logCall, logVerdicts };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/jev/log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three System One seams (jev business wiring) — and ONLY these three
|
|
3
|
+
* places in the plugin may call out to a decision backend.
|
|
4
|
+
*
|
|
5
|
+
* WHERE THEY ARE ALLOWED TO RUN (red line ①, hot paths stay remote-free):
|
|
6
|
+
* - {@link askKeepSuggestions} — the background keep-suggestion batch, fired
|
|
7
|
+
* by the usage counter and awaited by nobody;
|
|
8
|
+
* - {@link rankListQuery} — `mcp_list {query}` only, i.e. when the MODEL
|
|
9
|
+
* explicitly asked for a ranked view;
|
|
10
|
+
* - {@link rankDidYouMean} — `mcp_call`'s unknown-tool branch, i.e. an error
|
|
11
|
+
* the model is going to read anyway.
|
|
12
|
+
* The assemble waterfall, `mcp_list` without a query, and a SUCCESSFUL
|
|
13
|
+
* dispatch never appear in this file's call graph.
|
|
14
|
+
*
|
|
15
|
+
* FAIL-OPEN (red line ②): every export returns a "no opinion" value on ANY
|
|
16
|
+
* failure — disabled, timeout, 5xx, bad JSON, a thrown hook, an unparseable
|
|
17
|
+
* answer. None of them throws. The caller's lexical order then stands, which
|
|
18
|
+
* is the pre-seam behavior; a jev outage is indistinguishable from jev being
|
|
19
|
+
* off.
|
|
20
|
+
*
|
|
21
|
+
* CALIBRATION: the batch in {@link askKeepSuggestions} is the one seam that
|
|
22
|
+
* reads a band (thresholds.ts, kind 'suggest') — and only as a DISPLAY label
|
|
23
|
+
* on a suggestion row, and only for a PRIMARY probability. The two ranking
|
|
24
|
+
* seams log their probabilities with band 'record': a rank is relative
|
|
25
|
+
* ordering, and adopting/vetoing thresholds ported from another plugin's
|
|
26
|
+
* protocol would be a category error. An laya probability is not calibrated at
|
|
27
|
+
* all (dual.ts: relative use only), so a pace-maker answer NEVER takes a band
|
|
28
|
+
* and NEVER becomes a suggestion row — see {@link askKeepSuggestions}.
|
|
29
|
+
*
|
|
30
|
+
* @module jev/seams
|
|
31
|
+
*/
|
|
32
|
+
import type { JevDualConfig } from './dual.ts';
|
|
33
|
+
import type { DecisionLog } from './log.ts';
|
|
34
|
+
import type { RankCandidate } from '../rank.ts';
|
|
35
|
+
import type { SuggestCandidate, SuggestionRecord } from '../usage.ts';
|
|
36
|
+
/**
|
|
37
|
+
* Everything a seam needs from the host, read PER CALL so a settings-page
|
|
38
|
+
* write takes effect on the next request (0.1.7 volatile config contract).
|
|
39
|
+
* Production builds one of these in apply() over the live config refs.
|
|
40
|
+
*/
|
|
41
|
+
export interface JevRuntime {
|
|
42
|
+
/** The master switch; false means "no call, and no failure". */
|
|
43
|
+
enabled(): boolean;
|
|
44
|
+
/** Resolved backend/model/timeout/secret/pace-maker settings. */
|
|
45
|
+
config(): JevDualConfig;
|
|
46
|
+
/** decisions.jsonl sink (createDecisionLog over the gate directory). */
|
|
47
|
+
log(): DecisionLog;
|
|
48
|
+
}
|
|
49
|
+
/** A tool as the ranking seams see it: a name plus a blurb for the criteria. */
|
|
50
|
+
export type { RankCandidate } from '../rank.ts';
|
|
51
|
+
/** A probability is usable only when it is a finite 0..1 number. */
|
|
52
|
+
export declare function noulProbability(answer: unknown): number | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Turn one jev ranking answer into an order. A full probability map sorts by
|
|
55
|
+
* probability descending (ties keep the lexical order); a bare `choice` field
|
|
56
|
+
* only moves the winner to the front; anything unparseable reports "no
|
|
57
|
+
* opinion" so the caller keeps the order it already had.
|
|
58
|
+
*
|
|
59
|
+
* @param candidates - the pool the question was asked about, in lexical order.
|
|
60
|
+
* @param answer - the raw answer value for {@link RANK_QUESTION_ID}.
|
|
61
|
+
* @returns the reordered names, or undefined for "no opinion".
|
|
62
|
+
*/
|
|
63
|
+
export declare function orderFromChoice(candidates: readonly RankCandidate[], answer: unknown): readonly string[] | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Seam B: rerank `mcp_list {query}` candidates by relevance.
|
|
66
|
+
*
|
|
67
|
+
* The incoming order is the LEXICAL one (see ../rank.ts) and is also the
|
|
68
|
+
* fail-open result: any failure, disabled switch, or unusable answer returns
|
|
69
|
+
* `undefined`, which the caller reads as "keep what you had".
|
|
70
|
+
*
|
|
71
|
+
* A DEGRADED answer (the local laya pace-maker took over because the primary
|
|
72
|
+
* backend failed) is usable here: relative ordering is exactly what a rank
|
|
73
|
+
* is, and jev/dual.ts's contract allows degraded results for relative use
|
|
74
|
+
* only — which is all this seam does.
|
|
75
|
+
*
|
|
76
|
+
* @param query - the model's query text.
|
|
77
|
+
* @param ordered - lexical order (best first), already capped by the caller.
|
|
78
|
+
* @param env - the live jev runtime.
|
|
79
|
+
* @returns the jev order, or undefined for "no opinion".
|
|
80
|
+
*/
|
|
81
|
+
export declare function rankListQuery(query: string, ordered: readonly RankCandidate[], env: JevRuntime): Promise<readonly string[] | undefined>;
|
|
82
|
+
/**
|
|
83
|
+
* Seam C: rerank the near-miss candidates of an unregistered tool name.
|
|
84
|
+
*
|
|
85
|
+
* Same fail-open contract as {@link rankListQuery}. Only runs when the lexical
|
|
86
|
+
* prefilter found MORE THAN ONE candidate — a single lexical hit needs no
|
|
87
|
+
* ranking, so the cold branch stays free when it has nothing to offer.
|
|
88
|
+
*
|
|
89
|
+
* @param tool - the name dispatch could not resolve.
|
|
90
|
+
* @param ordered - lexical near-miss order (best first).
|
|
91
|
+
* @param env - the live jev runtime.
|
|
92
|
+
* @returns the jev order, or undefined for "no opinion".
|
|
93
|
+
*/
|
|
94
|
+
export declare function rankDidYouMean(tool: string, ordered: readonly RankCandidate[], env: JevRuntime): Promise<readonly string[] | undefined>;
|
|
95
|
+
/**
|
|
96
|
+
* Seam A: ask, in ONE batched noul request, whether each folded-and-used tool
|
|
97
|
+
* earns a permanent place in every prompt.
|
|
98
|
+
*
|
|
99
|
+
* Fire-and-forget by contract: the caller never awaits this on a hot path.
|
|
100
|
+
* `null` means "this round produced nothing" (disabled, call failed, no
|
|
101
|
+
* candidates, unusable answers, DEGRADED primary) — the caller then keeps the
|
|
102
|
+
* PREVIOUS batch in suggestions.json, and the usage record is unaffected
|
|
103
|
+
* either way.
|
|
104
|
+
*
|
|
105
|
+
* ABSOLUTE SCORES ONLY FROM THE PRIMARY: a suggestion row is a calibrated
|
|
106
|
+
* probability plus a band plus an actionable "keep it resident" line that a
|
|
107
|
+
* human reads in `/mcp suggest`. When the primary failed and the local laya
|
|
108
|
+
* pace-maker took over, that answer is usable for RELATIVE ranking only
|
|
109
|
+
* (dual.ts) and laya's negatives sit in the same band as its positives — so a
|
|
110
|
+
* degraded round returns `null` instead of rendering uncalibrated numbers. The
|
|
111
|
+
* answer is still logged (band 'record', degraded=true): the laya-vs-jev bench
|
|
112
|
+
* and the fallback-rate health metric must not lose a round.
|
|
113
|
+
*
|
|
114
|
+
* @param candidates - folded tools with recorded calls, most-used first.
|
|
115
|
+
* @param env - the live jev runtime.
|
|
116
|
+
* @returns one row per answered candidate, or null.
|
|
117
|
+
*/
|
|
118
|
+
export declare function askKeepSuggestions(candidates: readonly SuggestCandidate[], env: JevRuntime): Promise<SuggestionRecord[] | null>;
|