@dzhechkov/harness-core 0.3.90 → 0.3.94
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/__tests__/golden-baseline.test.d.ts +2 -0
- package/dist/__tests__/golden-baseline.test.d.ts.map +1 -0
- package/dist/__tests__/golden-baseline.test.js +60 -0
- package/dist/__tests__/golden-baseline.test.js.map +1 -0
- package/dist/agentdb-index.d.ts +9 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +44 -1
- package/dist/agentdb-index.js.map +1 -1
- package/dist/brain.d.ts +26 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +105 -4
- package/dist/brain.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +69 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +79 -9
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/learning-backend.d.ts +85 -0
- package/dist/learning-backend.d.ts.map +1 -0
- package/dist/learning-backend.js +133 -0
- package/dist/learning-backend.js.map +1 -0
- package/dist/patterns.d.ts +43 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +0 -0
- package/dist/patterns.js.map +1 -1
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +17 -3
- package/dist/recommend.js.map +1 -1
- package/dist/statusline.d.ts +3 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +2 -0
- package/dist/statusline.js.map +1 -1
- package/dist/usage.d.ts +70 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +255 -0
- package/dist/usage.js.map +1 -0
- package/dist/vector-tier.d.ts +15 -0
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +101 -19
- package/dist/vector-tier.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/golden-baseline.test.ts +67 -0
- package/src/agentdb-index.ts +52 -1
- package/src/brain.ts +127 -4
- package/src/feature-adr-routing.ts +131 -8
- package/src/index.ts +15 -4
- package/src/learning-backend.ts +202 -0
- package/src/patterns.ts +0 -0
- package/src/recommend.ts +19 -5
- package/src/statusline.ts +5 -0
- package/src/usage.ts +289 -0
- package/src/vector-tier.ts +116 -16
package/src/brain.ts
CHANGED
|
@@ -207,6 +207,36 @@ export function readBookKus(opts: {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Exact by-kuId FULL-content lookup over the brain's lexical store (`brainBooksPath(home)` by
|
|
212
|
+
* default) — the by-id reader behind `dz brain expand <kuId>` (brain-ground-expand Tier 1) and the
|
|
213
|
+
* worth-enrichment pass of the budgeted-eager grounding path. Reuses the audited {@link readBookKus}
|
|
214
|
+
* primitive (shared require-path resolution, schema detection, error surfacing), then filters by
|
|
215
|
+
* `kuId`, returning the reconstructed {@link BookKU} with its whole `content` (never a snippet).
|
|
216
|
+
*
|
|
217
|
+
* Best-effort and deterministic: a missing store, unknown kuId, or dependency error returns
|
|
218
|
+
* `{ error }`, never throws. `source` narrows the scan to one book slug (saves a full-table scan on a
|
|
219
|
+
* large brain); omit to search all sources.
|
|
220
|
+
*/
|
|
221
|
+
export function expandKu(opts: {
|
|
222
|
+
kuId: string;
|
|
223
|
+
brainHome?: string;
|
|
224
|
+
depsRoot?: string;
|
|
225
|
+
source?: string;
|
|
226
|
+
}): { ku?: BookKU; error?: string } {
|
|
227
|
+
const home = opts.brainHome ?? brainHome();
|
|
228
|
+
const storePath = brainBooksPath(home);
|
|
229
|
+
const res = readBookKus({
|
|
230
|
+
storePath,
|
|
231
|
+
...(opts.depsRoot !== undefined ? { depsRoot: opts.depsRoot } : {}),
|
|
232
|
+
...(opts.source !== undefined ? { source: opts.source } : {}),
|
|
233
|
+
});
|
|
234
|
+
if (res.error !== undefined) return { error: res.error };
|
|
235
|
+
const ku = res.kus.find((k) => k.kuId === opts.kuId);
|
|
236
|
+
if (ku === undefined) return { error: `kuId not found: ${opts.kuId}` };
|
|
237
|
+
return { ku };
|
|
238
|
+
}
|
|
239
|
+
|
|
210
240
|
/**
|
|
211
241
|
* The vector primitive is insert-only, so before re-indexing a book we delete its existing brain
|
|
212
242
|
* vector rows — mirroring the reindex approach. Best-effort: a missing store/table is a no-op.
|
|
@@ -1077,11 +1107,36 @@ function snippet(s: string, max = 160): string {
|
|
|
1077
1107
|
return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
|
|
1078
1108
|
}
|
|
1079
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* Approximate token count for `s`. ASCII/Latin ≈ chars/4, but CYRILLIC — the brain's primary KU
|
|
1112
|
+
* language — costs ~2 tokens/char under multilingual tokenizers, so a plain chars/4 UNDERCOUNTS
|
|
1113
|
+
* Russian content and lets `--budget N` OVERSHOOT the real token budget. We count Cyrillic at chars/2
|
|
1114
|
+
* and the rest at chars/4; OVERESTIMATING is the safe direction for a soft ceiling (inlines slightly
|
|
1115
|
+
* less, never more). Used only by the budgeted-eager grounding path; never on the default hot path.
|
|
1116
|
+
*/
|
|
1117
|
+
function approxTokens(s: string): number {
|
|
1118
|
+
const cyrillic = (s.match(/[Ѐ-ӿ]/g) || []).length;
|
|
1119
|
+
return Math.ceil((s.length - cyrillic) / 4 + cyrillic / 2);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1080
1122
|
/** The directive line prepended to every grounding block (§7.1). */
|
|
1081
1123
|
const GROUNDING_DIRECTIVE =
|
|
1082
1124
|
'Ground your answer in the KNOWLEDGE BRAIN below; prefer these ingested sources over ' +
|
|
1083
1125
|
'training-data recall; cite [Kn] source+page per claim; if the brain is silent on a point, say so.';
|
|
1084
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Expand-capable variant of the grounding directive (brain-ground-expand Tier 1/2). Used ONLY when
|
|
1129
|
+
* `contentBudget > 0`; the default (no-budget) path keeps {@link GROUNDING_DIRECTIVE} byte-identical.
|
|
1130
|
+
* Augments the base directive with a model instruction: each citation carries its `kuId` as the first
|
|
1131
|
+
* field, and the model can pull a KU's FULL content ON DEMAND by running `dz brain expand <kuId>` —
|
|
1132
|
+
* so it expands only the KUs it actually needs (latency paid only on real use).
|
|
1133
|
+
*/
|
|
1134
|
+
const GROUNDING_DIRECTIVE_EXPAND =
|
|
1135
|
+
'Ground your answer in the KNOWLEDGE BRAIN below; prefer these ingested sources over ' +
|
|
1136
|
+
'training-data recall; cite [Kn] source+page per claim; if the brain is silent on a point, say so. ' +
|
|
1137
|
+
"Each citation shows its kuId as the first field — to read a KU's FULL content on demand, run " +
|
|
1138
|
+
'`dz brain expand <kuId>`; expand only the KUs you actually need.';
|
|
1139
|
+
|
|
1085
1140
|
/**
|
|
1086
1141
|
* The grounding-enforcement hook entrypoint (ADR-001 §7, P1). Given a user prompt, deterministically
|
|
1087
1142
|
* builds a GROUNDING DIRECTIVE block from the brain's top lexical hits — the mechanical half of
|
|
@@ -1105,6 +1160,12 @@ export async function groundPrompt(opts: {
|
|
|
1105
1160
|
depsRoot?: string;
|
|
1106
1161
|
k?: number;
|
|
1107
1162
|
source?: string;
|
|
1163
|
+
/**
|
|
1164
|
+
* brain-ground-expand: approximate token budget (chars/4) for EAGERLY inlining full KU `content`
|
|
1165
|
+
* into the block, worth-ranked. `undefined`/`0` ⇒ the byte-identical pointer-only default (Tier 0);
|
|
1166
|
+
* `> 0` ⇒ the expand-capable block (Tier 1/2) with full content inlined for the top-K KUs that fit.
|
|
1167
|
+
*/
|
|
1168
|
+
contentBudget?: number;
|
|
1108
1169
|
}): Promise<{ emitted: boolean; block: string; hitCount: number; error?: string }> {
|
|
1109
1170
|
const silent = { emitted: false, block: '', hitCount: 0 } as const;
|
|
1110
1171
|
try {
|
|
@@ -1158,13 +1219,75 @@ export async function groundPrompt(opts: {
|
|
|
1158
1219
|
if (covered < 2 && semanticHits.length === 0) return silent;
|
|
1159
1220
|
}
|
|
1160
1221
|
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1222
|
+
// ── Tier 0: DEFAULT (backward-compatible pointer block) ────────────────────────────────────
|
|
1223
|
+
// No budget ⇒ the exact block emitted before brain-ground-expand, byte-identical (property a).
|
|
1224
|
+
// `!opts.contentBudget` covers both `undefined` and `0` (--budget 0 ≡ no budget, FR-03.6).
|
|
1225
|
+
if (!opts.contentBudget) {
|
|
1226
|
+
// Build the GROUNDING DIRECTIVE block (§7.1): directive line + numbered citations.
|
|
1227
|
+
const lines: string[] = [GROUNDING_DIRECTIVE, ''];
|
|
1228
|
+
merged.forEach((h, i) => {
|
|
1229
|
+
const ch = h.chapter !== undefined && h.chapter !== '' ? ` гл.${h.chapter}` : '';
|
|
1230
|
+
const pg = h.pages !== undefined && h.pages.length > 0 ? ` с.${h.pages.join('–')}` : '';
|
|
1231
|
+
const body = snippet(h.problem !== '' ? h.problem : h.content);
|
|
1232
|
+
lines.push(`[K${i + 1}] ${h.book}${ch}${pg} — ${h.name}: ${body}`);
|
|
1233
|
+
});
|
|
1234
|
+
return { emitted: true, block: lines.join('\n'), hitCount: merged.length };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ── Tier 1/2: BUDGETED EAGER (contentBudget > 0) — inside the try/catch so any bug in ranking /
|
|
1238
|
+
// budgeting falls back to `silent` and NEVER errors the prompt (property e). Content is already
|
|
1239
|
+
// in-hand on every hit; budgeting only chooses what to RENDER (no extra retrieval).
|
|
1240
|
+
|
|
1241
|
+
// Enrich hits with declared worth for ranking — BookKUHit drops `metadata`, so recover it by a
|
|
1242
|
+
// by-kuId lookup. HONEST PERF NOTE: each expandKu re-reads the books store, so this is k FULL-store
|
|
1243
|
+
// reads (NOT a metadata-only pass) — fine at the top-K scale here (k≤~5, small brain); if k grows,
|
|
1244
|
+
// pre-load readBookKus once into a kuId→metadata map. Absent worth ⇒ worthScore's 0.5 default.
|
|
1245
|
+
type RankedHit = { hit: BookKUHit; worth: number; rank: number };
|
|
1246
|
+
const rankedHits: RankedHit[] = merged.map((h, i) => {
|
|
1247
|
+
const expanded = expandKu({
|
|
1248
|
+
kuId: h.kuId,
|
|
1249
|
+
...(opts.brainHome !== undefined ? { brainHome: opts.brainHome } : {}),
|
|
1250
|
+
...(opts.depsRoot !== undefined ? { depsRoot: opts.depsRoot } : {}),
|
|
1251
|
+
...(opts.source !== undefined ? { source: opts.source } : {}),
|
|
1252
|
+
});
|
|
1253
|
+
const worth = expanded.ku !== undefined ? worthScore(expanded.ku.metadata ?? {}) : 0.5;
|
|
1254
|
+
return { hit: h, worth, rank: i }; // rank = merge/rerank position (lower = more on-point)
|
|
1255
|
+
});
|
|
1256
|
+
// Rank: worth desc → similarity/rerank-order (rank) asc → kuId asc (stable, deterministic).
|
|
1257
|
+
rankedHits.sort((a, b) => b.worth - a.worth || a.rank - b.rank || a.hit.kuId.localeCompare(b.hit.kuId));
|
|
1258
|
+
|
|
1259
|
+
// Greedy budget fill: inline a KU's full content ONLY if it fits the remaining budget; the first
|
|
1260
|
+
// KU that would overflow stops the fill (property c: hard ceiling, atomic KUs — never partial).
|
|
1261
|
+
let tokensBudgeted = 0;
|
|
1262
|
+
const inlineSet = new Set<string>();
|
|
1263
|
+
for (const { hit } of rankedHits) {
|
|
1264
|
+
if (hit.content === '') continue; // empty content → stays a pointer (zero-token)
|
|
1265
|
+
const cost = approxTokens(hit.content);
|
|
1266
|
+
if (tokensBudgeted + cost <= opts.contentBudget) {
|
|
1267
|
+
inlineSet.add(hit.kuId);
|
|
1268
|
+
tokensBudgeted += cost;
|
|
1269
|
+
} else {
|
|
1270
|
+
break; // stop at first overflow (stop-don't-skip policy, FR-03.4)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Emit the expand-capable block: directive names `dz brain expand <kuId>`; every citation exposes
|
|
1275
|
+
// its kuId (property b). Chosen KUs get full content inlined; the rest stay pointers.
|
|
1276
|
+
const lines: string[] = [GROUNDING_DIRECTIVE_EXPAND, ''];
|
|
1163
1277
|
merged.forEach((h, i) => {
|
|
1164
1278
|
const ch = h.chapter !== undefined && h.chapter !== '' ? ` гл.${h.chapter}` : '';
|
|
1165
1279
|
const pg = h.pages !== undefined && h.pages.length > 0 ? ` с.${h.pages.join('–')}` : '';
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1280
|
+
const header = `[K${i + 1}] ${h.kuId} · ${h.book}${ch}${pg} — ${h.name}`;
|
|
1281
|
+
if (inlineSet.has(h.kuId) && h.content !== '') {
|
|
1282
|
+
// Inlined full content (budget allocated) — content supersedes the snippet suffix.
|
|
1283
|
+
lines.push(header);
|
|
1284
|
+
lines.push(h.content);
|
|
1285
|
+
lines.push(''); // blank separator between KUs for readability
|
|
1286
|
+
} else {
|
|
1287
|
+
// Pointer with kuId annotation; the model can expand this one on demand.
|
|
1288
|
+
const body = snippet(h.problem !== '' ? h.problem : h.content);
|
|
1289
|
+
lines.push(`${header}: ${body}`);
|
|
1290
|
+
}
|
|
1168
1291
|
});
|
|
1169
1292
|
return { emitted: true, block: lines.join('\n'), hitCount: merged.length };
|
|
1170
1293
|
} catch {
|
|
@@ -33,6 +33,12 @@ export interface StageOpts {
|
|
|
33
33
|
readonly agentType?: string;
|
|
34
34
|
readonly codexModel?: string;
|
|
35
35
|
readonly _reasoning?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Observability marker set ONLY when the usage-adaptive override chose this stage's codex spec
|
|
38
|
+
* (not on a user's explicit codex routing). `modelLabel` appends ` (usage-switched)` when present
|
|
39
|
+
* so the switch is auditable in `modelsUsed` (AC-6).
|
|
40
|
+
*/
|
|
41
|
+
_usageSwitched?: boolean;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** The knobs `resolveStageModel` closes over — passed in from the workflow. */
|
|
@@ -55,6 +61,124 @@ export interface RoutingEnv {
|
|
|
55
61
|
readonly codexAvailable?: boolean;
|
|
56
62
|
/** Optional log sink (the workflow passes its `log`); defaults to a no-op. */
|
|
57
63
|
readonly log?: (msg: string) => void;
|
|
64
|
+
/**
|
|
65
|
+
* USAGE-ADAPTIVE override bit (env-threaded, non-global — LOCKED L-5). When true,
|
|
66
|
+
* `resolveStageModel` routes ALL stages to `codex:<topCodexId>` REGARDLESS of `MODELS`/knobs.
|
|
67
|
+
* The single mutable `let usageOverride` lives ONLY in the workflow script; the library stays
|
|
68
|
+
* pure — the state travels through THIS field, never a module-level global. Absent/undefined ⇒
|
|
69
|
+
* byte-identical resolution to the pre-feature behavior (NFR-2 / AC-4).
|
|
70
|
+
*/
|
|
71
|
+
readonly usageOverride?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Per-stage reasoning for the usage-override spec (merged OVER {@link OVERRIDE_REASONING}).
|
|
74
|
+
* `args.usageReasoning` — stage → `'high'|'xhigh'|…`; a single stage may be overridden without
|
|
75
|
+
* touching the others.
|
|
76
|
+
*/
|
|
77
|
+
readonly usageReasoning?: Record<string, string>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── USAGE-ADAPTIVE ROUTING (pre-emptive codex switch at >= usageThreshold) ────
|
|
81
|
+
|
|
82
|
+
/** A probe reading. `null` on a pct ⇔ that limit is unconfigured (unknown — never a guess). */
|
|
83
|
+
export interface UsageSignal {
|
|
84
|
+
readonly sessionPct: number | null;
|
|
85
|
+
readonly weeklyPct: number | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The LOCKED 6-value action vocabulary (L-1). `decideUsageAction` emits the first five (probe
|
|
90
|
+
* path); `'reactive-switch'` is pushed only by the workflow's belt sites — one shared type, no
|
|
91
|
+
* parallel enums.
|
|
92
|
+
*/
|
|
93
|
+
export type UsageAction =
|
|
94
|
+
| 'none'
|
|
95
|
+
| 'switch'
|
|
96
|
+
| 'restore'
|
|
97
|
+
| 'keep'
|
|
98
|
+
| 'fail-safe-switch'
|
|
99
|
+
| 'reactive-switch';
|
|
100
|
+
|
|
101
|
+
/** The `decideUsageAction` verdict: the new override bit + the event action. */
|
|
102
|
+
export interface UsageDecision {
|
|
103
|
+
readonly override: boolean;
|
|
104
|
+
readonly action: UsageAction;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Per-stage reasoning applied under the usage override (LOCKED L-4, arch §4.1 verbatim):
|
|
109
|
+
* design/code/plan stages ⇒ `xhigh`; router/qe/fleet ⇒ `high`. A pure DATA table (not control
|
|
110
|
+
* flow); user-overridable via `env.usageReasoning`. An unknown stage falls back to `'high'`.
|
|
111
|
+
*/
|
|
112
|
+
export const OVERRIDE_REASONING: Record<string, 'high' | 'xhigh'> = {
|
|
113
|
+
router: 'high',
|
|
114
|
+
requirements: 'xhigh',
|
|
115
|
+
research: 'xhigh',
|
|
116
|
+
adr: 'xhigh',
|
|
117
|
+
ideation: 'xhigh',
|
|
118
|
+
ddd: 'xhigh',
|
|
119
|
+
architecture: 'xhigh',
|
|
120
|
+
plan: 'xhigh',
|
|
121
|
+
code: 'xhigh',
|
|
122
|
+
qe: 'high',
|
|
123
|
+
fleet: 'high',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The gpt-5.6-ready TOP codex-id pick — SHARED by the cross-model QE default and the usage
|
|
128
|
+
* override so adding an id to {@link KNOWN_CODEX} (e.g. `gpt-5.7`) retargets BOTH with zero
|
|
129
|
+
* control-flow diff (AC-3). Pinned `CODEX_MODEL` when ≠ `'auto'`; else the last non-`auto` key of
|
|
130
|
+
* `KNOWN_CODEX`.
|
|
131
|
+
*/
|
|
132
|
+
export function topCodexId(env: RoutingEnv): string {
|
|
133
|
+
let top = env.CODEX_MODEL;
|
|
134
|
+
if (top === 'auto') {
|
|
135
|
+
const ids = Object.keys(KNOWN_CODEX);
|
|
136
|
+
for (let i = 0; i < ids.length; i++) {
|
|
137
|
+
if (ids[i] !== 'auto') top = ids[i] || top;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return top;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The PURE hysteresis core (the load-bearing safety property, AC-1). Given the previous override
|
|
145
|
+
* bit, a probe signal (or `null` when the probe agent itself DIED), and the threshold, decide the
|
|
146
|
+
* new override bit + the event action. Total function — no input throws or returns an
|
|
147
|
+
* out-of-vocabulary action.
|
|
148
|
+
*
|
|
149
|
+
* The load-bearing asymmetry (INV-2):
|
|
150
|
+
* - **agent-null** (`signal === null`, the probe dispatch died — OFTEN MEANS limits) ⇒ fail-safe
|
|
151
|
+
* switch TO codex (OFF→ON), or `keep` if already overridden.
|
|
152
|
+
* - **value-null** (a pct is `null` — limits unconfigured / garbled) ⇒ flips NOTHING: never
|
|
153
|
+
* switches a fresh run to codex AND never restores an active override (unknown ⇒ hysteresis, no
|
|
154
|
+
* flapping).
|
|
155
|
+
* - `>= threshold` on EITHER known metric ⇒ switch (OFF→ON) / keep (ON stays ON). Boundary `= 70`
|
|
156
|
+
* counts as over (`>=`).
|
|
157
|
+
* - ONLY a positive BOTH-below reading (both pcts known and `< threshold`) clears an override
|
|
158
|
+
* (`restore`); from OFF it is `none`.
|
|
159
|
+
*
|
|
160
|
+
* Non-finite pcts (`NaN`, negatives from a garbled probe) are treated as value-null.
|
|
161
|
+
*/
|
|
162
|
+
export function decideUsageAction(
|
|
163
|
+
prevOverride: boolean,
|
|
164
|
+
signal: UsageSignal | null,
|
|
165
|
+
threshold: number,
|
|
166
|
+
): UsageDecision {
|
|
167
|
+
if (signal === null || signal === undefined) {
|
|
168
|
+
if (prevOverride) return { override: true, action: 'keep' };
|
|
169
|
+
return { override: true, action: 'fail-safe-switch' };
|
|
170
|
+
}
|
|
171
|
+
const s = signal.sessionPct;
|
|
172
|
+
const w = signal.weeklyPct;
|
|
173
|
+
const sKnown = typeof s === 'number' && isFinite(s) && s >= 0;
|
|
174
|
+
const wKnown = typeof w === 'number' && isFinite(w) && w >= 0;
|
|
175
|
+
if ((sKnown && s >= threshold) || (wKnown && w >= threshold)) {
|
|
176
|
+
return { override: true, action: prevOverride ? 'keep' : 'switch' };
|
|
177
|
+
}
|
|
178
|
+
if (sKnown && wKnown) {
|
|
179
|
+
return { override: false, action: prevOverride ? 'restore' : 'none' };
|
|
180
|
+
}
|
|
181
|
+
return { override: prevOverride, action: prevOverride ? 'keep' : 'none' };
|
|
58
182
|
}
|
|
59
183
|
|
|
60
184
|
// ── Data tables (data-only extensibility — gpt-5.6-ready) ───────────────────
|
|
@@ -147,14 +271,7 @@ export function resolveQeSpec(env: RoutingEnv): string {
|
|
|
147
271
|
if (coderIsCodex(env)) return 'opus';
|
|
148
272
|
const CODEX_AVAILABLE = env.codexAvailable !== false;
|
|
149
273
|
if (!CODEX_AVAILABLE) return 'opus';
|
|
150
|
-
|
|
151
|
-
if (top === 'auto') {
|
|
152
|
-
const ids = Object.keys(KNOWN_CODEX);
|
|
153
|
-
for (let i = 0; i < ids.length; i++) {
|
|
154
|
-
if (ids[i] !== 'auto') top = ids[i] || top;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return 'codex:' + top + ':high';
|
|
274
|
+
return 'codex:' + topCodexId(env) + ':high';
|
|
158
275
|
}
|
|
159
276
|
|
|
160
277
|
/**
|
|
@@ -201,6 +318,12 @@ export function qeShouldUseCodex(env: RoutingEnv): boolean {
|
|
|
201
318
|
* 4. `code`/`qe` `null` sentinels resolve via the coder / cross-model rules
|
|
202
319
|
*/
|
|
203
320
|
export function resolveStageModel(stage: string, env: RoutingEnv): StageOpts {
|
|
321
|
+
if (env.usageOverride) {
|
|
322
|
+
const r = (env.usageReasoning && env.usageReasoning[stage]) || OVERRIDE_REASONING[stage] || 'high';
|
|
323
|
+
const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);
|
|
324
|
+
o._usageSwitched = true;
|
|
325
|
+
return o;
|
|
326
|
+
}
|
|
204
327
|
let spec = env.MODELS[stage];
|
|
205
328
|
if (spec === undefined) {
|
|
206
329
|
if (!routingRequested(env)) return {};
|
package/src/index.ts
CHANGED
|
@@ -27,11 +27,14 @@ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
|
27
27
|
export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs } from './registry.js';
|
|
28
28
|
export { recommend } from './recommend.js';
|
|
29
29
|
export { pretrain } from './pretrain.js';
|
|
30
|
-
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, loadStoreRecords, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore } from './patterns.js';
|
|
31
|
-
export type { PatternRecord, SessionRecord, LearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult } from './patterns.js';
|
|
30
|
+
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, loadStoreRecords, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats } from './patterns.js';
|
|
31
|
+
export type { PatternRecord, SessionRecord, LearningConfig, MemoryLearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult, RemovePatternsResult, SnapshotStoreResult, ReinforcementState, ReinforcePatternResult, StoreStats } from './patterns.js';
|
|
32
|
+
export { DEFAULT_REINFORCE_THRESHOLD, NoopLearningBackend, NativeReinforcementBackend, resolveLearningBackend, isLearningSignalBackend } from './learning-backend.js';
|
|
33
|
+
export type { LearningSignalBackend, LearningSignalStats, LearningSample, SignalCandidate, EnhanceContext, TrainingResult, LearningBackendMode } from './learning-backend.js';
|
|
32
34
|
export {
|
|
33
35
|
DEFAULT_VECTOR_TIMEOUT_MS,
|
|
34
36
|
DEFAULT_HARMONIZE_THRESHOLD,
|
|
37
|
+
REINFORCE_RRF_CAP,
|
|
35
38
|
withVectorTimeout,
|
|
36
39
|
isVectorNoise,
|
|
37
40
|
patternVectorEntry,
|
|
@@ -46,6 +49,7 @@ export {
|
|
|
46
49
|
backfillVectorMirror,
|
|
47
50
|
mergeHybridHits,
|
|
48
51
|
recallHybrid,
|
|
52
|
+
teachGuard,
|
|
49
53
|
vectorTierStatus,
|
|
50
54
|
reindexVectorStore,
|
|
51
55
|
harmonizeVectorStore,
|
|
@@ -74,11 +78,12 @@ export type {
|
|
|
74
78
|
ImportReport,
|
|
75
79
|
ImportOptions,
|
|
76
80
|
ReindexVectorReport,
|
|
81
|
+
TeachGuardResult,
|
|
77
82
|
} from './vector-tier.js';
|
|
78
83
|
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
|
|
79
84
|
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStatePath } from './statusline.js';
|
|
80
85
|
export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
|
|
81
|
-
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows } from './agentdb-index.js';
|
|
86
|
+
export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses } from './agentdb-index.js';
|
|
82
87
|
export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
|
|
83
88
|
export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
|
|
84
89
|
export type { EmbedModelConfig, EmbedModelSource, EmbedManifest } from './embedding-config.js';
|
|
@@ -99,6 +104,7 @@ export {
|
|
|
99
104
|
reindexBrainVectors,
|
|
100
105
|
rerankHits,
|
|
101
106
|
groundPrompt,
|
|
107
|
+
expandKu,
|
|
102
108
|
buildPrimer,
|
|
103
109
|
writePrimer,
|
|
104
110
|
readBookKus,
|
|
@@ -178,5 +184,10 @@ export {
|
|
|
178
184
|
DEFAULT_MODELS,
|
|
179
185
|
KNOWN_CODEX,
|
|
180
186
|
CLAUDE_NAMES,
|
|
187
|
+
topCodexId,
|
|
188
|
+
decideUsageAction,
|
|
189
|
+
OVERRIDE_REASONING,
|
|
181
190
|
} from './feature-adr-routing.js';
|
|
182
|
-
export type { StageOpts, RoutingEnv } from './feature-adr-routing.js';
|
|
191
|
+
export type { StageOpts, RoutingEnv, UsageSignal, UsageAction } from './feature-adr-routing.js';
|
|
192
|
+
export { computeUsage, readUsageLimits } from './usage.js';
|
|
193
|
+
export type { UsageEstimate, UsageLimits } from './usage.js';
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
readMemoryLearningConfig,
|
|
5
|
+
readReinforcementState,
|
|
6
|
+
reinforcePattern,
|
|
7
|
+
type ReinforcementState,
|
|
8
|
+
type MemoryLearningConfig,
|
|
9
|
+
} from './patterns.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_REINFORCE_THRESHOLD = 0.95;
|
|
12
|
+
|
|
13
|
+
export type LearningBackendMode = 'native' | 'off' | 'ruvector-gnn';
|
|
14
|
+
export type LearningSampleKind = 'recall-hit' | 'reinforce' | 'merge';
|
|
15
|
+
|
|
16
|
+
export interface SignalCandidate {
|
|
17
|
+
readonly dzId: string;
|
|
18
|
+
readonly score: number;
|
|
19
|
+
readonly reinforcement?: ReinforcementState | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EnhanceContext {
|
|
23
|
+
readonly kind: 'recall' | 'recommend';
|
|
24
|
+
readonly now?: number;
|
|
25
|
+
readonly cap?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LearningSample {
|
|
29
|
+
readonly dzId: string;
|
|
30
|
+
readonly kind: LearningSampleKind;
|
|
31
|
+
readonly reward?: number;
|
|
32
|
+
readonly ts: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TrainingResult {
|
|
36
|
+
readonly trained: boolean;
|
|
37
|
+
readonly flushed: number;
|
|
38
|
+
readonly failed: number;
|
|
39
|
+
readonly error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LearningSignalStats {
|
|
43
|
+
readonly enabled: boolean;
|
|
44
|
+
readonly backend: string;
|
|
45
|
+
readonly samplesCollected: number;
|
|
46
|
+
readonly lastTrainingTime: number | null;
|
|
47
|
+
readonly flushedTotal: number;
|
|
48
|
+
readonly failedTotal: number;
|
|
49
|
+
readonly advisory?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LearningSignalBackend {
|
|
53
|
+
enhance(candidates: readonly SignalCandidate[], ctx: EnhanceContext): Float32Array;
|
|
54
|
+
addSample(sample: LearningSample): void;
|
|
55
|
+
train(opts?: { readonly maxMs?: number }): Promise<TrainingResult>;
|
|
56
|
+
clearSamples(): void;
|
|
57
|
+
saveModel(path: string): Promise<void>;
|
|
58
|
+
loadModel(path: string): Promise<void>;
|
|
59
|
+
getStats(): LearningSignalStats;
|
|
60
|
+
reset(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isLearningSignalBackend(v: unknown): v is LearningSignalBackend {
|
|
64
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
65
|
+
const o = v as Record<string, unknown>;
|
|
66
|
+
return ['enhance', 'addSample', 'train', 'clearSamples', 'saveModel', 'loadModel', 'getStats', 'reset']
|
|
67
|
+
.every((k) => typeof o[k] === 'function');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class NoopLearningBackend implements LearningSignalBackend {
|
|
71
|
+
enhance(candidates: readonly SignalCandidate[]): Float32Array {
|
|
72
|
+
return new Float32Array(candidates.length);
|
|
73
|
+
}
|
|
74
|
+
addSample(): void { /* no-op kill switch */ }
|
|
75
|
+
async train(): Promise<TrainingResult> { return { trained: false, flushed: 0, failed: 0 }; }
|
|
76
|
+
clearSamples(): void { /* no-op */ }
|
|
77
|
+
async saveModel(path: string): Promise<void> {
|
|
78
|
+
writeFileSync(path, JSON.stringify({ backend: 'off', note: 'NoopLearningBackend has no model state' }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
async loadModel(): Promise<void> { /* no-op */ }
|
|
81
|
+
getStats(): LearningSignalStats {
|
|
82
|
+
return { enabled: false, backend: 'off', samplesCollected: 0, lastTrainingTime: null, flushedTotal: 0, failedTotal: 0 };
|
|
83
|
+
}
|
|
84
|
+
reset(): void { /* no-op */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class NativeReinforcementBackend implements LearningSignalBackend {
|
|
88
|
+
private readonly samples: LearningSample[] = [];
|
|
89
|
+
private flushedTotal = 0;
|
|
90
|
+
private failedTotal = 0;
|
|
91
|
+
private lastTrainingTime: number | null = null;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
private readonly projectRoot: string,
|
|
95
|
+
private readonly opts: { readonly usesSat: number; readonly halfLifeDays: number; readonly advisory?: string } = { usesSat: 64, halfLifeDays: 30 },
|
|
96
|
+
) {}
|
|
97
|
+
|
|
98
|
+
enhance(candidates: readonly SignalCandidate[], ctx: EnhanceContext): Float32Array {
|
|
99
|
+
const out = new Float32Array(candidates.length);
|
|
100
|
+
const now = ctx.now ?? Date.now();
|
|
101
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
102
|
+
const st = candidates[i]!.reinforcement;
|
|
103
|
+
out[i] = st === undefined ? 0 : this.signal(st, now);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
addSample(sample: LearningSample): void {
|
|
109
|
+
this.samples.push(sample);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async train(): Promise<TrainingResult> {
|
|
113
|
+
const batch = this.samples.splice(0);
|
|
114
|
+
let flushed = 0;
|
|
115
|
+
let failed = 0;
|
|
116
|
+
let error: string | undefined;
|
|
117
|
+
for (const sample of batch) {
|
|
118
|
+
const r = await reinforcePattern(this.projectRoot, sample.dzId, {
|
|
119
|
+
ts: sample.ts,
|
|
120
|
+
...(sample.reward !== undefined ? { reward: sample.reward } : {}),
|
|
121
|
+
});
|
|
122
|
+
if (r.ok) flushed += 1;
|
|
123
|
+
else {
|
|
124
|
+
failed += 1;
|
|
125
|
+
error = r.error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.flushedTotal += flushed;
|
|
129
|
+
this.failedTotal += failed;
|
|
130
|
+
this.lastTrainingTime = Date.now();
|
|
131
|
+
return { trained: batch.length > 0, flushed, failed, ...(error !== undefined ? { error } : {}) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clearSamples(): void {
|
|
135
|
+
this.samples.splice(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async saveModel(path: string): Promise<void> {
|
|
139
|
+
writeFileSync(path, JSON.stringify({ backend: 'native', note: 'state lives in .dz/memory records' }, null, 2));
|
|
140
|
+
}
|
|
141
|
+
async loadModel(): Promise<void> { /* native state lives in the store */ }
|
|
142
|
+
|
|
143
|
+
getStats(): LearningSignalStats {
|
|
144
|
+
return {
|
|
145
|
+
enabled: true,
|
|
146
|
+
backend: 'native',
|
|
147
|
+
samplesCollected: this.samples.length,
|
|
148
|
+
lastTrainingTime: this.lastTrainingTime,
|
|
149
|
+
flushedTotal: this.flushedTotal,
|
|
150
|
+
failedTotal: this.failedTotal,
|
|
151
|
+
...(this.opts.advisory !== undefined ? { advisory: this.opts.advisory } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
reset(): void {
|
|
156
|
+
this.samples.splice(0);
|
|
157
|
+
this.flushedTotal = 0;
|
|
158
|
+
this.failedTotal = 0;
|
|
159
|
+
this.lastTrainingTime = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private signal(state: ReinforcementState, now: number): number {
|
|
163
|
+
if (state.uses <= 0) return 0;
|
|
164
|
+
const usesSat = Math.max(2, this.opts.usesSat);
|
|
165
|
+
const freq = Math.min(1, Math.log1p(state.uses) / Math.log1p(usesSat));
|
|
166
|
+
const t = state.lastUsedTs !== undefined ? Date.parse(state.lastUsedTs) : Number.NaN;
|
|
167
|
+
const halfLifeMs = Math.max(1, this.opts.halfLifeDays) * 86_400_000;
|
|
168
|
+
const age = Number.isFinite(t) ? Math.max(0, now - t) : halfLifeMs;
|
|
169
|
+
const recency = 0.5 + 0.5 * Math.exp(-age / halfLifeMs);
|
|
170
|
+
return Math.max(0, Math.min(1, freq * recency));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function resolveLearningBackend(projectRoot: string, config: MemoryLearningConfig = readMemoryLearningConfig(projectRoot)): LearningSignalBackend {
|
|
175
|
+
try {
|
|
176
|
+
const cfg = config;
|
|
177
|
+
if (cfg.backend === 'off') return new NoopLearningBackend();
|
|
178
|
+
const advisory = cfg.backend === 'ruvector-gnn'
|
|
179
|
+
? 'memory.learning.backend="ruvector-gnn" is reserved; falling back to native reinforcement'
|
|
180
|
+
: undefined;
|
|
181
|
+
return new NativeReinforcementBackend(projectRoot, {
|
|
182
|
+
usesSat: cfg.usesSat,
|
|
183
|
+
halfLifeDays: cfg.halfLifeDays,
|
|
184
|
+
...(advisory !== undefined ? { advisory } : {}),
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
return new NativeReinforcementBackend(projectRoot);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function applyLearningSignals<H extends { readonly score: number }>(
|
|
192
|
+
hits: readonly H[],
|
|
193
|
+
backend: LearningSignalBackend,
|
|
194
|
+
candidates: readonly SignalCandidate[],
|
|
195
|
+
cap: number,
|
|
196
|
+
): H[] {
|
|
197
|
+
const signals = backend.enhance(candidates, { kind: 'recall', cap });
|
|
198
|
+
return hits
|
|
199
|
+
.map((hit, i) => ({ hit, adjusted: hit.score + cap * (signals[i] ?? 0), i }))
|
|
200
|
+
.sort((a, b) => b.adjusted - a.adjusted || a.i - b.i)
|
|
201
|
+
.map((x) => x.hit);
|
|
202
|
+
}
|
package/src/patterns.ts
CHANGED
|
Binary file
|
package/src/recommend.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import type { Registry, RegistryEntry } from './registry.js';
|
|
17
17
|
import { pretrain } from './pretrain.js';
|
|
18
|
-
import { loadPatterns,
|
|
18
|
+
import { computePatternBoost, loadPatterns, loadStoreRecords, readLearningConfig, readReinforcementState, recordToPattern } from './patterns.js';
|
|
19
|
+
import { resolveLearningBackend } from './learning-backend.js';
|
|
19
20
|
|
|
20
21
|
/** A recommended skill with relevance score. */
|
|
21
22
|
export interface SkillRecommendation {
|
|
@@ -202,21 +203,34 @@ export function recommend(task: string, registry: Registry, projectRoot?: string
|
|
|
202
203
|
// bounded, monotonic boost. Gated on the rollout flag; when memory is empty or
|
|
203
204
|
// the flag is off, `patterns` is [] and the boost is 0 — ranking stays
|
|
204
205
|
// byte-identical to the pure keyword scoring (the graceful invariant, R3).
|
|
205
|
-
const
|
|
206
|
-
|
|
206
|
+
const records = projectRoot && readLearningConfig(projectRoot).recommendBoost ? loadStoreRecords(projectRoot) : [];
|
|
207
|
+
const patterns = projectRoot && readLearningConfig(projectRoot).recommendBoost ? loadPatterns(projectRoot) : [];
|
|
208
|
+
const backend = projectRoot !== undefined ? resolveLearningBackend(projectRoot) : undefined;
|
|
209
|
+
const boostFor = (entry: RegistryEntry): number => {
|
|
210
|
+
const base = computePatternBoost(entry.id, entry.description, patterns);
|
|
211
|
+
if (base <= 0 || backend === undefined) return base;
|
|
212
|
+
const matched = records.filter((r) => computePatternBoost(entry.id, entry.description, [recordToPattern(r)]) > 0);
|
|
213
|
+
if (matched.length === 0) return base;
|
|
214
|
+
const signals = backend.enhance(
|
|
215
|
+
matched.map((r) => ({ dzId: r.id, score: r.score, reinforcement: readReinforcementState(r) })),
|
|
216
|
+
{ kind: 'recommend' },
|
|
217
|
+
);
|
|
218
|
+
const maxSignal = Math.max(0, ...signals);
|
|
219
|
+
return Math.min(50, Math.round(base * (1 + 0.25 * maxSignal)));
|
|
220
|
+
};
|
|
207
221
|
|
|
208
222
|
// Score and rank skills
|
|
209
223
|
const scored = registry.entries
|
|
210
224
|
.map((e) => ({
|
|
211
225
|
entry: e,
|
|
212
|
-
score: scoreSkill(e, topics) + (patterns.length ?
|
|
226
|
+
score: scoreSkill(e, topics) + (patterns.length ? boostFor(e) : 0),
|
|
213
227
|
}))
|
|
214
228
|
.filter((s) => s.score > 0)
|
|
215
229
|
.sort((a, b) => b.score - a.score);
|
|
216
230
|
|
|
217
231
|
const skills: SkillRecommendation[] = scored.slice(0, 10).map((s) => {
|
|
218
232
|
let reason = `Matches topics: ${topics.filter((t) => scoreSkill(s.entry, [t]) > 0).join(', ')}`;
|
|
219
|
-
if (patterns.length &&
|
|
233
|
+
if (patterns.length && boostFor(s.entry) > 0) {
|
|
220
234
|
reason += ' + learned patterns';
|
|
221
235
|
}
|
|
222
236
|
return {
|