@soleri/core 9.3.1 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.js +3 -3
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +52 -19
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
- package/dist/planning/task-complexity-assessor.js.map +1 -1
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +100 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +71 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +133 -30
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +38 -1
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +29 -1
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +8 -2
- package/src/engine/module-manifest.ts +3 -3
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/index.ts +22 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/planning/plan-lifecycle.ts +80 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +6 -2
- package/src/planning/task-complexity-assessor.ts +1 -4
- package/src/runtime/admin-ops.test.ts +139 -6
- package/src/runtime/admin-ops.ts +104 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +10 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +202 -2
- package/src/runtime/orchestrate-ops.ts +85 -4
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/types.ts +3 -0
- package/src/skills/sync-skills.ts +14 -7
- package/vitest.config.ts +1 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OperatorContextStore — persistence and compounding for the operator context
|
|
3
|
+
* signal taxonomy.
|
|
4
|
+
*
|
|
5
|
+
* Manages a single SQLite table (`operator_context`) and implements the
|
|
6
|
+
* compounding algorithms that blend per-session signals into a stable profile.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import type { PersistenceProvider } from '../persistence/types.js';
|
|
11
|
+
import {
|
|
12
|
+
DECLINED_CATEGORIES,
|
|
13
|
+
type ContextItemType,
|
|
14
|
+
type CorrectionItem,
|
|
15
|
+
type CorrectionSignal,
|
|
16
|
+
type ExpertiseItem,
|
|
17
|
+
type ExpertiseLevel,
|
|
18
|
+
type ExpertiseSignal,
|
|
19
|
+
type InterestItem,
|
|
20
|
+
type InterestSignal,
|
|
21
|
+
type OperatorContext,
|
|
22
|
+
type OperatorSignals,
|
|
23
|
+
type WorkPatternItem,
|
|
24
|
+
type WorkPatternSignal,
|
|
25
|
+
} from './operator-context-types.js';
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// CONSTANTS
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
const DEFAULT_CONFIDENCE = 0.5;
|
|
32
|
+
|
|
33
|
+
/** Patterns that indicate the user is prohibiting / stopping something. */
|
|
34
|
+
const DONT_PATTERNS = /^(don't|do not|stop|never|no |avoid |quit )/i;
|
|
35
|
+
|
|
36
|
+
/** Patterns that indicate the user is reversing a previous prohibition. */
|
|
37
|
+
const DO_PATTERNS = /^(actually,? ?|you can |feel free |go ahead |it's fine |is fine |start )/i;
|
|
38
|
+
|
|
39
|
+
/** Regex to detect declined-category content in signal text fields. */
|
|
40
|
+
const DECLINED_RE = new RegExp(`\\b(${DECLINED_CATEGORIES.join('|')})\\b`, 'i');
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// STORE
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export class OperatorContextStore {
|
|
47
|
+
private provider: PersistenceProvider;
|
|
48
|
+
private lastRendered: string | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(provider: PersistenceProvider) {
|
|
51
|
+
this.provider = provider;
|
|
52
|
+
this.init();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Initialization ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
init(): void {
|
|
58
|
+
this.provider.execSql(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS operator_context (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
type TEXT NOT NULL,
|
|
62
|
+
key TEXT NOT NULL,
|
|
63
|
+
value TEXT NOT NULL,
|
|
64
|
+
confidence REAL DEFAULT 0.5,
|
|
65
|
+
scope TEXT DEFAULT 'global',
|
|
66
|
+
session_count INTEGER DEFAULT 1,
|
|
67
|
+
last_observed INTEGER NOT NULL,
|
|
68
|
+
created_at INTEGER NOT NULL,
|
|
69
|
+
active INTEGER DEFAULT 1
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_operator_context_type
|
|
73
|
+
ON operator_context(type);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_operator_context_key
|
|
76
|
+
ON operator_context(type, key);
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Read ──────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
getContext(): OperatorContext {
|
|
83
|
+
const rows = this.provider.all<ContextRow>(
|
|
84
|
+
'SELECT * FROM operator_context WHERE active = 1 ORDER BY type, last_observed DESC',
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const expertise: ExpertiseItem[] = [];
|
|
88
|
+
const corrections: CorrectionItem[] = [];
|
|
89
|
+
const interests: InterestItem[] = [];
|
|
90
|
+
const patterns: WorkPatternItem[] = [];
|
|
91
|
+
|
|
92
|
+
for (const row of rows) {
|
|
93
|
+
const parsed = JSON.parse(row.value);
|
|
94
|
+
switch (row.type as ContextItemType) {
|
|
95
|
+
case 'expertise':
|
|
96
|
+
expertise.push(parsed as ExpertiseItem);
|
|
97
|
+
break;
|
|
98
|
+
case 'correction':
|
|
99
|
+
corrections.push(parsed as CorrectionItem);
|
|
100
|
+
break;
|
|
101
|
+
case 'interest':
|
|
102
|
+
interests.push(parsed as InterestItem);
|
|
103
|
+
break;
|
|
104
|
+
case 'pattern':
|
|
105
|
+
patterns.push(parsed as WorkPatternItem);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sessionCountRow = this.provider.get<{ cnt: number }>(
|
|
111
|
+
'SELECT MAX(session_count) as cnt FROM operator_context',
|
|
112
|
+
);
|
|
113
|
+
const lastRow = this.provider.get<{ ts: number }>(
|
|
114
|
+
'SELECT MAX(last_observed) as ts FROM operator_context',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
expertise,
|
|
119
|
+
corrections,
|
|
120
|
+
interests,
|
|
121
|
+
patterns,
|
|
122
|
+
sessionCount: sessionCountRow?.cnt ?? 0,
|
|
123
|
+
lastUpdated: lastRow?.ts ?? 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Compound ─────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
compoundSignals(signals: OperatorSignals, sessionId: string): void {
|
|
130
|
+
this.provider.transaction(() => {
|
|
131
|
+
for (const sig of signals.expertise) {
|
|
132
|
+
if (this.isDeclined(sig.topic, sig.evidence)) continue;
|
|
133
|
+
this.compoundExpertise(sig);
|
|
134
|
+
}
|
|
135
|
+
for (const sig of signals.corrections) {
|
|
136
|
+
if (this.isDeclined(sig.rule, sig.quote)) continue;
|
|
137
|
+
// If this correction undoes an existing one, deactivate it and skip storage
|
|
138
|
+
if (this.tryUndoCorrection(sig.rule)) continue;
|
|
139
|
+
this.compoundCorrection(sig, sessionId);
|
|
140
|
+
}
|
|
141
|
+
for (const sig of signals.interests) {
|
|
142
|
+
if (this.isDeclined(sig.tag, sig.context)) continue;
|
|
143
|
+
this.compoundInterest(sig);
|
|
144
|
+
}
|
|
145
|
+
for (const sig of signals.patterns) {
|
|
146
|
+
if (this.isDeclined(sig.pattern)) continue;
|
|
147
|
+
this.compoundPattern(sig);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Decay interests not mentioned in this session
|
|
151
|
+
this.decayInterests(signals.interests.map((i) => i.tag));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Drift ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
hasDrifted(): boolean {
|
|
158
|
+
const current = this.renderContextFile();
|
|
159
|
+
if (this.lastRendered === null) {
|
|
160
|
+
this.lastRendered = current;
|
|
161
|
+
return true; // first render is always drift
|
|
162
|
+
}
|
|
163
|
+
const drifted = current !== this.lastRendered;
|
|
164
|
+
if (drifted) {
|
|
165
|
+
this.lastRendered = current;
|
|
166
|
+
}
|
|
167
|
+
return drifted;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Render ───────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
renderContextFile(): string {
|
|
173
|
+
const ctx = this.getContext();
|
|
174
|
+
const lines: string[] = ['# Operator Context', ''];
|
|
175
|
+
|
|
176
|
+
// Expertise
|
|
177
|
+
if (ctx.expertise.length > 0) {
|
|
178
|
+
const items = ctx.expertise
|
|
179
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
180
|
+
.map(
|
|
181
|
+
(e) =>
|
|
182
|
+
`${e.topic} (${e.level}, ${e.sessionCount} sessions, confidence ${e.confidence.toFixed(2)})`,
|
|
183
|
+
);
|
|
184
|
+
lines.push(`**Expertise:** ${items.join(', ')}.`);
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Corrections
|
|
189
|
+
const active = ctx.corrections.filter((c) => c.active);
|
|
190
|
+
if (active.length > 0) {
|
|
191
|
+
lines.push('**Corrections:**');
|
|
192
|
+
for (const c of active) {
|
|
193
|
+
const scopeTag = c.scope === 'project' ? ` [project]` : '';
|
|
194
|
+
const quoteTag = c.quote ? ` — "${c.quote}"` : '';
|
|
195
|
+
lines.push(`- ${c.rule}${scopeTag}${quoteTag}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Interests
|
|
201
|
+
const significantInterests = ctx.interests
|
|
202
|
+
.filter((i) => i.confidence >= 0.3)
|
|
203
|
+
.sort((a, b) => b.confidence - a.confidence);
|
|
204
|
+
if (significantInterests.length > 0) {
|
|
205
|
+
const items = significantInterests.map(
|
|
206
|
+
(i) => `${i.tag} (${i.mentionCount} mentions, confidence ${i.confidence.toFixed(2)})`,
|
|
207
|
+
);
|
|
208
|
+
lines.push(`**Interests:** ${items.join(', ')}.`);
|
|
209
|
+
lines.push('');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Patterns
|
|
213
|
+
if (ctx.patterns.length > 0) {
|
|
214
|
+
const items = ctx.patterns
|
|
215
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
216
|
+
.map((p) => `${p.pattern} (${p.frequency}, ${p.observedCount} observations)`);
|
|
217
|
+
lines.push(`**Work patterns:** ${items.join(', ')}.`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return lines.join('\n').trimEnd();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Inspect / Delete ─────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
inspect(): OperatorContext {
|
|
227
|
+
return this.getContext();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
deleteItem(type: ContextItemType, id: string): boolean {
|
|
231
|
+
const result = this.provider.run('DELETE FROM operator_context WHERE type = ? AND id = ?', [
|
|
232
|
+
type,
|
|
233
|
+
id,
|
|
234
|
+
]);
|
|
235
|
+
return result.changes > 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Private: Compounding Algorithms ──────────────────────────────
|
|
239
|
+
|
|
240
|
+
private compoundExpertise(sig: ExpertiseSignal): void {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
const signalConf = sig.confidence ?? DEFAULT_CONFIDENCE;
|
|
243
|
+
const key = sig.topic.toLowerCase();
|
|
244
|
+
|
|
245
|
+
const existing = this.getRow('expertise', key);
|
|
246
|
+
if (existing) {
|
|
247
|
+
const item = JSON.parse(existing.value) as ExpertiseItem;
|
|
248
|
+
|
|
249
|
+
// Exponential moving average
|
|
250
|
+
const newConfidence = item.confidence * 0.7 + signalConf * 0.3;
|
|
251
|
+
|
|
252
|
+
// Level upgrade: only up, never down automatically
|
|
253
|
+
let level = item.level;
|
|
254
|
+
if (this.levelRank(sig.level) > this.levelRank(item.level) && newConfidence > 0.8) {
|
|
255
|
+
level = sig.level;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const updated: ExpertiseItem = {
|
|
259
|
+
...item,
|
|
260
|
+
level,
|
|
261
|
+
confidence: Math.min(1.0, newConfidence),
|
|
262
|
+
sessionCount: item.sessionCount + 1,
|
|
263
|
+
lastObserved: now,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
this.updateRow(existing.id, 'expertise', key, updated, updated.confidence);
|
|
267
|
+
} else {
|
|
268
|
+
const item: ExpertiseItem = {
|
|
269
|
+
topic: sig.topic,
|
|
270
|
+
level: sig.level,
|
|
271
|
+
confidence: signalConf,
|
|
272
|
+
sessionCount: 1,
|
|
273
|
+
lastObserved: now,
|
|
274
|
+
};
|
|
275
|
+
this.insertRow('expertise', key, item, signalConf);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private compoundCorrection(sig: CorrectionSignal, sessionId: string): void {
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const key = sig.rule.toLowerCase();
|
|
282
|
+
|
|
283
|
+
// Corrections: latest wins on conflict
|
|
284
|
+
const existing = this.getRow('correction', key);
|
|
285
|
+
if (existing) {
|
|
286
|
+
const item: CorrectionItem = {
|
|
287
|
+
...(JSON.parse(existing.value) as CorrectionItem),
|
|
288
|
+
rule: sig.rule,
|
|
289
|
+
quote: sig.quote,
|
|
290
|
+
scope: sig.scope,
|
|
291
|
+
active: true,
|
|
292
|
+
sessionId,
|
|
293
|
+
};
|
|
294
|
+
this.updateRow(existing.id, 'correction', key, item, 1.0, sig.scope);
|
|
295
|
+
} else {
|
|
296
|
+
const correctionId = randomUUID();
|
|
297
|
+
const item: CorrectionItem = {
|
|
298
|
+
id: correctionId,
|
|
299
|
+
rule: sig.rule,
|
|
300
|
+
quote: sig.quote,
|
|
301
|
+
scope: sig.scope,
|
|
302
|
+
active: true,
|
|
303
|
+
createdAt: now,
|
|
304
|
+
sessionId,
|
|
305
|
+
};
|
|
306
|
+
this.insertRow('correction', key, item, 1.0, sig.scope, correctionId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private compoundInterest(sig: InterestSignal): void {
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const key = sig.tag.toLowerCase();
|
|
313
|
+
|
|
314
|
+
const existing = this.getRow('interest', key);
|
|
315
|
+
if (existing) {
|
|
316
|
+
const item = JSON.parse(existing.value) as InterestItem;
|
|
317
|
+
const newMentions = item.mentionCount + 1;
|
|
318
|
+
const newConfidence = Math.min(1.0, item.confidence + 0.1);
|
|
319
|
+
|
|
320
|
+
const updated: InterestItem = {
|
|
321
|
+
...item,
|
|
322
|
+
confidence: newConfidence,
|
|
323
|
+
mentionCount: newMentions,
|
|
324
|
+
lastMentioned: now,
|
|
325
|
+
};
|
|
326
|
+
this.updateRow(existing.id, 'interest', key, updated, updated.confidence);
|
|
327
|
+
} else {
|
|
328
|
+
const item: InterestItem = {
|
|
329
|
+
tag: sig.tag,
|
|
330
|
+
confidence: DEFAULT_CONFIDENCE,
|
|
331
|
+
mentionCount: 1,
|
|
332
|
+
lastMentioned: now,
|
|
333
|
+
};
|
|
334
|
+
this.insertRow('interest', key, item, item.confidence);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private compoundPattern(sig: WorkPatternSignal): void {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
const signalConf = DEFAULT_CONFIDENCE;
|
|
341
|
+
const key = sig.pattern.toLowerCase();
|
|
342
|
+
|
|
343
|
+
const existing = this.getRow('pattern', key);
|
|
344
|
+
if (existing) {
|
|
345
|
+
const item = JSON.parse(existing.value) as WorkPatternItem;
|
|
346
|
+
|
|
347
|
+
// Exponential moving average
|
|
348
|
+
const newConfidence = item.confidence * 0.8 + signalConf * 0.2;
|
|
349
|
+
const newCount = item.observedCount + 1;
|
|
350
|
+
|
|
351
|
+
// Frequency upgrades
|
|
352
|
+
let frequency = item.frequency;
|
|
353
|
+
if (newCount >= 8) frequency = 'frequent';
|
|
354
|
+
else if (newCount >= 3) frequency = 'occasional';
|
|
355
|
+
|
|
356
|
+
const updated: WorkPatternItem = {
|
|
357
|
+
...item,
|
|
358
|
+
frequency,
|
|
359
|
+
confidence: Math.min(1.0, newConfidence),
|
|
360
|
+
observedCount: newCount,
|
|
361
|
+
lastObserved: now,
|
|
362
|
+
};
|
|
363
|
+
this.updateRow(existing.id, 'pattern', key, updated, updated.confidence);
|
|
364
|
+
} else {
|
|
365
|
+
const item: WorkPatternItem = {
|
|
366
|
+
pattern: sig.pattern,
|
|
367
|
+
frequency: sig.frequency ?? 'once',
|
|
368
|
+
confidence: signalConf,
|
|
369
|
+
observedCount: 1,
|
|
370
|
+
lastObserved: now,
|
|
371
|
+
};
|
|
372
|
+
this.insertRow('pattern', key, item, item.confidence);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private decayInterests(mentionedTags: string[]): void {
|
|
377
|
+
const mentioned = new Set(mentionedTags.map((t) => t.toLowerCase()));
|
|
378
|
+
const allInterests = this.provider.all<ContextRow>(
|
|
379
|
+
"SELECT * FROM operator_context WHERE type = 'interest' AND active = 1",
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
for (const row of allInterests) {
|
|
383
|
+
if (mentioned.has(row.key)) continue;
|
|
384
|
+
const item = JSON.parse(row.value) as InterestItem;
|
|
385
|
+
const decayed = Math.max(0.1, item.confidence - 0.01);
|
|
386
|
+
if (decayed !== item.confidence) {
|
|
387
|
+
const updated = { ...item, confidence: decayed };
|
|
388
|
+
this.updateRow(row.id, 'interest', row.key, updated, decayed);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Private: DB Helpers ──────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
private getRow(type: ContextItemType, key: string): ContextRow | undefined {
|
|
396
|
+
return this.provider.get<ContextRow>(
|
|
397
|
+
'SELECT * FROM operator_context WHERE type = ? AND key = ? AND active = 1',
|
|
398
|
+
[type, key],
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private insertRow(
|
|
403
|
+
type: ContextItemType,
|
|
404
|
+
key: string,
|
|
405
|
+
value: unknown,
|
|
406
|
+
confidence: number,
|
|
407
|
+
scope: string = 'global',
|
|
408
|
+
rowId?: string,
|
|
409
|
+
): void {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
this.provider.run(
|
|
412
|
+
`INSERT INTO operator_context (id, type, key, value, confidence, scope, session_count, last_observed, created_at, active)
|
|
413
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, 1)`,
|
|
414
|
+
[rowId ?? randomUUID(), type, key, JSON.stringify(value), confidence, scope, now, now],
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private updateRow(
|
|
419
|
+
id: string,
|
|
420
|
+
type: ContextItemType,
|
|
421
|
+
key: string,
|
|
422
|
+
value: unknown,
|
|
423
|
+
confidence: number,
|
|
424
|
+
scope?: string,
|
|
425
|
+
): void {
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
const scopeClause = scope !== undefined ? ', scope = ?' : '';
|
|
428
|
+
const params: unknown[] = [
|
|
429
|
+
JSON.stringify(value),
|
|
430
|
+
confidence,
|
|
431
|
+
now,
|
|
432
|
+
...(scope !== undefined ? [scope] : []),
|
|
433
|
+
id,
|
|
434
|
+
];
|
|
435
|
+
this.provider.run(
|
|
436
|
+
`UPDATE operator_context SET value = ?, confidence = ?, last_observed = ?${scopeClause} WHERE id = ?`,
|
|
437
|
+
params,
|
|
438
|
+
);
|
|
439
|
+
// Suppress unused warnings
|
|
440
|
+
void type;
|
|
441
|
+
void key;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Private: Utilities ───────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
private levelRank(level: ExpertiseLevel): number {
|
|
447
|
+
switch (level) {
|
|
448
|
+
case 'learning':
|
|
449
|
+
return 0;
|
|
450
|
+
case 'intermediate':
|
|
451
|
+
return 1;
|
|
452
|
+
case 'expert':
|
|
453
|
+
return 2;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private isDeclined(...fields: (string | undefined | null)[]): boolean {
|
|
458
|
+
for (const field of fields) {
|
|
459
|
+
if (field && DECLINED_RE.test(field)) return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check all active corrections for an undo match. If found, deactivate the
|
|
466
|
+
* existing correction and return `true` (meaning: the new signal is a
|
|
467
|
+
* reversal, not a new rule to store).
|
|
468
|
+
*/
|
|
469
|
+
private tryUndoCorrection(rule: string): boolean {
|
|
470
|
+
const newNorm = normalizeCorrection(rule);
|
|
471
|
+
const activeCorrections = this.provider.all<ContextRow>(
|
|
472
|
+
"SELECT * FROM operator_context WHERE type = 'correction' AND active = 1",
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
for (const row of activeCorrections) {
|
|
476
|
+
const item = JSON.parse(row.value) as CorrectionItem;
|
|
477
|
+
const existingNorm = normalizeCorrection(item.rule);
|
|
478
|
+
|
|
479
|
+
if (isUndoCorrection(newNorm, existingNorm)) {
|
|
480
|
+
// Deactivate the old correction
|
|
481
|
+
const deactivated = { ...item, active: false };
|
|
482
|
+
this.provider.run(
|
|
483
|
+
'UPDATE operator_context SET value = ?, active = 0, last_observed = ? WHERE id = ?',
|
|
484
|
+
[JSON.stringify(deactivated), Date.now(), row.id],
|
|
485
|
+
);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// =============================================================================
|
|
494
|
+
// UNDO DETECTION HELPERS (exported for testing)
|
|
495
|
+
// =============================================================================
|
|
496
|
+
|
|
497
|
+
export interface NormalizedCorrection {
|
|
498
|
+
topic: string;
|
|
499
|
+
direction: 'do' | 'dont';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Extract the core topic and direction from a correction rule.
|
|
504
|
+
*
|
|
505
|
+
* "don't summarize" → { topic: "summarize", direction: "dont" }
|
|
506
|
+
* "actually, summaries are fine" → { topic: "summaries are fine", direction: "do" }
|
|
507
|
+
*/
|
|
508
|
+
export function normalizeCorrection(rule: string): NormalizedCorrection {
|
|
509
|
+
let direction: 'do' | 'dont' = 'dont';
|
|
510
|
+
let topic = rule.toLowerCase().trim();
|
|
511
|
+
|
|
512
|
+
if (DONT_PATTERNS.test(topic)) {
|
|
513
|
+
direction = 'dont';
|
|
514
|
+
topic = topic.replace(DONT_PATTERNS, '').trim();
|
|
515
|
+
} else if (DO_PATTERNS.test(topic)) {
|
|
516
|
+
direction = 'do';
|
|
517
|
+
topic = topic.replace(DO_PATTERNS, '').trim();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Strip trailing punctuation and common filler for comparison
|
|
521
|
+
topic = topic.replace(/[.!?]+$/, '').trim();
|
|
522
|
+
|
|
523
|
+
return { topic, direction };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Two corrections are an undo pair when they share a topic (fuzzy substring
|
|
528
|
+
* match) but have opposite directions.
|
|
529
|
+
*/
|
|
530
|
+
export function isUndoCorrection(a: NormalizedCorrection, b: NormalizedCorrection): boolean {
|
|
531
|
+
if (a.direction === b.direction) return false;
|
|
532
|
+
|
|
533
|
+
// Exact match
|
|
534
|
+
if (a.topic === b.topic) return true;
|
|
535
|
+
|
|
536
|
+
// Fuzzy: one topic is a substring of the other (covers "summarize" vs
|
|
537
|
+
// "summaries are fine" — we check if either contains the first significant
|
|
538
|
+
// word of the other).
|
|
539
|
+
const aWords = significantWords(a.topic);
|
|
540
|
+
const bWords = significantWords(b.topic);
|
|
541
|
+
|
|
542
|
+
// At least one significant word must overlap
|
|
543
|
+
return aWords.some((w) => bWords.includes(w));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Extract meaningful words (>= 4 chars) for fuzzy topic matching. */
|
|
547
|
+
function significantWords(topic: string): string[] {
|
|
548
|
+
return topic
|
|
549
|
+
.split(/\s+/)
|
|
550
|
+
.filter((w) => w.length >= 4)
|
|
551
|
+
.map((w) => w.replace(/(?:ing|ies|ise|ize|es|s)$/, '')); // crude stemming
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// =============================================================================
|
|
555
|
+
// INTERNAL ROW TYPE
|
|
556
|
+
// =============================================================================
|
|
557
|
+
|
|
558
|
+
interface ContextRow {
|
|
559
|
+
id: string;
|
|
560
|
+
type: string;
|
|
561
|
+
key: string;
|
|
562
|
+
value: string;
|
|
563
|
+
confidence: number;
|
|
564
|
+
scope: string;
|
|
565
|
+
session_count: number;
|
|
566
|
+
last_observed: number;
|
|
567
|
+
created_at: number;
|
|
568
|
+
active: number;
|
|
569
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator Context Signal Taxonomy — types for Soleri's adaptive persona feature.
|
|
3
|
+
*
|
|
4
|
+
* 4 signal types the agent reports through `orchestrate_complete`:
|
|
5
|
+
* - expertise: what the operator knows
|
|
6
|
+
* - corrections: what the operator wants done differently
|
|
7
|
+
* - interests: what the operator cares about beyond work
|
|
8
|
+
* - patterns: how the operator works
|
|
9
|
+
*
|
|
10
|
+
* Signals are compounded over sessions into a stable OperatorContext profile.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// INPUT SIGNALS (reported per session)
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/** Bag of signals the agent reports after a session. */
|
|
18
|
+
export interface OperatorSignals {
|
|
19
|
+
expertise: ExpertiseSignal[];
|
|
20
|
+
corrections: CorrectionSignal[];
|
|
21
|
+
interests: InterestSignal[];
|
|
22
|
+
patterns: WorkPatternSignal[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Observed expertise in a topic. */
|
|
26
|
+
export interface ExpertiseSignal {
|
|
27
|
+
/** e.g. "typescript", "react", "postgresql" */
|
|
28
|
+
topic: string;
|
|
29
|
+
level: ExpertiseLevel;
|
|
30
|
+
/** Brief quote or observation. */
|
|
31
|
+
evidence?: string;
|
|
32
|
+
/** 0.0–1.0, engine assigns default if missing. */
|
|
33
|
+
confidence?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Operator correction — "do this" / "don't do that". */
|
|
37
|
+
export interface CorrectionSignal {
|
|
38
|
+
/** What to do or not do. */
|
|
39
|
+
rule: string;
|
|
40
|
+
/** User's exact words. */
|
|
41
|
+
quote?: string;
|
|
42
|
+
scope: SignalScope;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Something the operator cares about outside of work. */
|
|
46
|
+
export interface InterestSignal {
|
|
47
|
+
/** e.g. "metal music", "coffee", "climbing" */
|
|
48
|
+
tag: string;
|
|
49
|
+
/** How it came up. */
|
|
50
|
+
context?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** How the operator works. */
|
|
54
|
+
export interface WorkPatternSignal {
|
|
55
|
+
/** e.g. "batches work locally", "prefers small PRs" */
|
|
56
|
+
pattern: string;
|
|
57
|
+
frequency?: PatternFrequency;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// COMPOUNDED PROFILE (stored in SQLite)
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/** Full compounded operator context, assembled from accumulated signals. */
|
|
65
|
+
export interface OperatorContext {
|
|
66
|
+
expertise: ExpertiseItem[];
|
|
67
|
+
corrections: CorrectionItem[];
|
|
68
|
+
interests: InterestItem[];
|
|
69
|
+
patterns: WorkPatternItem[];
|
|
70
|
+
sessionCount: number;
|
|
71
|
+
lastUpdated: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Compounded expertise entry. */
|
|
75
|
+
export interface ExpertiseItem {
|
|
76
|
+
topic: string;
|
|
77
|
+
level: ExpertiseLevel;
|
|
78
|
+
confidence: number;
|
|
79
|
+
/** How many sessions observed this topic. */
|
|
80
|
+
sessionCount: number;
|
|
81
|
+
lastObserved: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Stored correction. */
|
|
85
|
+
export interface CorrectionItem {
|
|
86
|
+
id: string;
|
|
87
|
+
rule: string;
|
|
88
|
+
quote?: string;
|
|
89
|
+
scope: SignalScope;
|
|
90
|
+
projectPath?: string;
|
|
91
|
+
active: boolean;
|
|
92
|
+
createdAt: number;
|
|
93
|
+
sessionId?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Compounded interest entry. */
|
|
97
|
+
export interface InterestItem {
|
|
98
|
+
tag: string;
|
|
99
|
+
confidence: number;
|
|
100
|
+
mentionCount: number;
|
|
101
|
+
lastMentioned: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Compounded work pattern entry. */
|
|
105
|
+
export interface WorkPatternItem {
|
|
106
|
+
pattern: string;
|
|
107
|
+
frequency: PatternFrequency;
|
|
108
|
+
confidence: number;
|
|
109
|
+
observedCount: number;
|
|
110
|
+
lastObserved: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// SHARED ENUMS / LITERALS
|
|
115
|
+
// =============================================================================
|
|
116
|
+
|
|
117
|
+
export type ExpertiseLevel = 'learning' | 'intermediate' | 'expert';
|
|
118
|
+
export type SignalScope = 'global' | 'project';
|
|
119
|
+
export type PatternFrequency = 'once' | 'occasional' | 'frequent';
|
|
120
|
+
export type ContextItemType = 'expertise' | 'correction' | 'interest' | 'pattern';
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// MUST-NOT-LEARN CATEGORIES
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
/** Categories the agent must never store, regardless of what is observed. */
|
|
127
|
+
export const DECLINED_CATEGORIES = [
|
|
128
|
+
'health',
|
|
129
|
+
'medical',
|
|
130
|
+
'political',
|
|
131
|
+
'religious',
|
|
132
|
+
'sexual',
|
|
133
|
+
'financial',
|
|
134
|
+
'legal',
|
|
135
|
+
'family',
|
|
136
|
+
'relationship',
|
|
137
|
+
] as const;
|
|
138
|
+
|
|
139
|
+
export type DeclinedCategory = (typeof DECLINED_CATEGORIES)[number];
|