principles-disciple 1.140.0 → 1.141.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/core/detection-funnel.d.ts +25 -11
- package/dist/core/detection-funnel.js +32 -80
- package/dist/core/pain-diagnostic-gate.d.ts +26 -28
- package/dist/core/pain-diagnostic-gate.js +42 -89
- package/dist/hooks/raw-observation-adapter.d.ts +9 -50
- package/dist/hooks/raw-observation-adapter.js +9 -164
- package/dist/hooks/raw-observation-types.d.ts +6 -57
- package/dist/hooks/raw-observation-types.js +5 -9
- package/dist/hooks/triage-adapter.js +6 -26
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -1,24 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Funnel — PRI-446 thin adapter
|
|
3
|
+
*
|
|
4
|
+
* The pure three-layer funnel logic (LRU cache, async queue, layer dispatch)
|
|
5
|
+
* now lives in principles-core
|
|
6
|
+
* (runtime-v2/detection/detection-funnel-policy.ts). This file is a thin shell
|
|
7
|
+
* that wires DetectionFunnelCore with the two environment-coupled dependencies
|
|
8
|
+
* the plugin owns:
|
|
9
|
+
* - crypto.createHash (the L2 cache key hasher)
|
|
10
|
+
* - PainDictionary.match (the L1 exact matcher)
|
|
11
|
+
* - shouldIgnorePainProtocolText (the protocol-token gate)
|
|
12
|
+
*
|
|
13
|
+
* It re-exports DetectionFunnel (same class name + constructor signature) and
|
|
14
|
+
* DetectionResult so DetectionService, llm.ts, and evolution-worker keep working
|
|
15
|
+
* unchanged.
|
|
16
|
+
*
|
|
17
|
+
* ERR checklist:
|
|
18
|
+
* - ERR-011: this is a thin adapter delegating to core pure logic.
|
|
19
|
+
*/
|
|
1
20
|
import { type PainDictionary } from './dictionary.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
severity?: number;
|
|
5
|
-
ruleId?: string;
|
|
6
|
-
source: 'l1_exact' | 'l2_cache' | 'l3_async_queued' | 'l3_semantic_hit';
|
|
7
|
-
}
|
|
21
|
+
import { type DetectionResult } from '@principles/core/runtime-v2';
|
|
22
|
+
export type { DetectionResult };
|
|
8
23
|
/**
|
|
9
24
|
* Orchestrates the three-layer detection funnel for pain signals.
|
|
25
|
+
*
|
|
26
|
+
* Delegates to the core DetectionFunnelCore, injecting the crypto hasher,
|
|
27
|
+
* the dictionary matcher, and the protocol-token gate.
|
|
10
28
|
*/
|
|
11
29
|
export declare class DetectionFunnel {
|
|
12
|
-
private readonly
|
|
13
|
-
private asyncQueue;
|
|
14
|
-
private readonly dictionary;
|
|
30
|
+
private readonly core;
|
|
15
31
|
constructor(dictionary: PainDictionary);
|
|
16
32
|
/**
|
|
17
33
|
* Detects pain in the given text using L1 (Exact), L2 (Cache), and L3 (Async).
|
|
18
34
|
*/
|
|
19
35
|
detect(text: string): DetectionResult;
|
|
20
|
-
private static computeHash;
|
|
21
|
-
private enqueueAsync;
|
|
22
36
|
/**
|
|
23
37
|
* Internal method for the worker to update the cache after a semantic hit.
|
|
24
38
|
*/
|
|
@@ -1,104 +1,56 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { shouldIgnorePainProtocolText } from './dictionary.js';
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
2
|
+
* Detection Funnel — PRI-446 thin adapter
|
|
3
|
+
*
|
|
4
|
+
* The pure three-layer funnel logic (LRU cache, async queue, layer dispatch)
|
|
5
|
+
* now lives in principles-core
|
|
6
|
+
* (runtime-v2/detection/detection-funnel-policy.ts). This file is a thin shell
|
|
7
|
+
* that wires DetectionFunnelCore with the two environment-coupled dependencies
|
|
8
|
+
* the plugin owns:
|
|
9
|
+
* - crypto.createHash (the L2 cache key hasher)
|
|
10
|
+
* - PainDictionary.match (the L1 exact matcher)
|
|
11
|
+
* - shouldIgnorePainProtocolText (the protocol-token gate)
|
|
12
|
+
*
|
|
13
|
+
* It re-exports DetectionFunnel (same class name + constructor signature) and
|
|
14
|
+
* DetectionResult so DetectionService, llm.ts, and evolution-worker keep working
|
|
15
|
+
* unchanged.
|
|
16
|
+
*
|
|
17
|
+
* ERR checklist:
|
|
18
|
+
* - ERR-011: this is a thin adapter delegating to core pure logic.
|
|
5
19
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
constructor(maxSize = 100) {
|
|
10
|
-
this.cache = new Map();
|
|
11
|
-
this.maxSize = maxSize;
|
|
12
|
-
}
|
|
13
|
-
get(key) {
|
|
14
|
-
const item = this.cache.get(key);
|
|
15
|
-
if (item !== undefined) {
|
|
16
|
-
// Refresh: delete and re-insert
|
|
17
|
-
this.cache.delete(key);
|
|
18
|
-
this.cache.set(key, item);
|
|
19
|
-
}
|
|
20
|
-
return item;
|
|
21
|
-
}
|
|
22
|
-
set(key, value) {
|
|
23
|
-
if (this.cache.has(key)) {
|
|
24
|
-
this.cache.delete(key);
|
|
25
|
-
}
|
|
26
|
-
else if (this.cache.size >= this.maxSize) {
|
|
27
|
-
// Remove the oldest (first) item
|
|
28
|
-
const firstKey = this.cache.keys().next().value;
|
|
29
|
-
if (firstKey !== undefined) {
|
|
30
|
-
this.cache.delete(firstKey);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
this.cache.set(key, value);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
20
|
+
import { createHash } from 'crypto';
|
|
21
|
+
import { shouldIgnorePainProtocolText } from './dictionary.js';
|
|
22
|
+
import { DetectionFunnelCore, } from '@principles/core/runtime-v2';
|
|
36
23
|
/**
|
|
37
24
|
* Orchestrates the three-layer detection funnel for pain signals.
|
|
25
|
+
*
|
|
26
|
+
* Delegates to the core DetectionFunnelCore, injecting the crypto hasher,
|
|
27
|
+
* the dictionary matcher, and the protocol-token gate.
|
|
38
28
|
*/
|
|
39
29
|
export class DetectionFunnel {
|
|
40
|
-
|
|
41
|
-
asyncQueue = [];
|
|
42
|
-
dictionary;
|
|
30
|
+
core;
|
|
43
31
|
constructor(dictionary) {
|
|
44
|
-
this.
|
|
32
|
+
this.core = new DetectionFunnelCore({
|
|
33
|
+
match: (text) => dictionary.match(text),
|
|
34
|
+
hash: (text) => createHash('sha256').update(text).digest('hex'),
|
|
35
|
+
shouldIgnoreProtocol: shouldIgnorePainProtocolText,
|
|
36
|
+
});
|
|
45
37
|
}
|
|
46
38
|
/**
|
|
47
39
|
* Detects pain in the given text using L1 (Exact), L2 (Cache), and L3 (Async).
|
|
48
40
|
*/
|
|
49
41
|
detect(text) {
|
|
50
|
-
|
|
51
|
-
return { detected: false, source: 'l1_exact' };
|
|
52
|
-
}
|
|
53
|
-
// --- Layer 1: Exact Match (Sync) ---
|
|
54
|
-
const exactMatch = this.dictionary.match(text);
|
|
55
|
-
if (exactMatch) {
|
|
56
|
-
return {
|
|
57
|
-
detected: true,
|
|
58
|
-
severity: exactMatch.severity,
|
|
59
|
-
ruleId: exactMatch.ruleId,
|
|
60
|
-
source: 'l1_exact'
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
// --- Layer 2: LRU Cache (Sync) ---
|
|
64
|
-
const hash = DetectionFunnel.computeHash(text);
|
|
65
|
-
const cached = this.cache.get(hash);
|
|
66
|
-
if (cached) {
|
|
67
|
-
return {
|
|
68
|
-
detected: cached.detected,
|
|
69
|
-
severity: cached.severity,
|
|
70
|
-
source: 'l2_cache'
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// --- Layer 3: Async Semantic Queue ---
|
|
74
|
-
this.enqueueAsync(text);
|
|
75
|
-
return {
|
|
76
|
-
detected: false,
|
|
77
|
-
source: 'l3_async_queued'
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
static computeHash(text) {
|
|
81
|
-
return createHash('sha256').update(text).digest('hex');
|
|
82
|
-
}
|
|
83
|
-
enqueueAsync(text) {
|
|
84
|
-
if (this.asyncQueue.length < 1000) {
|
|
85
|
-
this.asyncQueue.push(text);
|
|
86
|
-
}
|
|
87
|
-
// Worker will pick this up and perform semantic search via createMemorySearchTool
|
|
42
|
+
return this.core.detect(text);
|
|
88
43
|
}
|
|
89
44
|
/**
|
|
90
45
|
* Internal method for the worker to update the cache after a semantic hit.
|
|
91
46
|
*/
|
|
92
47
|
updateCache(text, result) {
|
|
93
|
-
|
|
94
|
-
this.cache.set(hash, result);
|
|
48
|
+
this.core.updateCache(text, result);
|
|
95
49
|
}
|
|
96
50
|
/**
|
|
97
51
|
* Retrieves and clears the current asynchronous queue.
|
|
98
52
|
*/
|
|
99
53
|
flushQueue() {
|
|
100
|
-
|
|
101
|
-
this.asyncQueue = [];
|
|
102
|
-
return queue;
|
|
54
|
+
return this.core.flushQueue();
|
|
103
55
|
}
|
|
104
56
|
}
|
|
@@ -1,30 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
export interface PainDiagnosticGateDecision {
|
|
23
|
-
shouldDiagnose: boolean;
|
|
24
|
-
reason: PainDiagnosticGateReason;
|
|
25
|
-
episodeKey: string;
|
|
26
|
-
detail: string;
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pain Diagnostic Gate — PRI-446 thin adapter
|
|
3
|
+
*
|
|
4
|
+
* The pure decision logic (threshold tree, cooldown comparison, episode-key
|
|
5
|
+
* construction) now lives in principles-core
|
|
6
|
+
* (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
|
|
7
|
+
* stateful adapter that owns:
|
|
8
|
+
* - the cooldown Map (lastDiagnosedAtByEpisode)
|
|
9
|
+
* - Date.now() injection
|
|
10
|
+
* - SystemLogger for unknown-source telemetry
|
|
11
|
+
*
|
|
12
|
+
* It preserves the original export names (evaluatePainDiagnosticGate,
|
|
13
|
+
* isCooldownActiveForEpisode, resetPainDiagnosticGateForTest) so all 5 callers
|
|
14
|
+
* (gate-block-helper, llm, pain, prompt x2) and the characterization test keep
|
|
15
|
+
* working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* ERR checklist:
|
|
18
|
+
* - ERR-011: this is a stateful adapter delegating to core pure logic.
|
|
19
|
+
*/
|
|
20
|
+
import { type PainDiagnosticSource, type PainDiagnosticGateReason, type PainDiagnosticGateInput, type PainDiagnosticGateDecision } from '@principles/core/runtime-v2';
|
|
21
|
+
export type { PainDiagnosticSource, PainDiagnosticGateReason, PainDiagnosticGateInput, PainDiagnosticGateDecision, };
|
|
28
22
|
export declare function resetPainDiagnosticGateForTest(): void;
|
|
29
23
|
/**
|
|
30
24
|
* Check whether cooldown is currently active for a given episode.
|
|
@@ -32,5 +26,9 @@ export declare function resetPainDiagnosticGateForTest(): void;
|
|
|
32
26
|
* with the PainDiagnosticGate's cooldown state.
|
|
33
27
|
*/
|
|
34
28
|
export declare function isCooldownActiveForEpisode(source: string, sessionId: string | undefined, errorHash: string | undefined, cooldownMs?: number): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate the pain diagnostic gate. Delegates the pure decision to core and,
|
|
31
|
+
* when the decision is to diagnose, records the current time against the
|
|
32
|
+
* episode so subsequent calls within the cooldown window are suppressed.
|
|
33
|
+
*/
|
|
35
34
|
export declare function evaluatePainDiagnosticGate(input: PainDiagnosticGateInput): PainDiagnosticGateDecision;
|
|
36
|
-
export {};
|
|
@@ -1,42 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pain Diagnostic Gate — PRI-446 thin adapter
|
|
3
|
+
*
|
|
4
|
+
* The pure decision logic (threshold tree, cooldown comparison, episode-key
|
|
5
|
+
* construction) now lives in principles-core
|
|
6
|
+
* (runtime-v2/pain-gate/pain-diagnostic-gate-policy.ts). This file is the
|
|
7
|
+
* stateful adapter that owns:
|
|
8
|
+
* - the cooldown Map (lastDiagnosedAtByEpisode)
|
|
9
|
+
* - Date.now() injection
|
|
10
|
+
* - SystemLogger for unknown-source telemetry
|
|
11
|
+
*
|
|
12
|
+
* It preserves the original export names (evaluatePainDiagnosticGate,
|
|
13
|
+
* isCooldownActiveForEpisode, resetPainDiagnosticGateForTest) so all 5 callers
|
|
14
|
+
* (gate-block-helper, llm, pain, prompt x2) and the characterization test keep
|
|
15
|
+
* working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* ERR checklist:
|
|
18
|
+
* - ERR-011: this is a stateful adapter delegating to core pure logic.
|
|
19
|
+
*/
|
|
1
20
|
import { SystemLogger } from './system-logger.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'tool_failure',
|
|
5
|
-
'dispatch_error',
|
|
6
|
-
'gate_blocked',
|
|
7
|
-
'user_empathy',
|
|
8
|
-
'llm_paralysis',
|
|
9
|
-
'semantic',
|
|
10
|
-
'subagent_error',
|
|
11
|
-
];
|
|
12
|
-
const DEFAULT_COOLDOWN_MS = 15 * 60 * 1000;
|
|
21
|
+
import { evaluatePainDiagnosticGateDecision, normalizedSource, buildEpisodeKey, isCooldownActive as isCooldownActiveCore, } from '@principles/core/runtime-v2';
|
|
22
|
+
// Module-level cooldown state — owned by this adapter (core is stateless).
|
|
13
23
|
const lastDiagnosedAtByEpisode = new Map();
|
|
14
|
-
function normalizedSource(source) {
|
|
15
|
-
if (source.startsWith('llm_') && source !== 'llm_paralysis') {
|
|
16
|
-
return 'semantic';
|
|
17
|
-
}
|
|
18
|
-
if (!PAIN_DIAGNOSTIC_SOURCES.includes(source)) {
|
|
19
|
-
SystemLogger.log('', 'GATE_UNKNOWN_SOURCE', `Unknown pain source: "${source}"`);
|
|
20
|
-
}
|
|
21
|
-
return source;
|
|
22
|
-
}
|
|
23
|
-
function buildEpisodeKey(input) {
|
|
24
|
-
const source = normalizedSource(input.source);
|
|
25
|
-
const sessionId = input.sessionId || 'unknown';
|
|
26
|
-
const hash = input.errorHash || 'no-hash';
|
|
27
|
-
return `${sessionId}:${source}:${hash}`;
|
|
28
|
-
}
|
|
29
|
-
function withinCooldown(input, episodeKey) {
|
|
30
|
-
const cooldownMs = input.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
31
|
-
if (cooldownMs <= 0)
|
|
32
|
-
return false;
|
|
33
|
-
const nowMs = input.nowMs ?? Date.now();
|
|
34
|
-
const last = lastDiagnosedAtByEpisode.get(episodeKey);
|
|
35
|
-
return last !== undefined && nowMs - last < cooldownMs;
|
|
36
|
-
}
|
|
37
|
-
function markDiagnosed(input, episodeKey) {
|
|
38
|
-
lastDiagnosedAtByEpisode.set(episodeKey, input.nowMs ?? Date.now());
|
|
39
|
-
}
|
|
40
24
|
export function resetPainDiagnosticGateForTest() {
|
|
41
25
|
lastDiagnosedAtByEpisode.clear();
|
|
42
26
|
}
|
|
@@ -46,60 +30,29 @@ export function resetPainDiagnosticGateForTest() {
|
|
|
46
30
|
* with the PainDiagnosticGate's cooldown state.
|
|
47
31
|
*/
|
|
48
32
|
export function isCooldownActiveForEpisode(source, sessionId, errorHash, cooldownMs) {
|
|
49
|
-
const episodeKey = buildEpisodeKey({ source, sessionId, errorHash });
|
|
50
|
-
|
|
33
|
+
const episodeKey = buildEpisodeKey({ source, sessionId, errorHash, score: 0, currentGfi: 0 });
|
|
34
|
+
const last = lastDiagnosedAtByEpisode.get(episodeKey);
|
|
35
|
+
const nowMs = Date.now();
|
|
36
|
+
return isCooldownActiveCore({ source, sessionId, errorHash, cooldownMs, nowMs, lastDiagnosedAtMs: last });
|
|
51
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the pain diagnostic gate. Delegates the pure decision to core and,
|
|
40
|
+
* when the decision is to diagnose, records the current time against the
|
|
41
|
+
* episode so subsequent calls within the cooldown window are suppressed.
|
|
42
|
+
*/
|
|
52
43
|
export function evaluatePainDiagnosticGate(input) {
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const highGfi = input.thresholds?.highGfi ?? Math.max(highSeverity, painTrigger + 30);
|
|
58
|
-
const repeatedFailure = input.thresholds?.repeatedFailure ?? 4;
|
|
59
|
-
const semanticPain = input.thresholds?.semanticPain ?? Math.max(painTrigger, 60);
|
|
60
|
-
const score = Number.isFinite(input.score) ? input.score : 0;
|
|
61
|
-
const currentGfi = Number.isFinite(input.currentGfi) ? input.currentGfi : 0;
|
|
62
|
-
const consecutiveErrors = Number.isFinite(input.consecutiveErrors) ? input.consecutiveErrors : 0;
|
|
63
|
-
const approve = (reason, detail) => {
|
|
64
|
-
if (withinCooldown(input, episodeKey)) {
|
|
65
|
-
return {
|
|
66
|
-
shouldDiagnose: false,
|
|
67
|
-
reason: 'cooldown',
|
|
68
|
-
episodeKey,
|
|
69
|
-
detail: `recently diagnosed; ${detail}`,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
markDiagnosed(input, episodeKey);
|
|
73
|
-
return { shouldDiagnose: true, reason, episodeKey, detail };
|
|
74
|
-
};
|
|
75
|
-
if (source === 'manual') {
|
|
76
|
-
return approve('manual', 'manual pain signal bypasses automatic gate');
|
|
77
|
-
}
|
|
78
|
-
if (source === 'subagent_error' && score >= painTrigger) {
|
|
79
|
-
return approve('subagent_error', `subagent error score ${score} >= ${painTrigger}`);
|
|
80
|
-
}
|
|
81
|
-
if (source === 'llm_paralysis' && score >= painTrigger) {
|
|
82
|
-
return approve('llm_paralysis', `llm paralysis score ${score} >= ${painTrigger}`);
|
|
83
|
-
}
|
|
84
|
-
if (source === 'gate_blocked' && score >= painTrigger) {
|
|
85
|
-
return approve('gate_blocked', `gate blocked score ${score} >= ${painTrigger}`);
|
|
86
|
-
}
|
|
87
|
-
if ((source === 'user_empathy' || source === 'semantic') && score >= semanticPain) {
|
|
88
|
-
return approve('semantic_pain', `semantic pain score ${score} >= ${semanticPain}`);
|
|
44
|
+
// Surface unknown sources via telemetry (core cannot log).
|
|
45
|
+
const { unknown } = normalizedSource(input.source);
|
|
46
|
+
if (unknown) {
|
|
47
|
+
SystemLogger.log('', 'GATE_UNKNOWN_SOURCE', `Unknown pain source: "${input.source}"`);
|
|
89
48
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return approve('high_gfi', `GFI ${currentGfi.toFixed(1)} >= ${highGfi}`);
|
|
49
|
+
const episodeKey = buildEpisodeKey(input);
|
|
50
|
+
const last = lastDiagnosedAtByEpisode.get(episodeKey);
|
|
51
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
52
|
+
const decision = evaluatePainDiagnosticGateDecision({ ...input, nowMs }, last);
|
|
53
|
+
// Record the diagnosis time when approved (matches prior markDiagnosed behavior).
|
|
54
|
+
if (decision.shouldDiagnose) {
|
|
55
|
+
lastDiagnosedAtByEpisode.set(episodeKey, nowMs);
|
|
98
56
|
}
|
|
99
|
-
return
|
|
100
|
-
shouldDiagnose: false,
|
|
101
|
-
reason: 'below_gate',
|
|
102
|
-
episodeKey,
|
|
103
|
-
detail: `score=${score}; gfi=${currentGfi.toFixed(1)}; consecutive=${consecutiveErrors}`,
|
|
104
|
-
};
|
|
57
|
+
return decision;
|
|
105
58
|
}
|
|
@@ -1,57 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Raw Observation Adapter — PRI-362
|
|
2
|
+
* Raw Observation Adapter — PRI-362 / PRI-446
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The source-kind resolution logic and RawObservation builders have been
|
|
5
|
+
* migrated to principles-core (runtime-v2/evidence-triage/observation-resolver.ts).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 1. isManualEntry → owner_reported
|
|
11
|
-
* 2. isGateBlock → rulehost_block
|
|
12
|
-
* 3. isSubagentError → subagent_error
|
|
13
|
-
* 4. isRateLimit → rate_limit (if true)
|
|
14
|
-
* 5. toolName === 'pain' / 'skill:pain' → agent_on_owner_request (with openclaw_context_bound) / owner_reported
|
|
15
|
-
* 6. failureSource → tool_failure / dispatch_error
|
|
16
|
-
* 7. isGfiTriggered → gfi_threshold
|
|
17
|
-
* 8. detectionSource → llm_paralysis / semantic / empathy_inferred / unknown
|
|
18
|
-
* 9. Fallback → unknown
|
|
7
|
+
* This file is now a thin re-export adapter. It preserves the original export
|
|
8
|
+
* names (resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation,
|
|
9
|
+
* RawObservation) so all existing import sites and the source-string
|
|
10
|
+
* characterization tests keep working without changes.
|
|
19
11
|
*
|
|
20
12
|
* ERR checklist:
|
|
21
|
-
* - ERR-
|
|
22
|
-
* - ERR-002: Every path returns a valid SourceKind (fallback to 'unknown').
|
|
23
|
-
* - EP-01: Runtime values validated before use.
|
|
13
|
+
* - ERR-011: re-export adapter, not a local re-definition of migrated logic.
|
|
24
14
|
*/
|
|
25
|
-
|
|
26
|
-
import type { RawObservation } from './raw-observation-types.js';
|
|
15
|
+
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, } from '@principles/core/runtime-v2';
|
|
27
16
|
export type { RawObservation } from './raw-observation-types.js';
|
|
28
|
-
/**
|
|
29
|
-
* Resolve SourceKind from a unified RawObservation.
|
|
30
|
-
*
|
|
31
|
-
* This function replaces the scattered resolveSourceKindFrom* functions
|
|
32
|
-
* and provides a single entry point for source-kind classification.
|
|
33
|
-
*
|
|
34
|
-
* Field precedence is explicitly defined in the function body to ensure
|
|
35
|
-
* deterministic behavior and make the logic easy to understand and test.
|
|
36
|
-
*/
|
|
37
|
-
export declare function resolveSourceKind(observation: RawObservation): SourceKind;
|
|
38
|
-
/**
|
|
39
|
-
* Build a RawObservation for a tool failure context.
|
|
40
|
-
*
|
|
41
|
-
* This replaces classifyToolFailureSource and the inline classification
|
|
42
|
-
* in after-tool-call-helpers. All tool error → dispatch/tool_failure
|
|
43
|
-
* classification is centralized here.
|
|
44
|
-
*/
|
|
45
|
-
export declare function buildToolFailureObservation(options: {
|
|
46
|
-
toolName: string | undefined;
|
|
47
|
-
error: unknown;
|
|
48
|
-
exitCode?: number;
|
|
49
|
-
provenance?: RawObservation['provenance'];
|
|
50
|
-
}): RawObservation;
|
|
51
|
-
/**
|
|
52
|
-
* Build a RawObservation for an LLM detection context.
|
|
53
|
-
*/
|
|
54
|
-
export declare function buildLlmDetectionObservation(options: {
|
|
55
|
-
detectionSource: string;
|
|
56
|
-
isGfiTriggered: boolean;
|
|
57
|
-
}): RawObservation;
|
|
@@ -1,170 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Raw Observation Adapter — PRI-362
|
|
2
|
+
* Raw Observation Adapter — PRI-362 / PRI-446
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The source-kind resolution logic and RawObservation builders have been
|
|
5
|
+
* migrated to principles-core (runtime-v2/evidence-triage/observation-resolver.ts).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 1. isManualEntry → owner_reported
|
|
11
|
-
* 2. isGateBlock → rulehost_block
|
|
12
|
-
* 3. isSubagentError → subagent_error
|
|
13
|
-
* 4. isRateLimit → rate_limit (if true)
|
|
14
|
-
* 5. toolName === 'pain' / 'skill:pain' → agent_on_owner_request (with openclaw_context_bound) / owner_reported
|
|
15
|
-
* 6. failureSource → tool_failure / dispatch_error
|
|
16
|
-
* 7. isGfiTriggered → gfi_threshold
|
|
17
|
-
* 8. detectionSource → llm_paralysis / semantic / empathy_inferred / unknown
|
|
18
|
-
* 9. Fallback → unknown
|
|
7
|
+
* This file is now a thin re-export adapter. It preserves the original export
|
|
8
|
+
* names (resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation,
|
|
9
|
+
* RawObservation) so all existing import sites and the source-string
|
|
10
|
+
* characterization tests keep working without changes.
|
|
19
11
|
*
|
|
20
12
|
* ERR checklist:
|
|
21
|
-
* - ERR-
|
|
22
|
-
* - ERR-002: Every path returns a valid SourceKind (fallback to 'unknown').
|
|
23
|
-
* - EP-01: Runtime values validated before use.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* Resolve SourceKind from a unified RawObservation.
|
|
27
|
-
*
|
|
28
|
-
* This function replaces the scattered resolveSourceKindFrom* functions
|
|
29
|
-
* and provides a single entry point for source-kind classification.
|
|
30
|
-
*
|
|
31
|
-
* Field precedence is explicitly defined in the function body to ensure
|
|
32
|
-
* deterministic behavior and make the logic easy to understand and test.
|
|
33
|
-
*/
|
|
34
|
-
export function resolveSourceKind(observation) {
|
|
35
|
-
const { isManualEntry, isGateBlock, isSubagentError, isRateLimit, toolName, failureSource, isGfiTriggered, detectionSource, nonZeroExit, timedOut, toolNotFound, } = observation;
|
|
36
|
-
// Priority 1: Manual entry (CLI, owner-reported)
|
|
37
|
-
if (isManualEntry) {
|
|
38
|
-
return 'owner_reported';
|
|
39
|
-
}
|
|
40
|
-
// Priority 2: Gate block
|
|
41
|
-
if (isGateBlock) {
|
|
42
|
-
return 'rulehost_block';
|
|
43
|
-
}
|
|
44
|
-
// Priority 3: Subagent error
|
|
45
|
-
if (isSubagentError) {
|
|
46
|
-
return 'subagent_error';
|
|
47
|
-
}
|
|
48
|
-
// Priority 4: Provider rate limit (explicit true/false)
|
|
49
|
-
if (isRateLimit === true) {
|
|
50
|
-
return 'rate_limit';
|
|
51
|
-
}
|
|
52
|
-
if (isRateLimit === false) {
|
|
53
|
-
return 'provider_failure';
|
|
54
|
-
}
|
|
55
|
-
// Priority 5: Manual pain tool
|
|
56
|
-
if (toolName === 'pain' || toolName === 'skill:pain') {
|
|
57
|
-
// Match resolveSourceKindFromToolFailure behavior:
|
|
58
|
-
// openclaw_context_bound → agent_on_owner_request
|
|
59
|
-
// other provenance or undefined → owner_reported
|
|
60
|
-
if (observation.provenance === 'openclaw_context_bound') {
|
|
61
|
-
return 'agent_on_owner_request';
|
|
62
|
-
}
|
|
63
|
-
return 'owner_reported';
|
|
64
|
-
}
|
|
65
|
-
// Priority 6: GFI threshold (must check before failure source for LLM detection path)
|
|
66
|
-
if (isGfiTriggered) {
|
|
67
|
-
return 'gfi_threshold';
|
|
68
|
-
}
|
|
69
|
-
// Priority 7: Tool failure / dispatch error
|
|
70
|
-
if (failureSource) {
|
|
71
|
-
// Match resolveSourceKindFromToolFailure behavior:
|
|
72
|
-
// dispatch_error → dispatch_error, anything else → tool_failure
|
|
73
|
-
if (failureSource === 'dispatch_error') {
|
|
74
|
-
return 'dispatch_error';
|
|
75
|
-
}
|
|
76
|
-
return 'tool_failure';
|
|
77
|
-
}
|
|
78
|
-
// Infer failureSource from tool failure indicators if not explicitly set
|
|
79
|
-
if (toolNotFound) {
|
|
80
|
-
return 'dispatch_error';
|
|
81
|
-
}
|
|
82
|
-
// Match classifyToolFailureSource behavior: unknown tool name → dispatch_error
|
|
83
|
-
// BUT only if this looks like a tool failure context (has other tool fields)
|
|
84
|
-
// Otherwise, this is likely a non-tool observation (e.g., LLM detection)
|
|
85
|
-
const hasToolContext = toolName !== undefined || nonZeroExit || timedOut || toolNotFound;
|
|
86
|
-
if (hasToolContext && (!toolName || toolName.trim() === '')) {
|
|
87
|
-
return 'dispatch_error';
|
|
88
|
-
}
|
|
89
|
-
// Exit code-based detection: non-zero exit or timeout → tool_failure
|
|
90
|
-
if (nonZeroExit || timedOut) {
|
|
91
|
-
return 'tool_failure';
|
|
92
|
-
}
|
|
93
|
-
// Priority 8: LLM detection source
|
|
94
|
-
if (detectionSource) {
|
|
95
|
-
// Match resolveSourceKindFromLlmDetection behavior:
|
|
96
|
-
if (detectionSource === 'llm_paralysis') {
|
|
97
|
-
return 'llm_paralysis';
|
|
98
|
-
}
|
|
99
|
-
if (detectionSource.startsWith('llm_')) {
|
|
100
|
-
return 'semantic';
|
|
101
|
-
}
|
|
102
|
-
if (detectionSource === 'user_empathy') {
|
|
103
|
-
return 'empathy_inferred';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Fallback: unknown
|
|
107
|
-
return 'unknown';
|
|
108
|
-
}
|
|
109
|
-
// ── Builder Functions ──────────────────────────────────────────────────────
|
|
110
|
-
//
|
|
111
|
-
// PRI-360 S1: These builders construct RawObservation from specific contexts,
|
|
112
|
-
// centralizing source classification rules in the adapter layer.
|
|
113
|
-
// Hooks should NOT hold source classification logic — use these builders.
|
|
114
|
-
/**
|
|
115
|
-
* Classify error message as dispatch_error vs tool_failure.
|
|
116
|
-
*
|
|
117
|
-
* This centralizes the regex-based classification that was previously
|
|
118
|
-
* scattered in classifyToolFailureSource and after-tool-call-helpers.
|
|
119
|
-
* Now hooks call this builder + resolveSourceKind instead of holding rules.
|
|
120
|
-
*/
|
|
121
|
-
function classifyErrorForDispatch(error) {
|
|
122
|
-
if (!error)
|
|
123
|
-
return 'tool_failure';
|
|
124
|
-
const msg = String(error);
|
|
125
|
-
if (/\btool\s+(?:\S+\s+)?not\s+found\b/i.test(msg) || /\bunknown\s+tool\b/i.test(msg)) {
|
|
126
|
-
return 'dispatch_error';
|
|
127
|
-
}
|
|
128
|
-
return 'tool_failure';
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Build a RawObservation for a tool failure context.
|
|
132
|
-
*
|
|
133
|
-
* This replaces classifyToolFailureSource and the inline classification
|
|
134
|
-
* in after-tool-call-helpers. All tool error → dispatch/tool_failure
|
|
135
|
-
* classification is centralized here.
|
|
136
|
-
*/
|
|
137
|
-
export function buildToolFailureObservation(options) {
|
|
138
|
-
const { toolName, error, provenance } = options;
|
|
139
|
-
const nonZeroExit = typeof options.exitCode === 'number' && options.exitCode !== 0;
|
|
140
|
-
// Classify dispatch vs tool_failure centrally
|
|
141
|
-
let failureSource;
|
|
142
|
-
if (!toolName || toolName.trim() === '') {
|
|
143
|
-
// Empty/whitespace tool name → dispatch error
|
|
144
|
-
failureSource = 'dispatch_error';
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
failureSource = classifyErrorForDispatch(error);
|
|
148
|
-
}
|
|
149
|
-
// If neither error nor non-zero exit, this is not a failure context
|
|
150
|
-
if (!error && !nonZeroExit) {
|
|
151
|
-
failureSource = undefined;
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
observedAt: new Date().toISOString(),
|
|
155
|
-
toolName,
|
|
156
|
-
failureSource,
|
|
157
|
-
nonZeroExit,
|
|
158
|
-
provenance,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Build a RawObservation for an LLM detection context.
|
|
13
|
+
* - ERR-011: re-export adapter, not a local re-definition of migrated logic.
|
|
163
14
|
*/
|
|
164
|
-
export
|
|
165
|
-
return {
|
|
166
|
-
observedAt: new Date().toISOString(),
|
|
167
|
-
detectionSource: options.detectionSource,
|
|
168
|
-
isGfiTriggered: options.isGfiTriggered,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
15
|
+
export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObservation, } from '@principles/core/runtime-v2';
|
|
@@ -1,62 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Raw Observation Types — PRI-362
|
|
2
|
+
* Raw Observation Types — PRI-362 / PRI-446
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* observation
|
|
6
|
-
*
|
|
7
|
-
* This replaces scattered resolveSourceKindFrom* functions with a single
|
|
8
|
-
* field-driven adapter.
|
|
4
|
+
* The RawObservation type has been migrated to principles-core
|
|
5
|
+
* (runtime-v2/evidence-triage/observation-resolver.ts). This file re-exports it
|
|
6
|
+
* so existing plugin import sites keep working unchanged.
|
|
9
7
|
*
|
|
10
8
|
* ERR checklist:
|
|
11
|
-
* - ERR-
|
|
12
|
-
* - ERR-002: Every decision carries reason + nextAction.
|
|
13
|
-
* - EP-01: Source adapter validates before use.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Raw observation from a source adapter.
|
|
17
|
-
*
|
|
18
|
-
* This is the input to resolveSourceKind. It contains all possible
|
|
19
|
-
* context fields that different sources may provide. The adapter
|
|
20
|
-
* reads only the fields it needs based on the observation source.
|
|
9
|
+
* - ERR-011: this is a re-export adapter, not a local re-definition.
|
|
21
10
|
*/
|
|
22
|
-
export
|
|
23
|
-
/** When the observation was made (ISO timestamp) */
|
|
24
|
-
readonly observedAt: string;
|
|
25
|
-
/** Workspace identifier */
|
|
26
|
-
readonly workspaceId?: string;
|
|
27
|
-
/** Session identifier */
|
|
28
|
-
readonly sessionId?: string;
|
|
29
|
-
/** Trace identifier for correlation */
|
|
30
|
-
readonly traceId?: string;
|
|
31
|
-
/** Tool name (for after_tool_call hook) */
|
|
32
|
-
readonly toolName?: string;
|
|
33
|
-
/** Failure source classification */
|
|
34
|
-
readonly failureSource?: 'tool_failure' | 'dispatch_error';
|
|
35
|
-
/** Whether the tool call exited with non-zero code */
|
|
36
|
-
readonly nonZeroExit?: boolean;
|
|
37
|
-
/** Whether the tool call timed out */
|
|
38
|
-
readonly timedOut?: boolean;
|
|
39
|
-
/** Whether the tool does not exist */
|
|
40
|
-
readonly toolNotFound?: boolean;
|
|
41
|
-
/** Detection source identifier */
|
|
42
|
-
readonly detectionSource?: string;
|
|
43
|
-
/** Whether GFI threshold was crossed */
|
|
44
|
-
readonly isGfiTriggered?: boolean;
|
|
45
|
-
/** Whether the failure was a rate limit (429) */
|
|
46
|
-
readonly isRateLimit?: boolean;
|
|
47
|
-
/** Whether this observation came from a gate block */
|
|
48
|
-
readonly isGateBlock?: boolean;
|
|
49
|
-
/** Whether this was a manual CLI entry */
|
|
50
|
-
readonly isManualEntry?: boolean;
|
|
51
|
-
/** Provenance: how trustworthy and context-bound is the observation */
|
|
52
|
-
readonly provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook';
|
|
53
|
-
/** Whether this observation came from a subagent error */
|
|
54
|
-
readonly isSubagentError?: boolean;
|
|
55
|
-
/**
|
|
56
|
-
* Raw payload from the source.
|
|
57
|
-
*
|
|
58
|
-
* This is always `unknown` (ERR-005). Source adapters validate only
|
|
59
|
-
* enough to identify the source and capture bounded context.
|
|
60
|
-
*/
|
|
61
|
-
readonly payload?: unknown;
|
|
62
|
-
}
|
|
11
|
+
export type { RawObservation } from '@principles/core/runtime-v2';
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Raw Observation Types — PRI-362
|
|
2
|
+
* Raw Observation Types — PRI-362 / PRI-446
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* observation
|
|
6
|
-
*
|
|
7
|
-
* This replaces scattered resolveSourceKindFrom* functions with a single
|
|
8
|
-
* field-driven adapter.
|
|
4
|
+
* The RawObservation type has been migrated to principles-core
|
|
5
|
+
* (runtime-v2/evidence-triage/observation-resolver.ts). This file re-exports it
|
|
6
|
+
* so existing plugin import sites keep working unchanged.
|
|
9
7
|
*
|
|
10
8
|
* ERR checklist:
|
|
11
|
-
* - ERR-
|
|
12
|
-
* - ERR-002: Every decision carries reason + nextAction.
|
|
13
|
-
* - EP-01: Source adapter validates before use.
|
|
9
|
+
* - ERR-011: this is a re-export adapter, not a local re-definition.
|
|
14
10
|
*/
|
|
15
11
|
export {};
|
|
@@ -42,38 +42,18 @@ export { resolveSourceKind, buildToolFailureObservation, buildLlmDetectionObserv
|
|
|
42
42
|
* - Falling back to existing behavior when the flag is off
|
|
43
43
|
*/
|
|
44
44
|
export function evaluateEvidenceTriage(sourceKind, score, options) {
|
|
45
|
+
// PRI-446: the risky high-score and repeated-failure upgrade rules now live
|
|
46
|
+
// in core triage-policy.ts (single source of truth). The adapter passes the
|
|
47
|
+
// context flags straight through; core decides whether to upgrade.
|
|
45
48
|
const input = {
|
|
46
49
|
sourceKind,
|
|
47
50
|
score,
|
|
48
51
|
isUnsafeHighConfidence: options?.isUnsafeHighConfidence,
|
|
49
52
|
provenance: options?.provenance,
|
|
53
|
+
isRisky: options?.isRisky,
|
|
54
|
+
consecutiveErrors: options?.consecutiveErrors,
|
|
50
55
|
};
|
|
51
|
-
|
|
52
|
-
// PEAT-B1 upgrade logic: risky high-score overrides evidence_only
|
|
53
|
-
// Matches PainDiagnosticGate.risky_high_score: isRisky && score >= 70 → admit
|
|
54
|
-
if (result.decision === 'evidence_only' &&
|
|
55
|
-
options?.isRisky === true &&
|
|
56
|
-
score >= 70) {
|
|
57
|
-
result = {
|
|
58
|
-
...result,
|
|
59
|
-
decision: 'admit',
|
|
60
|
-
reason: 'Risky high-score operation overrides evidence-only decision. Immediate diagnosis required.',
|
|
61
|
-
nextAction: 'create_diagnostic_task',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
// PEAT-B1 upgrade logic: repeated failures override evidence_only
|
|
65
|
-
// Threshold: 4 consecutive failures (matches PainDiagnosticGate.repeatedFailure)
|
|
66
|
-
if (result.decision === 'evidence_only' &&
|
|
67
|
-
options?.consecutiveErrors !== undefined &&
|
|
68
|
-
options.consecutiveErrors >= 4) {
|
|
69
|
-
result = {
|
|
70
|
-
...result,
|
|
71
|
-
decision: 'admit',
|
|
72
|
-
reason: 'Repeated failures override evidence-only decision. Pattern suggests systemic issue requiring diagnosis.',
|
|
73
|
-
nextAction: 'create_diagnostic_task',
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
return result;
|
|
56
|
+
return evaluateTriage(input);
|
|
77
57
|
}
|
|
78
58
|
// ── High-Confidence Unsafe Action Detection ──────────────────────────────────
|
|
79
59
|
/**
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.141.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|