@dzhechkov/harness-core 0.3.92 → 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/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 +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- 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/feature-adr-routing.ts +131 -8
- package/src/index.ts +14 -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
|
@@ -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';
|
|
@@ -179,5 +184,10 @@ export {
|
|
|
179
184
|
DEFAULT_MODELS,
|
|
180
185
|
KNOWN_CODEX,
|
|
181
186
|
CLAUDE_NAMES,
|
|
187
|
+
topCodexId,
|
|
188
|
+
decideUsageAction,
|
|
189
|
+
OVERRIDE_REASONING,
|
|
182
190
|
} from './feature-adr-routing.js';
|
|
183
|
-
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 {
|
package/src/statusline.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface FeatureAdrState {
|
|
|
37
37
|
readonly recalled: number;
|
|
38
38
|
/** How many NEW patterns this run STORED back into the pool. */
|
|
39
39
|
readonly stored: number;
|
|
40
|
+
/** How many candidate lessons reinforced an existing pattern instead of writing a duplicate. */
|
|
41
|
+
readonly reinforced?: number;
|
|
40
42
|
/** ISO timestamp of the write — drives the freshness window on the render path. */
|
|
41
43
|
readonly ts: string;
|
|
42
44
|
/** Optional run mode (e.g. "reference", "full-qe", "full-qe-extended"). */
|
|
@@ -181,6 +183,7 @@ export function readFeatureAdrState(projectRoot: string, now: number = Date.now(
|
|
|
181
183
|
pool: num(parsed.pool),
|
|
182
184
|
recalled: num(parsed.recalled),
|
|
183
185
|
stored: num(parsed.stored),
|
|
186
|
+
...(num(parsed.reinforced) > 0 ? { reinforced: num(parsed.reinforced) } : {}),
|
|
184
187
|
ts: parsed.ts,
|
|
185
188
|
...(typeof parsed.mode === 'string' && parsed.mode.length > 0 ? { mode: parsed.mode } : {}),
|
|
186
189
|
};
|
|
@@ -196,6 +199,7 @@ export interface WriteFeatureAdrStateInput {
|
|
|
196
199
|
readonly step: string;
|
|
197
200
|
readonly recalled: number;
|
|
198
201
|
readonly stored: number;
|
|
202
|
+
readonly reinforced?: number;
|
|
199
203
|
readonly mode?: string;
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -225,6 +229,7 @@ export function writeFeatureAdrState(
|
|
|
225
229
|
pool,
|
|
226
230
|
recalled: Number.isFinite(input.recalled) ? input.recalled : 0,
|
|
227
231
|
stored: Number.isFinite(input.stored) ? input.stored : 0,
|
|
232
|
+
...(input.reinforced !== undefined && Number.isFinite(input.reinforced) ? { reinforced: input.reinforced } : {}),
|
|
228
233
|
ts: new Date(now).toISOString(),
|
|
229
234
|
...(input.mode !== undefined && input.mode.length > 0 ? { mode: input.mode } : {}),
|
|
230
235
|
};
|