principles-disciple 1.34.0 → 1.34.2
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/correction-cue-learner.ts +131 -7
- package/src/core/trajectory-types.ts +1 -1
- package/src/service/correction-observer-types.ts +11 -0
- package/src/service/correction-observer-workflow-manager.ts +32 -3
- package/src/service/evolution-worker.ts +362 -149
- package/src/service/keyword-optimization-service.ts +140 -0
- package/src/service/nocturnal-runtime.ts +30 -3
- package/src/service/nocturnal-service.ts +10 -4
- package/tests/service/keyword-optimization-service.test.ts +121 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import type {
|
|
17
17
|
CorrectionKeyword,
|
|
18
18
|
CorrectionKeywordStore,
|
|
19
|
-
CorrectionMatchResult
|
|
19
|
+
CorrectionMatchResult} from './correction-types.js';
|
|
20
|
+
import {
|
|
20
21
|
CORRECTION_SEED_KEYWORDS,
|
|
21
22
|
MAX_CORRECTION_KEYWORDS,
|
|
22
23
|
} from './correction-types.js';
|
|
24
|
+
import { checkCooldown, recordCooldown } from '../service/nocturnal-runtime.js';
|
|
23
25
|
|
|
24
26
|
const KEYWORD_STORE_FILE = 'correction_keywords.json';
|
|
25
27
|
|
|
@@ -128,8 +130,8 @@ export function _resetCorrectionCueLearnerInstance(): void {
|
|
|
128
130
|
// =========================================================================
|
|
129
131
|
|
|
130
132
|
export class CorrectionCueLearner {
|
|
131
|
-
private store: CorrectionKeywordStore;
|
|
132
|
-
private stateDir: string;
|
|
133
|
+
private readonly store: CorrectionKeywordStore;
|
|
134
|
+
private readonly stateDir: string;
|
|
133
135
|
|
|
134
136
|
constructor(stateDir: string) {
|
|
135
137
|
this.stateDir = stateDir;
|
|
@@ -142,7 +144,7 @@ export class CorrectionCueLearner {
|
|
|
142
144
|
* Checks whether text contains a correction cue (D-11).
|
|
143
145
|
* Normalisation is equivalent to the original detectCorrectionCue():
|
|
144
146
|
* trim → lowercase → strip punctuation
|
|
145
|
-
* Returns
|
|
147
|
+
* Returns weighted score based on keyword accuracy (D-39-03, D-39-04).
|
|
146
148
|
*/
|
|
147
149
|
match(text: string): CorrectionMatchResult {
|
|
148
150
|
const normalized = text
|
|
@@ -150,13 +152,98 @@ export class CorrectionCueLearner {
|
|
|
150
152
|
.toLowerCase()
|
|
151
153
|
.replace(/[.,!?;:,。!?;:]/g, '');
|
|
152
154
|
|
|
155
|
+
const matchedTerms: string[] = [];
|
|
156
|
+
let totalScore = 0;
|
|
157
|
+
|
|
153
158
|
for (const keyword of this.store.keywords) {
|
|
154
159
|
if (normalized.includes(keyword.term.toLowerCase())) {
|
|
155
|
-
|
|
160
|
+
// D-39-03, D-39-04: Weighted score formula
|
|
161
|
+
// score = weight x ((TP + 1) / (TP + FP + 2))
|
|
162
|
+
// +2 smoothing: new keywords (TP=0, FP=0) get accuracy=0.5
|
|
163
|
+
const tp = keyword.truePositiveCount ?? 0;
|
|
164
|
+
const fp = keyword.falsePositiveCount ?? 0;
|
|
165
|
+
const accuracy = (tp + 1) / (tp + fp + 2);
|
|
166
|
+
const score = keyword.weight * accuracy;
|
|
167
|
+
|
|
168
|
+
totalScore += score;
|
|
169
|
+
matchedTerms.push(keyword.term);
|
|
170
|
+
|
|
171
|
+
// Increment hitCount
|
|
172
|
+
keyword.hitCount = (keyword.hitCount ?? 0) + 1;
|
|
173
|
+
keyword.lastHitAt = new Date().toISOString();
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
|
|
177
|
+
const cappedScore = Math.min(1, totalScore);
|
|
178
|
+
const isMatched = matchedTerms.length > 0;
|
|
179
|
+
|
|
180
|
+
// D-39-04: Confidence derived from multiple signals
|
|
181
|
+
const termConfidence = Math.min(1, matchedTerms.length / 3);
|
|
182
|
+
const scoreConfidence = Math.min(1, cappedScore / 0.8);
|
|
183
|
+
const confidence = Math.max(termConfidence, scoreConfidence);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
matched: isMatched,
|
|
187
|
+
matchedTerms: matchedTerms.slice(0, 5),
|
|
188
|
+
score: cappedScore,
|
|
189
|
+
confidence,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Records a confirmed true positive for the given keyword term.
|
|
195
|
+
* Increments both hitCount and truePositiveCount.
|
|
196
|
+
*/
|
|
197
|
+
recordTruePositive(term: string): void {
|
|
198
|
+
const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
|
|
199
|
+
if (!keyword) return;
|
|
200
|
+
|
|
201
|
+
keyword.truePositiveCount = (keyword.truePositiveCount ?? 0) + 1;
|
|
202
|
+
keyword.hitCount = (keyword.hitCount ?? 0) + 1;
|
|
203
|
+
keyword.lastHitAt = new Date().toISOString();
|
|
204
|
+
|
|
205
|
+
this.flush();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Records a confirmed false positive for the given keyword term.
|
|
210
|
+
* CORR-10: Decreases keyword weight by 20% (x0.8 multiplicative factor).
|
|
211
|
+
*/
|
|
212
|
+
recordFalsePositive(term: string): void {
|
|
213
|
+
const keyword = this.store.keywords.find(k => k.term.toLowerCase() === term.toLowerCase());
|
|
214
|
+
if (!keyword) return;
|
|
215
|
+
|
|
216
|
+
keyword.falsePositiveCount = (keyword.falsePositiveCount ?? 0) + 1;
|
|
217
|
+
keyword.hitCount = (keyword.hitCount ?? 0) + 1;
|
|
218
|
+
|
|
219
|
+
// D-39-15: Multiplicative weight decay x0.8 on confirmed FP
|
|
220
|
+
keyword.weight = Math.max(0.1, keyword.weight * 0.8);
|
|
221
|
+
keyword.lastHitAt = new Date().toISOString();
|
|
222
|
+
|
|
223
|
+
this.flush();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Returns true if optimization is allowed (within daily throttle limit).
|
|
228
|
+
* CORR-08: Max 4 optimizations per day across all triggers.
|
|
229
|
+
*/
|
|
230
|
+
canRunKeywordOptimization(): boolean {
|
|
231
|
+
// D-39-12, D-39-13: Per-workspace throttle, 4 calls/day
|
|
232
|
+
const cooldown = checkCooldown(this.stateDir, 'keyword_optimization', {
|
|
233
|
+
maxRunsPerWindow: 4,
|
|
234
|
+
quotaWindowMs: 24 * 60 * 60 * 1000,
|
|
235
|
+
});
|
|
236
|
+
return !cooldown.globalCooldownActive && !cooldown.quotaExhausted;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Records that an optimization was performed.
|
|
241
|
+
* Increments the daily throttle counter and updates lastOptimizedAt.
|
|
242
|
+
*/
|
|
243
|
+
async recordOptimizationPerformed(): Promise<void> {
|
|
244
|
+
await recordCooldown(this.stateDir, 24 * 60 * 60 * 1000);
|
|
245
|
+
this.store.lastOptimizedAt = new Date().toISOString();
|
|
246
|
+
this.flush();
|
|
160
247
|
}
|
|
161
248
|
|
|
162
249
|
/**
|
|
@@ -177,11 +264,48 @@ export class CorrectionCueLearner {
|
|
|
177
264
|
this.flush();
|
|
178
265
|
}
|
|
179
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Updates the weight of an existing keyword.
|
|
269
|
+
* Weight is clamped to 0.1-0.9 range.
|
|
270
|
+
* Throws if keyword not found.
|
|
271
|
+
*/
|
|
272
|
+
updateWeight(term: string, weight: number): void {
|
|
273
|
+
const idx = this.store.keywords.findIndex(
|
|
274
|
+
k => k.term.toLowerCase() === term.toLowerCase()
|
|
275
|
+
);
|
|
276
|
+
if (idx < 0) {
|
|
277
|
+
throw new Error(`Keyword not found: ${term}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.store.keywords[idx].weight = Math.max(0.1, Math.min(0.9, weight));
|
|
281
|
+
this.flush();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Removes a keyword from the store by term.
|
|
286
|
+
* Throws if keyword not found.
|
|
287
|
+
*/
|
|
288
|
+
remove(term: string): void {
|
|
289
|
+
const idx = this.store.keywords.findIndex(
|
|
290
|
+
k => k.term.toLowerCase() === term.toLowerCase()
|
|
291
|
+
);
|
|
292
|
+
if (idx < 0) {
|
|
293
|
+
throw new Error(`Keyword not found: ${term}`);
|
|
294
|
+
}
|
|
295
|
+
this.store.keywords.splice(idx, 1);
|
|
296
|
+
this.flush();
|
|
297
|
+
}
|
|
298
|
+
|
|
180
299
|
/** Returns a reference to the in-memory store. */
|
|
181
300
|
getStore(): CorrectionKeywordStore {
|
|
182
301
|
return this.store;
|
|
183
302
|
}
|
|
184
303
|
|
|
304
|
+
/** Returns the lastOptimizedAt timestamp. */
|
|
305
|
+
getLastOptimizedAt(): string {
|
|
306
|
+
return this.store.lastOptimizedAt;
|
|
307
|
+
}
|
|
308
|
+
|
|
185
309
|
/** Persists the current in-memory store to disk atomically. */
|
|
186
310
|
flush(): void {
|
|
187
311
|
saveCorrectionKeywordStore(this.stateDir, this.store);
|
|
@@ -117,7 +117,7 @@ export interface TrajectorySessionInput {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// V2: Task kind and priority types for queue schema
|
|
120
|
-
export type TaskKind = 'pain_diagnosis' | 'sleep_reflection' | 'model_eval';
|
|
120
|
+
export type TaskKind = 'pain_diagnosis' | 'sleep_reflection' | 'model_eval' | 'keyword_optimization';
|
|
121
121
|
export type TaskPriority = 'high' | 'medium' | 'low';
|
|
122
122
|
|
|
123
123
|
// V2: EvolutionTaskInput with all V2 fields
|
|
@@ -29,6 +29,17 @@ export interface CorrectionObserverPayload {
|
|
|
29
29
|
};
|
|
30
30
|
/** Recent user messages for pattern analysis */
|
|
31
31
|
recentMessages: string[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Trajectory history: user turns where correctionDetected=true (D-40-08).
|
|
35
|
+
* Includes term matched, timestamp, sessionId for FPR trend analysis.
|
|
36
|
+
*/
|
|
37
|
+
trajectoryHistory: Array<{
|
|
38
|
+
sessionId: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
term: string;
|
|
41
|
+
userMessage: string;
|
|
42
|
+
}>;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
/**
|
|
@@ -21,7 +21,6 @@ import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
|
|
|
21
21
|
import type {
|
|
22
22
|
CorrectionObserverPayload,
|
|
23
23
|
CorrectionObserverResult,
|
|
24
|
-
CorrectionObserverWorkflowSpec,
|
|
25
24
|
} from './correction-observer-types.js';
|
|
26
25
|
|
|
27
26
|
const WORKFLOW_SESSION_PREFIX = 'agent:main:subagent:workflow-correction-';
|
|
@@ -92,7 +91,7 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
|
|
|
92
91
|
|
|
93
92
|
buildPrompt(taskInput: unknown, _metadata: WorkflowMetadata): string {
|
|
94
93
|
const payload = taskInput as CorrectionObserverPayload;
|
|
95
|
-
const { keywordStoreSummary, recentMessages } = payload;
|
|
94
|
+
const { keywordStoreSummary, recentMessages, trajectoryHistory } = payload;
|
|
96
95
|
|
|
97
96
|
const termsList = keywordStoreSummary.terms
|
|
98
97
|
.map(t => ` - term="${t.term}", weight=${t.weight}, hits=${t.hitCount}, TP=${t.truePositiveCount}, FP=${t.falsePositiveCount}`)
|
|
@@ -102,6 +101,11 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
|
|
|
102
101
|
? recentMessages.map(m => ` - ${JSON.stringify(m)}`).join('\n')
|
|
103
102
|
: ' (none)';
|
|
104
103
|
|
|
104
|
+
const trajectory = trajectoryHistory.length > 0
|
|
105
|
+
? trajectoryHistory.map(t => ` - [${t.sessionId}] ${t.term} (${t.timestamp}): ${t.userMessage.substring(0, 80)}`)
|
|
106
|
+
.join('\n')
|
|
107
|
+
: ' (none)';
|
|
108
|
+
|
|
105
109
|
return [
|
|
106
110
|
'You are a correction keyword optimizer.',
|
|
107
111
|
'',
|
|
@@ -115,6 +119,9 @@ export const correctionObserverWorkflowSpec: SubagentWorkflowSpec<CorrectionObse
|
|
|
115
119
|
'## Recent User Messages (' + recentMessages.length + ' messages):',
|
|
116
120
|
messages,
|
|
117
121
|
'',
|
|
122
|
+
'## Correction Trajectory (recent confirmed corrections, D-40-08):',
|
|
123
|
+
trajectory,
|
|
124
|
+
'',
|
|
118
125
|
'## Rules:',
|
|
119
126
|
'- ADD: If a correction pattern is detected in messages but not in store',
|
|
120
127
|
'- UPDATE: If a term\'s weight should change based on TP/FP ratio',
|
|
@@ -187,7 +194,7 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
|
|
|
187
194
|
return super.startWorkflow(spec, options);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
|
|
197
|
+
|
|
191
198
|
protected override createWorkflowMetadata<TResult>(
|
|
192
199
|
spec: SubagentWorkflowSpec<TResult>,
|
|
193
200
|
options: {
|
|
@@ -207,6 +214,28 @@ export class CorrectionObserverWorkflowManager extends WorkflowManagerBase {
|
|
|
207
214
|
...options.metadata,
|
|
208
215
|
};
|
|
209
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Retrieves and parses the workflow result for a completed workflow.
|
|
220
|
+
* Called by evolution-worker.ts after getWorkflowDebugSummary reports state=completed.
|
|
221
|
+
*/
|
|
222
|
+
async getWorkflowResult(workflowId: string): Promise<CorrectionObserverResult | null> {
|
|
223
|
+
const workflow = this.store.getWorkflow(workflowId);
|
|
224
|
+
if (!workflow) return null;
|
|
225
|
+
|
|
226
|
+
const result = await this.driver.getResult({
|
|
227
|
+
sessionKey: workflow.child_session_key,
|
|
228
|
+
limit: 20,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const metadata = JSON.parse(workflow.metadata_json) as WorkflowMetadata;
|
|
232
|
+
return correctionObserverWorkflowSpec.parseResult({
|
|
233
|
+
messages: result.messages,
|
|
234
|
+
assistantTexts: result.assistantTexts,
|
|
235
|
+
metadata,
|
|
236
|
+
waitStatus: 'ok',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
// ── Factory ─────────────────────────────────────────────────────────────────
|