principles-disciple 1.5.4 → 1.7.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/commands/context.d.ts +5 -0
- package/dist/commands/context.js +312 -0
- package/dist/commands/evolution-status.d.ts +4 -0
- package/dist/commands/evolution-status.js +138 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +45 -0
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +582 -0
- package/dist/commands/pain.js +143 -6
- package/dist/commands/principle-rollback.d.ts +4 -0
- package/dist/commands/principle-rollback.js +22 -0
- package/dist/commands/rollback.d.ts +19 -0
- package/dist/commands/rollback.js +119 -0
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/core/config.d.ts +37 -0
- package/dist/core/config.js +47 -0
- package/dist/core/control-ui-db.d.ts +68 -0
- package/dist/core/control-ui-db.js +274 -0
- package/dist/core/detection-funnel.d.ts +1 -1
- package/dist/core/detection-funnel.js +4 -0
- package/dist/core/dictionary.d.ts +2 -0
- package/dist/core/dictionary.js +13 -0
- package/dist/core/event-log.d.ts +22 -1
- package/dist/core/event-log.js +319 -0
- package/dist/core/evolution-engine.d.ts +5 -5
- package/dist/core/evolution-engine.js +18 -18
- package/dist/core/evolution-migration.d.ts +5 -0
- package/dist/core/evolution-migration.js +65 -0
- package/dist/core/evolution-reducer.d.ts +69 -0
- package/dist/core/evolution-reducer.js +369 -0
- package/dist/core/evolution-types.d.ts +103 -0
- package/dist/core/focus-history.d.ts +65 -0
- package/dist/core/focus-history.js +266 -0
- package/dist/core/init.js +30 -7
- package/dist/core/migration.js +0 -2
- package/dist/core/path-resolver.d.ts +3 -0
- package/dist/core/path-resolver.js +90 -31
- package/dist/core/paths.d.ts +7 -8
- package/dist/core/paths.js +48 -40
- package/dist/core/profile.js +1 -1
- package/dist/core/session-tracker.d.ts +4 -0
- package/dist/core/session-tracker.js +15 -0
- package/dist/core/thinking-models.d.ts +38 -0
- package/dist/core/thinking-models.js +170 -0
- package/dist/core/trajectory.d.ts +184 -0
- package/dist/core/trajectory.js +817 -0
- package/dist/core/trust-engine.d.ts +2 -0
- package/dist/core/trust-engine.js +30 -4
- package/dist/core/workspace-context.d.ts +13 -0
- package/dist/core/workspace-context.js +50 -7
- package/dist/hooks/gate.js +301 -30
- package/dist/hooks/llm.d.ts +8 -0
- package/dist/hooks/llm.js +347 -69
- package/dist/hooks/message-sanitize.d.ts +3 -0
- package/dist/hooks/message-sanitize.js +37 -0
- package/dist/hooks/pain.js +105 -5
- package/dist/hooks/prompt.d.ts +20 -11
- package/dist/hooks/prompt.js +558 -158
- package/dist/hooks/subagent.d.ts +9 -2
- package/dist/hooks/subagent.js +40 -3
- package/dist/http/principles-console-route.d.ts +2 -0
- package/dist/http/principles-console-route.js +257 -0
- package/dist/i18n/commands.js +48 -20
- package/dist/index.js +264 -8
- package/dist/service/control-ui-query-service.d.ts +217 -0
- package/dist/service/control-ui-query-service.js +537 -0
- package/dist/service/empathy-observer-manager.d.ts +42 -0
- package/dist/service/empathy-observer-manager.js +147 -0
- package/dist/service/evolution-worker.d.ts +10 -0
- package/dist/service/evolution-worker.js +156 -24
- package/dist/service/trajectory-service.d.ts +2 -0
- package/dist/service/trajectory-service.js +15 -0
- package/dist/tools/agent-spawn.d.ts +27 -6
- package/dist/tools/agent-spawn.js +339 -87
- package/dist/tools/deep-reflect.d.ts +27 -7
- package/dist/tools/deep-reflect.js +282 -113
- package/dist/types/event-types.d.ts +84 -2
- package/dist/types/event-types.js +33 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +24 -1
- package/openclaw.plugin.json +43 -11
- package/package.json +16 -6
- package/templates/langs/zh/core/HEARTBEAT.md +28 -4
- package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
- package/templates/pain_settings.json +54 -2
- package/templates/workspace/.principles/PROFILE.json +2 -0
- package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
package/dist/hooks/llm.js
CHANGED
|
@@ -1,9 +1,180 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { trackLlmOutput, recordThinkingCheckpoint } from '../core/session-tracker.js';
|
|
3
|
+
import { trackFriction, trackLlmOutput, recordThinkingCheckpoint, resetFriction } from '../core/session-tracker.js';
|
|
4
4
|
import { writePainFlag } from '../core/pain.js';
|
|
5
|
+
import { ControlUiDatabase } from '../core/control-ui-db.js';
|
|
5
6
|
import { DetectionService } from '../core/detection-service.js';
|
|
7
|
+
import { detectThinkingModelMatches, deriveThinkingScenarios } from '../core/thinking-models.js';
|
|
6
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
|
+
import { sanitizeAssistantText } from './message-sanitize.js';
|
|
10
|
+
const empathyDedupState = new Map();
|
|
11
|
+
const empathyRateState = new Map();
|
|
12
|
+
function clamp(value, min, max) {
|
|
13
|
+
return Math.max(min, Math.min(max, value));
|
|
14
|
+
}
|
|
15
|
+
function normalizeSeverity(input) {
|
|
16
|
+
const normalized = (input || '').toLowerCase();
|
|
17
|
+
if (normalized === 'severe' || normalized === 'high')
|
|
18
|
+
return 'severe';
|
|
19
|
+
if (normalized === 'moderate' || normalized === 'medium')
|
|
20
|
+
return 'moderate';
|
|
21
|
+
return 'mild';
|
|
22
|
+
}
|
|
23
|
+
function parseConfidence(raw) {
|
|
24
|
+
const parsed = Number(raw);
|
|
25
|
+
if (!Number.isFinite(parsed))
|
|
26
|
+
return 1;
|
|
27
|
+
return clamp(parsed, 0, 1);
|
|
28
|
+
}
|
|
29
|
+
function parseTrustedLegacyTag(text) {
|
|
30
|
+
return text.match(/^\s*\[EMOTIONAL_DAMAGE_DETECTED(?::(mild|moderate|severe))?\]\s*$/i);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 检测标签是否是被用户诱导/引用输出的(回显),而非 LLM 主动输出的情绪信号
|
|
34
|
+
*/
|
|
35
|
+
function isEchoedTag(text, tagMatch) {
|
|
36
|
+
const tagIndex = tagMatch.index ?? 0;
|
|
37
|
+
const before = text.substring(Math.max(0, tagIndex - 100), tagIndex).toLowerCase();
|
|
38
|
+
// 1. 检查是否在引号内(用户引用)
|
|
39
|
+
const quotesBefore = (before.match(/["'\u300c\u300d\u201c\u201d`]/g) || []).length;
|
|
40
|
+
if (quotesBefore % 2 === 1)
|
|
41
|
+
return true;
|
|
42
|
+
// 2. Strong patterns: 用户指令关键词(任意位置匹配)
|
|
43
|
+
const strongPatterns = [
|
|
44
|
+
/用户(说|让|要求|让我输出)/,
|
|
45
|
+
/user\s+(said|asked|told|wants)\s+me\s+to\s+(output|write|say)/,
|
|
46
|
+
/请(输出|包含|显示).*\[emotional/,
|
|
47
|
+
/please\s+(output|include).*\[emotional/,
|
|
48
|
+
/你让我输出/,
|
|
49
|
+
];
|
|
50
|
+
for (const pattern of strongPatterns) {
|
|
51
|
+
if (pattern.test(before))
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// 3. Weak patterns: 仅在标签 15 字符内触发
|
|
55
|
+
const weakPatterns = [
|
|
56
|
+
{ pattern: /echo/, window: 15 },
|
|
57
|
+
{ pattern: /copy/, window: 15 },
|
|
58
|
+
{ pattern: /复述/, window: 15 },
|
|
59
|
+
];
|
|
60
|
+
for (const { pattern, window } of weakPatterns) {
|
|
61
|
+
const nearTag = text.substring(Math.max(0, tagIndex - window), tagIndex).toLowerCase();
|
|
62
|
+
if (pattern.test(nearTag))
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// 4. 检查是否在代码块内
|
|
66
|
+
const codeBlocksBefore = (before.match(/```/g) || []).length;
|
|
67
|
+
if (codeBlocksBefore % 2 === 1)
|
|
68
|
+
return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
export function extractEmpathySignal(text) {
|
|
72
|
+
if (!text || typeof text !== 'string') {
|
|
73
|
+
return { detected: false, severity: 'mild', confidence: 1 };
|
|
74
|
+
}
|
|
75
|
+
const xmlMatch = text.match(/<empathy\s+([^>]*)\/?>(?:<\/empathy>)?/i);
|
|
76
|
+
if (xmlMatch?.[1]) {
|
|
77
|
+
const attrs = xmlMatch[1];
|
|
78
|
+
const signal = attrs.match(/signal\s*=\s*"([^"]+)"/i)?.[1]?.toLowerCase();
|
|
79
|
+
if (signal === 'damage' || signal === 'pain' || signal === 'frustration') {
|
|
80
|
+
const severity = normalizeSeverity(attrs.match(/severity\s*=\s*"([^"]+)"/i)?.[1]);
|
|
81
|
+
const confidence = parseConfidence(attrs.match(/confidence\s*=\s*"([^"]+)"/i)?.[1]);
|
|
82
|
+
const reason = attrs.match(/reason\s*=\s*"([^"]+)"/i)?.[1];
|
|
83
|
+
return { detected: true, severity, confidence, reason, mode: 'structured' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const jsonMatch = text.match(/"empathy"\s*:\s*\{[\s\S]*?\}/i);
|
|
87
|
+
if (jsonMatch) {
|
|
88
|
+
const jsonText = `{${jsonMatch[0]}}`;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(jsonText);
|
|
91
|
+
if (parsed.empathy?.damageDetected === true) {
|
|
92
|
+
return {
|
|
93
|
+
detected: true,
|
|
94
|
+
severity: normalizeSeverity(parsed.empathy.severity),
|
|
95
|
+
confidence: clamp(Number(parsed.empathy.confidence ?? 1), 0, 1),
|
|
96
|
+
reason: parsed.empathy.reason,
|
|
97
|
+
mode: 'structured'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// ignore malformed snippet
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const tagMatch = parseTrustedLegacyTag(text);
|
|
106
|
+
if (tagMatch) {
|
|
107
|
+
if (isEchoedTag(text, tagMatch)) {
|
|
108
|
+
return { detected: false, severity: 'mild', confidence: 1 };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
detected: true,
|
|
112
|
+
severity: normalizeSeverity(tagMatch[1]),
|
|
113
|
+
confidence: 1,
|
|
114
|
+
mode: 'legacy_tag'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { detected: false, severity: 'mild', confidence: 1 };
|
|
118
|
+
}
|
|
119
|
+
function mapSeverityToPenalty(severity, config) {
|
|
120
|
+
const mild = Number(config.get('empathy_engine.penalties.mild') ?? 10);
|
|
121
|
+
const moderate = Number(config.get('empathy_engine.penalties.moderate') ?? 25);
|
|
122
|
+
const severe = Number(config.get('empathy_engine.penalties.severe') ?? 40);
|
|
123
|
+
if (severity === 'severe')
|
|
124
|
+
return severe;
|
|
125
|
+
if (severity === 'moderate')
|
|
126
|
+
return moderate;
|
|
127
|
+
return mild;
|
|
128
|
+
}
|
|
129
|
+
function dedupeKey(sessionId, runId, signal) {
|
|
130
|
+
return `${sessionId}:${runId}:${signal.severity}:${(signal.reason || '').slice(0, 80)}`;
|
|
131
|
+
}
|
|
132
|
+
function shouldDedupe(sessionId, runId, signal, windowMs) {
|
|
133
|
+
const key = dedupeKey(sessionId, runId, signal);
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const last = empathyDedupState.get(key);
|
|
136
|
+
if (typeof last === 'number' && now - last <= windowMs) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
empathyDedupState.set(key, now);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function resolveCalibrationFactor(event, config) {
|
|
143
|
+
const table = config.get('empathy_engine.model_calibration');
|
|
144
|
+
if (!table || typeof table !== 'object')
|
|
145
|
+
return 1;
|
|
146
|
+
const modelKey = `${event.provider}/${event.model}`;
|
|
147
|
+
const factor = Number(table[modelKey] ?? 1);
|
|
148
|
+
if (!Number.isFinite(factor))
|
|
149
|
+
return 1;
|
|
150
|
+
return clamp(factor, 0.1, 3);
|
|
151
|
+
}
|
|
152
|
+
function applyRateLimit(sessionId, runId, score, config) {
|
|
153
|
+
const maxPerTurn = Number(config.get('empathy_engine.rate_limit.max_per_turn') ?? 40);
|
|
154
|
+
const maxPerHour = Number(config.get('empathy_engine.rate_limit.max_per_hour') ?? 120);
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const prev = empathyRateState.get(sessionId) ?? {
|
|
157
|
+
turnScore: 0,
|
|
158
|
+
hourScore: 0,
|
|
159
|
+
hourWindowStart: now,
|
|
160
|
+
lastRunId: runId,
|
|
161
|
+
};
|
|
162
|
+
if (prev.lastRunId !== runId) {
|
|
163
|
+
prev.turnScore = 0;
|
|
164
|
+
prev.lastRunId = runId;
|
|
165
|
+
}
|
|
166
|
+
if (now - prev.hourWindowStart >= 60 * 60 * 1000) {
|
|
167
|
+
prev.hourScore = 0;
|
|
168
|
+
prev.hourWindowStart = now;
|
|
169
|
+
}
|
|
170
|
+
const byTurn = Math.max(0, maxPerTurn - prev.turnScore);
|
|
171
|
+
const byHour = Math.max(0, maxPerHour - prev.hourScore);
|
|
172
|
+
const allowed = Math.max(0, Math.min(score, byTurn, byHour));
|
|
173
|
+
prev.turnScore += allowed;
|
|
174
|
+
prev.hourScore += allowed;
|
|
175
|
+
empathyRateState.set(sessionId, prev);
|
|
176
|
+
return allowed;
|
|
177
|
+
}
|
|
7
178
|
export function handleLlmOutput(event, ctx) {
|
|
8
179
|
if (!ctx.workspaceDir || !ctx.sessionId)
|
|
9
180
|
return;
|
|
@@ -16,6 +187,25 @@ export function handleLlmOutput(event, ctx) {
|
|
|
16
187
|
if (!event.assistantTexts || event.assistantTexts.length === 0)
|
|
17
188
|
return;
|
|
18
189
|
const text = event.assistantTexts.join('\n');
|
|
190
|
+
const signal = extractEmpathySignal(text);
|
|
191
|
+
const createdAt = new Date().toISOString();
|
|
192
|
+
let assistantTurnId = null;
|
|
193
|
+
try {
|
|
194
|
+
assistantTurnId = wctx.trajectory?.recordAssistantTurn?.({
|
|
195
|
+
sessionId: ctx.sessionId,
|
|
196
|
+
runId: event.runId,
|
|
197
|
+
provider: event.provider,
|
|
198
|
+
model: event.model,
|
|
199
|
+
rawText: text,
|
|
200
|
+
sanitizedText: sanitizeAssistantText(text),
|
|
201
|
+
usageJson: event.usage || {},
|
|
202
|
+
empathySignalJson: signal,
|
|
203
|
+
createdAt,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
ctx.logger?.warn?.(`[PD:LLM] Failed to persist assistant turn to trajectory: ${String(error)}`);
|
|
208
|
+
}
|
|
19
209
|
// ── Track B: Semantic Pain Detection (V1.3.0 Funnel) ──
|
|
20
210
|
const detectionService = DetectionService.get(wctx.stateDir);
|
|
21
211
|
const detection = detectionService.detect(text);
|
|
@@ -34,6 +224,84 @@ export function handleLlmOutput(event, ctx) {
|
|
|
34
224
|
let matchedReason = detection.detected
|
|
35
225
|
? `Agent triggered pain detection (Source: ${detection.source}${detection.ruleId ? `, Rule: ${detection.ruleId}` : ''})`
|
|
36
226
|
: '';
|
|
227
|
+
// empathy sub-pipeline (enabled by default)
|
|
228
|
+
const empathyEnabled = config.get('empathy_engine.enabled');
|
|
229
|
+
if (empathyEnabled !== false) {
|
|
230
|
+
if (signal.detected) {
|
|
231
|
+
const dedupeWindow = Number(config.get('empathy_engine.dedupe_window_ms') ?? 60000);
|
|
232
|
+
const deduped = shouldDedupe(ctx.sessionId, event.runId, signal, dedupeWindow);
|
|
233
|
+
if (!deduped) {
|
|
234
|
+
const baseScore = mapSeverityToPenalty(signal.severity, config);
|
|
235
|
+
const weightedScore = Math.round(baseScore * signal.confidence);
|
|
236
|
+
const calibrationFactor = resolveCalibrationFactor(event, config);
|
|
237
|
+
const calibratedScore = Math.round(weightedScore * calibrationFactor);
|
|
238
|
+
const boundedScore = applyRateLimit(ctx.sessionId, event.runId, calibratedScore, config);
|
|
239
|
+
if (boundedScore > 0) {
|
|
240
|
+
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir);
|
|
241
|
+
try {
|
|
242
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
243
|
+
sessionId: ctx.sessionId,
|
|
244
|
+
source: 'user_empathy',
|
|
245
|
+
score: boundedScore,
|
|
246
|
+
reason: signal.reason || 'Assistant self-reported user emotional distress.',
|
|
247
|
+
severity: signal.severity,
|
|
248
|
+
origin: 'assistant_self_report',
|
|
249
|
+
confidence: signal.confidence,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
ctx.logger?.warn?.(`[PD:LLM] Failed to persist empathy pain event to trajectory: ${String(error)}`);
|
|
254
|
+
}
|
|
255
|
+
// Generate unique event ID for rollback support
|
|
256
|
+
const eventId = `emp_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
257
|
+
eventLog.recordPainSignal(ctx.sessionId, {
|
|
258
|
+
score: boundedScore,
|
|
259
|
+
source: 'user_empathy',
|
|
260
|
+
reason: signal.reason || 'Assistant self-reported user emotional distress.',
|
|
261
|
+
isRisky: false,
|
|
262
|
+
origin: 'assistant_self_report',
|
|
263
|
+
severity: signal.severity,
|
|
264
|
+
confidence: signal.confidence,
|
|
265
|
+
detection_mode: signal.mode,
|
|
266
|
+
deduped: false,
|
|
267
|
+
trigger_text_excerpt: text.substring(0, 120),
|
|
268
|
+
raw_score: weightedScore,
|
|
269
|
+
calibrated_score: calibratedScore,
|
|
270
|
+
eventId,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
eventLog.recordPainSignal(ctx.sessionId, {
|
|
276
|
+
score: 0,
|
|
277
|
+
source: 'user_empathy',
|
|
278
|
+
reason: signal.reason || 'Deduped empathy signal.',
|
|
279
|
+
isRisky: false,
|
|
280
|
+
origin: 'assistant_self_report',
|
|
281
|
+
severity: signal.severity,
|
|
282
|
+
confidence: signal.confidence,
|
|
283
|
+
detection_mode: signal.mode,
|
|
284
|
+
deduped: true,
|
|
285
|
+
trigger_text_excerpt: text.substring(0, 120),
|
|
286
|
+
raw_score: Math.round(mapSeverityToPenalty(signal.severity, config) * signal.confidence),
|
|
287
|
+
calibrated_score: Math.round(mapSeverityToPenalty(signal.severity, config) * signal.confidence * resolveCalibrationFactor(event, config))
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ═══ Natural Language Rollback Detection ═══
|
|
293
|
+
// Detect [EMPATHY_ROLLBACK_REQUEST] tag and trigger rollback
|
|
294
|
+
const rollbackMatch = text.match(/^\s*\[EMPATHY_ROLLBACK_REQUEST\]\s*$/m);
|
|
295
|
+
if (rollbackMatch) {
|
|
296
|
+
const eventId = eventLog.getLastEmpathyEventId(ctx.sessionId);
|
|
297
|
+
if (eventId) {
|
|
298
|
+
const rolledBackScore = eventLog.rollbackEmpathyEvent(eventId, ctx.sessionId, 'Natural language rollback request detected', 'natural_language');
|
|
299
|
+
if (rolledBackScore > 0) {
|
|
300
|
+
// Reset GFI after successful rollback
|
|
301
|
+
resetFriction(ctx.sessionId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
37
305
|
// 3. Paralysis Check (from session state tracker)
|
|
38
306
|
const stuckThreshold = config.get('thresholds.stuck_loops_trigger') || 3;
|
|
39
307
|
const inputThreshold = config.get('thresholds.cognitive_paralysis_input') || 4000;
|
|
@@ -64,56 +332,18 @@ export function handleLlmOutput(event, ctx) {
|
|
|
64
332
|
});
|
|
65
333
|
}
|
|
66
334
|
// ═══ Thinking OS: Mental Model Usage Tracking ═══
|
|
67
|
-
trackThinkingModelUsage(
|
|
335
|
+
trackThinkingModelUsage({
|
|
336
|
+
text,
|
|
337
|
+
wctx,
|
|
338
|
+
sessionId: ctx.sessionId,
|
|
339
|
+
runId: event.runId,
|
|
340
|
+
assistantTurnId,
|
|
341
|
+
createdAt,
|
|
342
|
+
logger: ctx.logger,
|
|
343
|
+
});
|
|
68
344
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/let me (first )?(understand|map|outline|survey|review the (structure|architecture|dependencies))/i,
|
|
72
|
-
/让我先(梳理|了解|画出|理解|查看)(一下)?(结构|架构|依赖|全貌)/,
|
|
73
|
-
],
|
|
74
|
-
'T-02': [
|
|
75
|
-
/(type|test|contract|schema|interface) (constraint|requirement|check|validation)/i,
|
|
76
|
-
/we (must|need to) (respect|follow|adhere to) the/i,
|
|
77
|
-
/(必须|需要).*?(遵守|符合|满足).*?(类型|测试|契约|接口|规范)/,
|
|
78
|
-
],
|
|
79
|
-
'T-03': [
|
|
80
|
-
/based on (the |this )?(evidence|logs?|output|error|stack trace|test result)/i,
|
|
81
|
-
/let me (check|verify|confirm|read|look at) (the |)(actual|source|code|file|log)/i,
|
|
82
|
-
/根据(日志|证据|输出|报错|堆栈|测试结果)/,
|
|
83
|
-
],
|
|
84
|
-
'T-04': [
|
|
85
|
-
/this (is|would be) (irreversible|destructive|permanent|not easily undone)/i,
|
|
86
|
-
/(reversible|can be undone|safely roll back)/i,
|
|
87
|
-
/(不可逆|破坏性|永久的|无法回滚|可以回滚|安全地撤销)/,
|
|
88
|
-
],
|
|
89
|
-
'T-05': [
|
|
90
|
-
/we (must|should) (not|never|avoid|prevent|ensure we don't)/i,
|
|
91
|
-
/(critical|important) (not to|that we don't|to avoid)/i,
|
|
92
|
-
/(绝不能|必须避免|不可以|禁止|确保不会)/,
|
|
93
|
-
],
|
|
94
|
-
'T-06': [
|
|
95
|
-
/(simpl(er|est|ify)|minimal|straightforward|lean) (approach|solution|fix|implementation)/i,
|
|
96
|
-
/(simple is better|keep it simple|no need to over)/i,
|
|
97
|
-
/(最简(单|洁)|精简|没有必要(过度|额外))/,
|
|
98
|
-
],
|
|
99
|
-
'T-07': [
|
|
100
|
-
/(minimal|smallest|narrowest|least) (change|diff|modification|impact)/i,
|
|
101
|
-
/only (change|modify|touch|edit) (the |what)/i,
|
|
102
|
-
/(最小(改动|变更|修改)|只(改|动|修))/,
|
|
103
|
-
],
|
|
104
|
-
'T-08': [
|
|
105
|
-
/this (error|failure|issue) (tells us|indicates|signals|suggests|means)/i,
|
|
106
|
-
/let me (stop|pause|step back|reconsider|rethink)/i,
|
|
107
|
-
/这个(错误|失败|问题)(告诉我们|表明|说明|意味)/,
|
|
108
|
-
/让我(停下|暂停|退一步|重新(考虑|思考|审视))/,
|
|
109
|
-
],
|
|
110
|
-
'T-09': [
|
|
111
|
-
/(break|split|decompose|divide) (this |the task |it )?(into|down)/i,
|
|
112
|
-
/(step 1|first,? (we|i|let's)|phase 1)/i,
|
|
113
|
-
/(拆分|分解|分步|分阶段|第一步)/,
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
function trackThinkingModelUsage(text, wctx, sessionId) {
|
|
345
|
+
function trackThinkingModelUsage(args) {
|
|
346
|
+
const { text, wctx, sessionId, runId, assistantTurnId, createdAt, logger } = args;
|
|
117
347
|
const logPath = wctx.resolve('THINKING_OS_USAGE');
|
|
118
348
|
const logDir = path.dirname(logPath);
|
|
119
349
|
if (!fs.existsSync(logDir))
|
|
@@ -127,27 +357,75 @@ function trackThinkingModelUsage(text, wctx, sessionId) {
|
|
|
127
357
|
console.error(`[PD:LLM] Failed to parse thinking OS usage log: ${String(e)}`);
|
|
128
358
|
}
|
|
129
359
|
}
|
|
130
|
-
|
|
131
|
-
for (const
|
|
132
|
-
|
|
133
|
-
if (pattern.test(text)) {
|
|
134
|
-
usageLog[modelId] = (usageLog[modelId] || 0) + 1;
|
|
135
|
-
anyMatch = true;
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
360
|
+
const matches = detectThinkingModelMatches(text);
|
|
361
|
+
for (const match of matches) {
|
|
362
|
+
usageLog[match.modelId] = (usageLog[match.modelId] || 0) + 1;
|
|
139
363
|
}
|
|
140
364
|
usageLog['_total_turns'] = (usageLog['_total_turns'] || 0) + 1;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
365
|
+
try {
|
|
366
|
+
fs.writeFileSync(logPath, JSON.stringify(usageLog, null, 2), 'utf8');
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
console.error(`[PD:LLM] Failed to write thinking OS usage log: ${String(e)}`);
|
|
370
|
+
}
|
|
371
|
+
if (matches.length === 0) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (sessionId) {
|
|
375
|
+
recordThinkingCheckpoint(sessionId, wctx.workspaceDir);
|
|
376
|
+
}
|
|
377
|
+
if (!sessionId || !assistantTurnId) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const uiDb = new ControlUiDatabase({ workspaceDir: wctx.workspaceDir });
|
|
381
|
+
try {
|
|
382
|
+
const recentContext = uiDb.getRecentThinkingContext(sessionId, createdAt);
|
|
383
|
+
const toolContext = recentContext.toolCalls.map((call) => ({
|
|
384
|
+
toolName: call.toolName,
|
|
385
|
+
outcome: call.outcome,
|
|
386
|
+
errorType: call.errorType,
|
|
387
|
+
}));
|
|
388
|
+
const painContext = recentContext.painEvents.map((event) => ({
|
|
389
|
+
source: event.source,
|
|
390
|
+
score: event.score,
|
|
391
|
+
}));
|
|
392
|
+
const principleContext = recentContext.principleEvents.map((event) => ({
|
|
393
|
+
principleId: event.principleId,
|
|
394
|
+
eventType: event.eventType,
|
|
395
|
+
}));
|
|
396
|
+
const triggerExcerpt = text.length > 280 ? `${text.slice(0, 277)}...` : text;
|
|
397
|
+
for (const match of matches) {
|
|
398
|
+
const scenarios = deriveThinkingScenarios(match.modelId, {
|
|
399
|
+
recentToolCalls: toolContext,
|
|
400
|
+
recentPainEvents: painContext,
|
|
401
|
+
recentGateBlocks: recentContext.gateBlocks.map((block) => ({
|
|
402
|
+
toolName: block.toolName,
|
|
403
|
+
reason: block.reason,
|
|
404
|
+
})),
|
|
405
|
+
recentUserCorrections: recentContext.userCorrections.map((correction) => ({
|
|
406
|
+
correctionCue: correction.correctionCue,
|
|
407
|
+
})),
|
|
408
|
+
recentPrincipleEvents: principleContext,
|
|
409
|
+
});
|
|
410
|
+
uiDb.recordThinkingModelEvent({
|
|
411
|
+
sessionId,
|
|
412
|
+
runId,
|
|
413
|
+
assistantTurnId,
|
|
414
|
+
modelId: match.modelId,
|
|
415
|
+
matchedPattern: match.matchedPattern,
|
|
416
|
+
scenarioJson: scenarios,
|
|
417
|
+
toolContextJson: toolContext,
|
|
418
|
+
painContextJson: painContext,
|
|
419
|
+
principleContextJson: principleContext,
|
|
420
|
+
triggerExcerpt,
|
|
421
|
+
createdAt,
|
|
422
|
+
});
|
|
151
423
|
}
|
|
152
424
|
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
logger?.warn?.(`[PD:LLM] Failed to persist thinking model events: ${String(error)}`);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
uiDb.dispose();
|
|
430
|
+
}
|
|
153
431
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PluginHookBeforeMessageWriteEvent, PluginHookBeforeMessageWriteResult } from '../openclaw-sdk.js';
|
|
2
|
+
export declare function sanitizeAssistantText(text: string): string;
|
|
3
|
+
export declare function handleBeforeMessageWrite(event: PluginHookBeforeMessageWriteEvent): PluginHookBeforeMessageWriteResult | void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const INTERNAL_TAG_PATTERNS = [
|
|
2
|
+
/\[EMOTIONAL_DAMAGE_DETECTED(?::(?:mild|moderate|severe))?\]/gi,
|
|
3
|
+
/\[EMPATHY_ROLLBACK_REQUEST\]/gi,
|
|
4
|
+
/<empathy\s+[^>]*\/?>(?:<\/empathy>)?/gi,
|
|
5
|
+
];
|
|
6
|
+
export function sanitizeAssistantText(text) {
|
|
7
|
+
let result = text;
|
|
8
|
+
for (const pattern of INTERNAL_TAG_PATTERNS) {
|
|
9
|
+
result = result.replace(pattern, '');
|
|
10
|
+
}
|
|
11
|
+
return result
|
|
12
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
13
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
export function handleBeforeMessageWrite(event) {
|
|
17
|
+
const msg = event.message;
|
|
18
|
+
if (!msg || msg.role !== 'assistant')
|
|
19
|
+
return;
|
|
20
|
+
if (typeof msg.content === 'string') {
|
|
21
|
+
const sanitized = sanitizeAssistantText(msg.content);
|
|
22
|
+
if (sanitized !== msg.content) {
|
|
23
|
+
return { message: { ...msg, content: sanitized } };
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(msg.content)) {
|
|
28
|
+
const next = msg.content.map((part) => {
|
|
29
|
+
if (part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string') {
|
|
30
|
+
return { ...part, text: sanitizeAssistantText(part.text) };
|
|
31
|
+
}
|
|
32
|
+
return part;
|
|
33
|
+
});
|
|
34
|
+
return { message: { ...msg, content: next } };
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
package/dist/hooks/pain.js
CHANGED
|
@@ -2,11 +2,25 @@ import * as fs from 'fs';
|
|
|
2
2
|
import { isRisky, normalizePath } from '../utils/io.js';
|
|
3
3
|
import { normalizeProfile } from '../core/profile.js';
|
|
4
4
|
import { computePainScore, writePainFlag } from '../core/pain.js';
|
|
5
|
-
import { trackFriction, resetFriction } from '../core/session-tracker.js';
|
|
5
|
+
import { getSession, trackFriction, resetFriction, getInjectedProbationIds, clearInjectedProbationIds } from '../core/session-tracker.js';
|
|
6
6
|
import { denoiseError, computeHash } from '../utils/hashing.js';
|
|
7
7
|
import { SystemLogger } from '../core/system-logger.js';
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file', 'replace'];
|
|
10
|
+
function shouldAttributePrincipleToTool(principle, toolName) {
|
|
11
|
+
return principle.contextTags.includes(toolName) || principle.trigger.includes(toolName);
|
|
12
|
+
}
|
|
13
|
+
function emitPainDetectedEvent(wctx, event) {
|
|
14
|
+
try {
|
|
15
|
+
wctx.evolutionReducer.emitSync(event);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
SystemLogger.log(wctx.workspaceDir, 'EVOLUTION_EMIT_WARN', `Failed to emit evolution event: ${String(e)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createPainId(sessionId) {
|
|
22
|
+
return `pain_${Date.now()}_${computeHash(sessionId).slice(0, 8)}`;
|
|
23
|
+
}
|
|
10
24
|
export function handleAfterToolCall(event, ctx, api) {
|
|
11
25
|
const effectiveWorkspaceDir = ctx.workspaceDir || api?.workspaceDir || api?.resolvePath?.('.');
|
|
12
26
|
if (!effectiveWorkspaceDir) {
|
|
@@ -17,6 +31,8 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
17
31
|
const eventLog = wctx.eventLog;
|
|
18
32
|
const trust = wctx.trust;
|
|
19
33
|
const sessionId = ctx.sessionId || 'unknown';
|
|
34
|
+
const sessionState = ctx.sessionId ? getSession(ctx.sessionId) : undefined;
|
|
35
|
+
const gfiBefore = sessionState?.currentGfi ?? 0;
|
|
20
36
|
const params = event.params;
|
|
21
37
|
// ── Track A: Empirical Friction (GFI) ──
|
|
22
38
|
// 0. Special Case: Manual Pain Intervention
|
|
@@ -30,6 +46,25 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
30
46
|
reason: `User intervention: ${reason}`,
|
|
31
47
|
isRisky: true
|
|
32
48
|
});
|
|
49
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
50
|
+
sessionId,
|
|
51
|
+
source: 'manual',
|
|
52
|
+
score: 100,
|
|
53
|
+
reason: `User intervention: ${reason}`,
|
|
54
|
+
origin: 'user_manual',
|
|
55
|
+
});
|
|
56
|
+
emitPainDetectedEvent(wctx, {
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
type: 'pain_detected',
|
|
59
|
+
data: {
|
|
60
|
+
painId: createPainId(sessionId),
|
|
61
|
+
painType: 'user_frustration',
|
|
62
|
+
source: event.toolName,
|
|
63
|
+
reason: `User intervention: ${reason}`,
|
|
64
|
+
score: 100,
|
|
65
|
+
sessionId,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
33
68
|
return;
|
|
34
69
|
}
|
|
35
70
|
// 1. Determine if this was a failure
|
|
@@ -52,13 +87,16 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
52
87
|
try {
|
|
53
88
|
profile = normalizeProfile(JSON.parse(fs.readFileSync(profilePath, 'utf8')));
|
|
54
89
|
}
|
|
55
|
-
catch (
|
|
90
|
+
catch (e) {
|
|
91
|
+
SystemLogger.log(effectiveWorkspaceDir, 'PROFILE_PARSE_WARN', `Failed to parse PROFILE.json: ${String(e)}`);
|
|
92
|
+
}
|
|
56
93
|
}
|
|
57
94
|
const isRisk = isRisky(relPath, profile.risk_paths);
|
|
58
95
|
trust.recordFailure(isRisk ? 'risky' : 'tool', {
|
|
59
96
|
sessionId,
|
|
60
97
|
api,
|
|
61
|
-
toolName: event.toolName
|
|
98
|
+
toolName: event.toolName,
|
|
99
|
+
error: event.error // Pass error for timeout detection
|
|
62
100
|
});
|
|
63
101
|
// Record tool call failure event
|
|
64
102
|
eventLog.recordToolCall(sessionId, {
|
|
@@ -70,16 +108,56 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
70
108
|
consecutiveErrors: updatedState.consecutiveErrors,
|
|
71
109
|
exitCode,
|
|
72
110
|
});
|
|
111
|
+
wctx.trajectory?.recordToolCall?.({
|
|
112
|
+
sessionId,
|
|
113
|
+
toolName: event.toolName,
|
|
114
|
+
outcome: 'failure',
|
|
115
|
+
durationMs: event.durationMs,
|
|
116
|
+
exitCode,
|
|
117
|
+
errorType,
|
|
118
|
+
errorMessage: event.error ? String(event.error) : undefined,
|
|
119
|
+
gfiBefore,
|
|
120
|
+
gfiAfter: updatedState.currentGfi,
|
|
121
|
+
paramsJson: event.params,
|
|
122
|
+
});
|
|
123
|
+
const injectedProbationIds = getInjectedProbationIds(sessionId, effectiveWorkspaceDir);
|
|
124
|
+
for (const id of injectedProbationIds) {
|
|
125
|
+
const principle = wctx.evolutionReducer.getPrincipleById(id);
|
|
126
|
+
const shouldAttribute = !!principle && shouldAttributePrincipleToTool(principle, event.toolName);
|
|
127
|
+
if (shouldAttribute) {
|
|
128
|
+
wctx.evolutionReducer.recordProbationFeedback(id, false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
clearInjectedProbationIds(sessionId, effectiveWorkspaceDir);
|
|
73
132
|
}
|
|
74
133
|
else {
|
|
75
134
|
// ── SUCCESS BRANCH ──
|
|
76
|
-
resetFriction(sessionId, effectiveWorkspaceDir);
|
|
135
|
+
const resetState = resetFriction(sessionId, effectiveWorkspaceDir);
|
|
77
136
|
// 👈 Record success to reset failure streak and earn minor trust (if constructive)
|
|
78
137
|
trust.recordSuccess('tool_success', {
|
|
79
138
|
sessionId,
|
|
80
139
|
api,
|
|
81
140
|
toolName: event.toolName // 👈 NEW: Pass toolName for classification
|
|
82
141
|
});
|
|
142
|
+
const injectedProbationIds = getInjectedProbationIds(sessionId, effectiveWorkspaceDir);
|
|
143
|
+
for (const id of injectedProbationIds) {
|
|
144
|
+
const principle = wctx.evolutionReducer.getPrincipleById(id);
|
|
145
|
+
const shouldAttribute = !!principle && shouldAttributePrincipleToTool(principle, event.toolName);
|
|
146
|
+
if (shouldAttribute) {
|
|
147
|
+
wctx.evolutionReducer.recordProbationFeedback(id, true);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
clearInjectedProbationIds(sessionId, effectiveWorkspaceDir);
|
|
151
|
+
wctx.trajectory?.recordToolCall?.({
|
|
152
|
+
sessionId,
|
|
153
|
+
toolName: event.toolName,
|
|
154
|
+
outcome: 'success',
|
|
155
|
+
durationMs: event.durationMs,
|
|
156
|
+
exitCode,
|
|
157
|
+
gfiBefore,
|
|
158
|
+
gfiAfter: resetState.currentGfi,
|
|
159
|
+
paramsJson: event.params,
|
|
160
|
+
});
|
|
83
161
|
if (WRITE_TOOLS.includes(event.toolName)) {
|
|
84
162
|
const filePath = params.file_path || params.path || params.file;
|
|
85
163
|
eventLog.recordToolCall(sessionId, {
|
|
@@ -128,7 +206,9 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
128
206
|
try {
|
|
129
207
|
profile = normalizeProfile(JSON.parse(fs.readFileSync(profilePath, 'utf8')));
|
|
130
208
|
}
|
|
131
|
-
catch (
|
|
209
|
+
catch (e) {
|
|
210
|
+
SystemLogger.log(effectiveWorkspaceDir, 'PROFILE_PARSE_WARN', `Failed to parse PROFILE.json: ${String(e)}`);
|
|
211
|
+
}
|
|
132
212
|
}
|
|
133
213
|
const isRisk = isRisky(relPath, profile.risk_paths);
|
|
134
214
|
const painScore = computePainScore(1, false, false, isRisk ? 20 : 0, effectiveWorkspaceDir);
|
|
@@ -146,6 +226,26 @@ export function handleAfterToolCall(event, ctx, api) {
|
|
|
146
226
|
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
147
227
|
isRisky: isRisk,
|
|
148
228
|
});
|
|
229
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
230
|
+
sessionId,
|
|
231
|
+
source: 'tool_failure',
|
|
232
|
+
score: painScore,
|
|
233
|
+
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
234
|
+
severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
|
|
235
|
+
origin: 'system_infer',
|
|
236
|
+
});
|
|
237
|
+
emitPainDetectedEvent(wctx, {
|
|
238
|
+
ts: new Date().toISOString(),
|
|
239
|
+
type: 'pain_detected',
|
|
240
|
+
data: {
|
|
241
|
+
painId: createPainId(sessionId),
|
|
242
|
+
painType: 'tool_failure',
|
|
243
|
+
source: event.toolName,
|
|
244
|
+
reason: `Tool ${event.toolName} failed on ${relPath}`,
|
|
245
|
+
score: painScore,
|
|
246
|
+
sessionId,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
149
249
|
}
|
|
150
250
|
function extractErrorType(error) {
|
|
151
251
|
if (!error)
|