principles-disciple 1.92.0 → 1.94.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/hooks/after-tool-call-helpers.ts +577 -0
- package/src/hooks/after-tool-call-types.ts +105 -0
- package/src/hooks/gate-block-helper.ts +72 -29
- package/src/hooks/llm.ts +49 -29
- package/src/hooks/pain.ts +176 -462
- package/src/hooks/trajectory-evidence.ts +75 -0
- package/src/hooks/triage-adapter.ts +156 -0
- package/tests/hooks/gate-block-helper-profile.test.ts +186 -0
- package/tests/hooks/pain.test.ts +288 -0
- package/tests/hooks/triage-adapter.test.ts +260 -0
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
* had their own block persistence implementations.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import * as fs from 'fs';
|
|
13
14
|
import { getSession, trackBlock } from '../core/session-tracker.js';
|
|
14
15
|
import type { WorkspaceContext } from '../core/workspace-context.js';
|
|
15
16
|
import type { PluginHookBeforeToolCallResult } from '../openclaw-sdk.js';
|
|
16
17
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
17
18
|
import { emitPainDetectedEvent } from './pain.js';
|
|
19
|
+
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
20
|
+
import { evaluateEvidenceTriage } from './triage-adapter.js';
|
|
21
|
+
import { isRisky } from '../utils/io.js';
|
|
22
|
+
import { normalizeProfile } from '../core/profile.js';
|
|
23
|
+
import { SystemLogger } from '../core/system-logger.js';
|
|
18
24
|
import {
|
|
19
25
|
TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS,
|
|
20
26
|
TRAJECTORY_GATE_BLOCK_MAX_RETRIES
|
|
@@ -114,38 +120,75 @@ export function recordGateBlockAndReturn(
|
|
|
114
120
|
origin: 'system_infer',
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
// PEAT-B1: Evidence triage (feature-flagged)
|
|
124
|
+
let triageAdmitted = true;
|
|
125
|
+
const gateBlockTriageFlag = loadFeatureFlagFromConfig(wctx.workspaceDir, 'painEvidenceAdmission');
|
|
126
|
+
if (gateBlockTriageFlag.enabled) {
|
|
127
|
+
// Load profile with 1MB size guard, matching pain.ts pattern
|
|
128
|
+
const profilePath = wctx.resolve('PROFILE');
|
|
129
|
+
let profile = normalizeProfile({});
|
|
130
|
+
if (fs.existsSync(profilePath)) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(profilePath, 'utf8');
|
|
133
|
+
if (content.length > 1024 * 1024) {
|
|
134
|
+
logger.warn?.('[PD_GATE] PROFILE.json exceeds 1 MB, skipping');
|
|
135
|
+
SystemLogger.log(wctx.workspaceDir, 'PROFILE_PARSE_WARN', 'PROFILE.json exceeds 1 MB, skipping — fallback to non-risky');
|
|
136
|
+
} else {
|
|
137
|
+
profile = normalizeProfile(JSON.parse(content));
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
logger.warn?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
|
|
141
|
+
SystemLogger.log(wctx.workspaceDir, 'PROFILE_PARSE_WARN', `Failed to parse PROFILE.json: ${String(e)} — fallback to non-risky`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Real judgment for rulehost blocks: if a principle blocked an action on a risky path,
|
|
145
|
+
// it IS a high-confidence unsafe action. The pain score (45) is the evidence friction
|
|
146
|
+
// weight, NOT the action risk severity. The rulehost principle already determined
|
|
147
|
+
// this action was important enough to block — that is the real signal.
|
|
148
|
+
const isUnsafe = isRisky(filePath, profile.risk_paths);
|
|
149
|
+
const triage = evaluateEvidenceTriage('rulehost_block', GATE_BLOCK_PAIN_SCORE, {
|
|
150
|
+
isUnsafeHighConfidence: isUnsafe,
|
|
151
|
+
});
|
|
152
|
+
if (triage.decision !== 'admit') {
|
|
153
|
+
triageAdmitted = false;
|
|
154
|
+
logger.info?.(`[PD_GATE] Triage ${triage.decision}: ${triage.reason}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
130
157
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
const session = getSession(sessionId);
|
|
159
|
+
if (triageAdmitted) {
|
|
160
|
+
const gate = evaluatePainDiagnosticGate({
|
|
161
|
+
source: 'gate_blocked',
|
|
162
|
+
score: GATE_BLOCK_PAIN_SCORE,
|
|
163
|
+
currentGfi: session?.currentGfi ?? 0,
|
|
164
|
+
consecutiveErrors: session?.consecutiveErrors ?? 0,
|
|
165
|
+
sessionId,
|
|
166
|
+
errorHash: `${toolName}:${filePath}:${reason}`,
|
|
167
|
+
thresholds: {
|
|
168
|
+
painTrigger: wctx.config.get('thresholds.pain_trigger') || 40,
|
|
169
|
+
highSeverity: wctx.config.get('severity_thresholds.high') || 70,
|
|
143
170
|
},
|
|
144
|
-
}).catch((emitErr) => {
|
|
145
|
-
logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
|
|
146
171
|
});
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
|
|
173
|
+
if (gate.shouldDiagnose) {
|
|
174
|
+
void emitPainDetectedEvent(wctx, {
|
|
175
|
+
ts: new Date().toISOString(),
|
|
176
|
+
type: 'pain_detected',
|
|
177
|
+
data: {
|
|
178
|
+
painId: `gate_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
179
|
+
painType: 'user_frustration',
|
|
180
|
+
source: 'gate_blocked',
|
|
181
|
+
reason: `Gate blocked ${toolName} on ${filePath}: ${reason}`,
|
|
182
|
+
score: GATE_BLOCK_PAIN_SCORE,
|
|
183
|
+
sessionId,
|
|
184
|
+
agentId: 'main',
|
|
185
|
+
},
|
|
186
|
+
}).catch((emitErr) => {
|
|
187
|
+
logWarn(`[PD_GATE] Failed to emit gate block pain event: ${String(emitErr)}`);
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
logger.info?.(`[PD_GATE] Gate block recorded without Runtime V2 diagnosis: ${gate.detail}`);
|
|
191
|
+
}
|
|
149
192
|
}
|
|
150
193
|
}
|
|
151
194
|
|
package/src/hooks/llm.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { sanitizeAssistantText } from './message-sanitize.js';
|
|
|
11
11
|
import { atomicWriteFileSync } from '../utils/io.js';
|
|
12
12
|
import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
|
|
13
13
|
import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
|
|
14
|
+
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
15
|
+
import { resolveSourceKindFromLlmDetection, evaluateEvidenceTriage } from './triage-adapter.js';
|
|
14
16
|
|
|
15
17
|
export interface EmpathySignal {
|
|
16
18
|
detected: boolean;
|
|
@@ -240,26 +242,26 @@ export function handleLlmOutput(
|
|
|
240
242
|
// GFI-triggered pain: when accumulated friction crosses highGfi threshold,
|
|
241
243
|
// emit pain signal even if L1 detection didn't fire.
|
|
242
244
|
const highGfiThreshold = Math.max(config.get('severity_thresholds.high') || 70, painTriggerThreshold + 30);
|
|
245
|
+
let isGfiTriggered = false;
|
|
243
246
|
if (state.currentGfi >= highGfiThreshold && painScore < painTriggerThreshold) {
|
|
244
247
|
painScore = Math.min(state.currentGfi, 60);
|
|
245
248
|
source = 'user_empathy';
|
|
249
|
+
isGfiTriggered = true;
|
|
246
250
|
matchedReason = `Accumulated GFI (${state.currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Source: empathy keyword friction.`;
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
if (painScore >= painTriggerThreshold) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
});
|
|
254
|
+
// PEAT-B1: Evidence triage (feature-flagged)
|
|
255
|
+
let triageAdmitted = true;
|
|
256
|
+
const llmTriageFlag = loadFeatureFlagFromConfig(ctx.workspaceDir!, 'painEvidenceAdmission');
|
|
257
|
+
if (llmTriageFlag.enabled) {
|
|
258
|
+
const sourceKind = resolveSourceKindFromLlmDetection(source, isGfiTriggered);
|
|
259
|
+
const triage = evaluateEvidenceTriage(sourceKind, painScore);
|
|
260
|
+
if (triage.decision !== 'admit') {
|
|
261
|
+
triageAdmitted = false;
|
|
262
|
+
ctx.logger?.info?.(`[PD:LLM] Triage ${triage.decision}: ${triage.reason}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
263
265
|
|
|
264
266
|
eventLog.recordPainSignal(ctx.sessionId, {
|
|
265
267
|
score: painScore,
|
|
@@ -268,25 +270,43 @@ export function handleLlmOutput(
|
|
|
268
270
|
isRisky: false
|
|
269
271
|
});
|
|
270
272
|
|
|
271
|
-
if (
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
agentId: ctx.agentId,
|
|
284
|
-
provenance: 'openclaw_context_bound',
|
|
285
|
-
evidence,
|
|
273
|
+
if (triageAdmitted) {
|
|
274
|
+
const gate = evaluatePainDiagnosticGate({
|
|
275
|
+
source: source === 'llm_paralysis' ? 'llm_paralysis' : 'semantic',
|
|
276
|
+
score: painScore,
|
|
277
|
+
currentGfi: state.currentGfi,
|
|
278
|
+
consecutiveErrors: state.consecutiveErrors,
|
|
279
|
+
sessionId: ctx.sessionId || 'unknown',
|
|
280
|
+
errorHash: source,
|
|
281
|
+
thresholds: {
|
|
282
|
+
painTrigger: painTriggerThreshold,
|
|
283
|
+
highSeverity: config.get('severity_thresholds.high') || 70,
|
|
284
|
+
semanticPain: Math.max(painTriggerThreshold, 60),
|
|
286
285
|
},
|
|
287
286
|
});
|
|
287
|
+
|
|
288
|
+
if (gate.shouldDiagnose) {
|
|
289
|
+
const evidence = buildTrajectoryEvidence(wctx, ctx.sessionId || 'unknown');
|
|
290
|
+
emitPainDetectedEvent(wctx, {
|
|
291
|
+
ts: new Date().toISOString(),
|
|
292
|
+
type: 'pain_detected',
|
|
293
|
+
data: {
|
|
294
|
+
painId: `llm_${Date.now()}`,
|
|
295
|
+
painType: 'user_frustration' as const,
|
|
296
|
+
source,
|
|
297
|
+
reason: `${matchedReason}; diagnosticGate=${gate.reason}`,
|
|
298
|
+
score: painScore,
|
|
299
|
+
sessionId: ctx.sessionId || 'unknown',
|
|
300
|
+
agentId: ctx.agentId,
|
|
301
|
+
provenance: 'openclaw_context_bound',
|
|
302
|
+
evidence,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
} else {
|
|
306
|
+
ctx.logger?.info?.(`[PD:LLM] Pain signal recorded without Runtime V2 diagnosis: ${gate.detail}`);
|
|
307
|
+
}
|
|
288
308
|
} else {
|
|
289
|
-
ctx.logger?.info?.(`[PD:LLM]
|
|
309
|
+
ctx.logger?.info?.(`[PD:LLM] Triage evidence-only: pain signal recorded without diagnosis or gate evaluation`);
|
|
290
310
|
}
|
|
291
311
|
}
|
|
292
312
|
|