principles-disciple 1.103.0 → 1.104.1
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 +79 -89
- package/src/hooks/after-tool-call-types.ts +2 -8
- package/src/hooks/raw-observation-adapter.ts +231 -0
- package/src/hooks/raw-observation-types.ts +77 -0
- package/src/hooks/triage-adapter.ts +59 -52
- package/src/hooks/trigger-cooldown-tracker.ts +82 -0
- package/tests/core/surface-guard.test.ts +5 -5
- package/tests/hooks/pain.test.ts +20 -14
- package/tests/hooks/raw-observation-adapter.test.ts +312 -0
- package/tests/hooks/single-gate-pain-admission.test.ts +258 -0
- package/tests/integration/auto-entry-gate.test.ts +13 -5
- package/tests/integration/mvp-surface-registry-guard.test.ts +2 -2
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.104.1",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -23,9 +23,8 @@ import { WorkspaceContext } from '../core/workspace-context.js';
|
|
|
23
23
|
import { getEvolutionLogger, createTraceId } from '../core/evolution-logger.js';
|
|
24
24
|
import { recordEvolutionSuccess, recordEvolutionFailure } from '../core/evolution-engine.js';
|
|
25
25
|
import type { PluginHookAfterToolCallEvent } from '../openclaw-sdk.js';
|
|
26
|
-
import {
|
|
26
|
+
import { isCooldownActive as isTriggerCooldownActive, markEpisodeAsDiagnosed, clearCooldownState } from './trigger-cooldown-tracker.js';
|
|
27
27
|
import { sanitizeForEvidence, sanitizeToolParamsForEvidence } from './message-sanitize.js';
|
|
28
|
-
import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
29
28
|
import { resolveSourceKindFromToolFailure, evaluateEvidenceTriage } from './triage-adapter.js';
|
|
30
29
|
import { evaluateTriggerController } from '@principles/core/runtime-v2';
|
|
31
30
|
import { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
@@ -40,8 +39,13 @@ import type { ToolCallOutcome, ToolCallObservation, PainAdmissionDecision } from
|
|
|
40
39
|
* Extracts exitCode logic, determines failure/success, classifies failure source.
|
|
41
40
|
*/
|
|
42
41
|
export function classifyToolCallOutcome(event: PluginHookAfterToolCallEvent): ToolCallOutcome {
|
|
43
|
-
|
|
44
|
-
const
|
|
42
|
+
// EP-01: Validate event.result at runtime instead of `as` cast
|
|
43
|
+
const resultObj = (event.result && typeof event.result === 'object' && !Array.isArray(event.result))
|
|
44
|
+
? event.result as Record<string, unknown> // safe: guarded by typeof + Array.isArray checks above
|
|
45
|
+
: null;
|
|
46
|
+
const details = (resultObj && resultObj.details && typeof resultObj.details === 'object' && !Array.isArray(resultObj.details))
|
|
47
|
+
? resultObj.details as Record<string, unknown> // safe: guarded by typeof + Array.isArray checks above
|
|
48
|
+
: null;
|
|
45
49
|
const topExitCode = resultObj?.exitCode;
|
|
46
50
|
const detailExitCode = details?.exitCode;
|
|
47
51
|
|
|
@@ -315,13 +319,26 @@ export function handleProbationFeedback(
|
|
|
315
319
|
|
|
316
320
|
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file', 'replace'];
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Cooldown map for trigger controller decisions.
|
|
324
|
+
*
|
|
325
|
+
* PRI-363: This replaces the hidden map in PainDiagnosticGate.
|
|
326
|
+
* Core trigger-controller is stateless; plugin layer owns cooldown state.
|
|
327
|
+
*
|
|
328
|
+
* EP-05: Loop state freshness — each check reads fresh state from this map.
|
|
329
|
+
*/
|
|
330
|
+
const TRIGGER_COOLDOWN_MAP = new Map<string, number>();
|
|
331
|
+
|
|
318
332
|
/**
|
|
319
333
|
* Evaluate whether a tool failure should trigger pain diagnosis.
|
|
320
334
|
*
|
|
335
|
+
* PRI-363: Single-gate architecture — only TriggerController decides.
|
|
336
|
+
*
|
|
321
337
|
* Combines:
|
|
322
338
|
* 1. Write-tool check — only write tools on failures enter this path
|
|
323
|
-
* 2. PEAT-B1 triage —
|
|
324
|
-
* 3.
|
|
339
|
+
* 2. PEAT-B1 triage — evidence triage (always enabled now)
|
|
340
|
+
* 3. PEAT-B2 trigger controller — single source of truth for task creation
|
|
341
|
+
* 4. Cooldown tracking — plugin layer owns this state
|
|
325
342
|
*
|
|
326
343
|
* Returns a structured decision with reason and stage.
|
|
327
344
|
*/
|
|
@@ -333,7 +350,7 @@ export function evaluatePainAdmissionForToolCall(
|
|
|
333
350
|
sessionState: SessionState | undefined,
|
|
334
351
|
sessionId: string,
|
|
335
352
|
workspaceDir: string,
|
|
336
|
-
|
|
353
|
+
_config: { get: (key: string) => unknown },
|
|
337
354
|
): PainAdmissionDecision {
|
|
338
355
|
// Only write-tool failures enter the pain path
|
|
339
356
|
if (!WRITE_TOOLS.includes(event.toolName) || !outcome.isFailure) {
|
|
@@ -347,98 +364,64 @@ export function evaluatePainAdmissionForToolCall(
|
|
|
347
364
|
|
|
348
365
|
const failureSource = outcome.failureSource ?? 'tool_failure';
|
|
349
366
|
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
365
|
-
const triggerDecision = evaluateTriggerController({
|
|
366
|
-
triageResult: triage,
|
|
367
|
-
isOwnerManual: false, // tool failures are never owner manual
|
|
368
|
-
isCooldownActive: cooldownActive,
|
|
369
|
-
isValid: true,
|
|
370
|
-
score: observation.painScore,
|
|
371
|
-
sessionId,
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
if (!triggerDecision.shouldCreateDiagnosticTask) {
|
|
375
|
-
SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
|
|
376
|
-
outcome: triggerDecision.outcome,
|
|
377
|
-
sourceKind: triggerDecision.sourceKind,
|
|
378
|
-
reason: triggerDecision.reason,
|
|
379
|
-
nextAction: triggerDecision.nextAction,
|
|
380
|
-
triageDecision: triggerDecision.triageDecision,
|
|
381
|
-
tool: event.toolName,
|
|
382
|
-
path: observation.relPath,
|
|
383
|
-
}));
|
|
384
|
-
return {
|
|
385
|
-
admitted: false,
|
|
386
|
-
stage: 'triage_evidence_only',
|
|
387
|
-
reason: triggerDecision.reason,
|
|
388
|
-
detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
367
|
+
// Check cooldown before calling trigger controller
|
|
368
|
+
const cooldownActive = isTriggerCooldownActive(
|
|
369
|
+
failureSource,
|
|
370
|
+
sessionId,
|
|
371
|
+
latestFailureState?.lastErrorHash,
|
|
372
|
+
TRIGGER_COOLDOWN_MAP,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// PEAT-B1: Evidence triage (with consecutiveErrors and isRisky for upgrade logic)
|
|
376
|
+
const sourceKind = resolveSourceKindFromToolFailure(event.toolName, failureSource);
|
|
377
|
+
const triage = evaluateEvidenceTriage(sourceKind, observation.painScore, {
|
|
378
|
+
consecutiveErrors: (latestFailureState ?? sessionState)?.consecutiveErrors,
|
|
379
|
+
isRisky: observation.isRisk,
|
|
380
|
+
});
|
|
392
381
|
|
|
393
|
-
//
|
|
394
|
-
const
|
|
395
|
-
|
|
382
|
+
// PEAT-B2: Trigger controller — single source of truth for task creation
|
|
383
|
+
const triggerDecision = evaluateTriggerController({
|
|
384
|
+
triageResult: triage,
|
|
385
|
+
isOwnerManual: false, // tool failures are never owner manual
|
|
386
|
+
isCooldownActive: cooldownActive,
|
|
387
|
+
isValid: true,
|
|
396
388
|
score: observation.painScore,
|
|
397
|
-
currentGfi: (latestFailureState ?? sessionState)?.currentGfi ?? 0,
|
|
398
|
-
consecutiveErrors: (latestFailureState ?? sessionState)?.consecutiveErrors ?? 0,
|
|
399
|
-
isRisky: observation.isRisk,
|
|
400
|
-
errorHash: latestFailureState?.lastErrorHash,
|
|
401
389
|
sessionId,
|
|
402
|
-
thresholds: {
|
|
403
|
-
painTrigger: (config.get('thresholds.pain_trigger') as number) || 40,
|
|
404
|
-
highSeverity: (config.get('severity_thresholds.high') as number) || 70,
|
|
405
|
-
repeatedFailure: (config.get('thresholds.stuck_loops_trigger') as number) || 4,
|
|
406
|
-
},
|
|
407
390
|
});
|
|
408
391
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
392
|
+
// Log the decision
|
|
393
|
+
SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
|
|
394
|
+
outcome: triggerDecision.outcome,
|
|
395
|
+
sourceKind: triggerDecision.sourceKind,
|
|
396
|
+
reason: triggerDecision.reason,
|
|
397
|
+
nextAction: triggerDecision.nextAction,
|
|
398
|
+
triageDecision: triggerDecision.triageDecision,
|
|
399
|
+
tool: event.toolName,
|
|
400
|
+
path: observation.relPath,
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
// If trigger controller says yes, mark cooldown and admit
|
|
404
|
+
if (triggerDecision.shouldCreateDiagnosticTask) {
|
|
405
|
+
markEpisodeAsDiagnosed(
|
|
406
|
+
failureSource,
|
|
407
|
+
sessionId,
|
|
408
|
+
latestFailureState?.lastErrorHash,
|
|
409
|
+
TRIGGER_COOLDOWN_MAP,
|
|
410
|
+
);
|
|
427
411
|
return {
|
|
428
|
-
admitted:
|
|
429
|
-
stage: '
|
|
430
|
-
reason:
|
|
431
|
-
detail:
|
|
432
|
-
gateResult: { shouldDiagnose: false, reason: diagnosticGate.reason, detail: diagnosticGate.detail },
|
|
412
|
+
admitted: true,
|
|
413
|
+
stage: 'trigger_admitted',
|
|
414
|
+
reason: triggerDecision.reason,
|
|
415
|
+
detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
|
|
433
416
|
};
|
|
434
417
|
}
|
|
435
418
|
|
|
419
|
+
// Otherwise, reject with trigger controller's reason
|
|
436
420
|
return {
|
|
437
|
-
admitted:
|
|
438
|
-
stage: '
|
|
439
|
-
reason:
|
|
440
|
-
detail:
|
|
441
|
-
gateResult: { shouldDiagnose: true, reason: diagnosticGate.reason, detail: diagnosticGate.detail },
|
|
421
|
+
admitted: false,
|
|
422
|
+
stage: 'trigger_rejected',
|
|
423
|
+
reason: triggerDecision.reason,
|
|
424
|
+
detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
|
|
442
425
|
};
|
|
443
426
|
}
|
|
444
427
|
|
|
@@ -562,6 +545,13 @@ export function emitPainIfAdmitted(
|
|
|
562
545
|
|
|
563
546
|
export { buildTrajectoryEvidence } from './trajectory-evidence.js';
|
|
564
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Reset trigger cooldown state (for tests).
|
|
550
|
+
*/
|
|
551
|
+
export function resetTriggerCooldownForTest(): void {
|
|
552
|
+
clearCooldownState(TRIGGER_COOLDOWN_MAP);
|
|
553
|
+
}
|
|
554
|
+
|
|
565
555
|
// ── Source Classification ────────────────────────────────────────────────────
|
|
566
556
|
|
|
567
557
|
/**
|
|
@@ -69,23 +69,17 @@ export interface ToolCallObservation {
|
|
|
69
69
|
/**
|
|
70
70
|
* Result of evaluating whether a tool failure should trigger pain diagnosis.
|
|
71
71
|
*
|
|
72
|
-
* Encapsulates the
|
|
72
|
+
* Encapsulates the single-gate trigger controller decision.
|
|
73
73
|
*/
|
|
74
74
|
export interface PainAdmissionDecision {
|
|
75
75
|
/** Whether the tool failure should proceed to pain emission */
|
|
76
76
|
readonly admitted: boolean;
|
|
77
77
|
/** The admission stage that made the decision */
|
|
78
|
-
readonly stage: '
|
|
78
|
+
readonly stage: 'not_applicable' | 'trigger_admitted' | 'trigger_rejected';
|
|
79
79
|
/** Human-readable reason for the decision */
|
|
80
80
|
readonly reason: string;
|
|
81
81
|
/** Detail about the decision */
|
|
82
82
|
readonly detail: string;
|
|
83
|
-
/** The diagnostic gate result (if gate was evaluated) */
|
|
84
|
-
readonly gateResult?: {
|
|
85
|
-
readonly shouldDiagnose: boolean;
|
|
86
|
-
readonly reason: string;
|
|
87
|
-
readonly detail: string;
|
|
88
|
-
};
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
// ── Friction Update Result ──────────────────────────────────────────────────
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw Observation Adapter — PRI-362
|
|
3
|
+
*
|
|
4
|
+
* Unified source-kind resolution from RawObservation.
|
|
5
|
+
*
|
|
6
|
+
* Replaces scattered resolveSourceKindFrom* functions with a single
|
|
7
|
+
* field-driven adapter that maps observation fields to SourceKind.
|
|
8
|
+
*
|
|
9
|
+
* Field precedence (highest to lowest):
|
|
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
|
|
19
|
+
*
|
|
20
|
+
* ERR checklist:
|
|
21
|
+
* - ERR-001: Source kind resolved from runtime values, no `as` casts.
|
|
22
|
+
* - ERR-002: Every path returns a valid SourceKind (fallback to 'unknown').
|
|
23
|
+
* - EP-01: Runtime values validated before use.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { SourceKind } from '@principles/core/runtime-v2';
|
|
27
|
+
import type { RawObservation } from './raw-observation-types.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve SourceKind from a unified RawObservation.
|
|
31
|
+
*
|
|
32
|
+
* This function replaces the scattered resolveSourceKindFrom* functions
|
|
33
|
+
* and provides a single entry point for source-kind classification.
|
|
34
|
+
*
|
|
35
|
+
* Field precedence is explicitly defined in the function body to ensure
|
|
36
|
+
* deterministic behavior and make the logic easy to understand and test.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveSourceKind(observation: RawObservation): SourceKind {
|
|
39
|
+
const {
|
|
40
|
+
isManualEntry,
|
|
41
|
+
isGateBlock,
|
|
42
|
+
isSubagentError,
|
|
43
|
+
isRateLimit,
|
|
44
|
+
toolName,
|
|
45
|
+
failureSource,
|
|
46
|
+
isGfiTriggered,
|
|
47
|
+
detectionSource,
|
|
48
|
+
nonZeroExit,
|
|
49
|
+
timedOut,
|
|
50
|
+
toolNotFound,
|
|
51
|
+
} = observation;
|
|
52
|
+
|
|
53
|
+
// Priority 1: Manual entry (CLI, owner-reported)
|
|
54
|
+
if (isManualEntry) {
|
|
55
|
+
return 'owner_reported';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Priority 2: Gate block
|
|
59
|
+
if (isGateBlock) {
|
|
60
|
+
return 'rulehost_block';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Priority 3: Subagent error
|
|
64
|
+
if (isSubagentError) {
|
|
65
|
+
return 'subagent_error';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Priority 4: Provider rate limit (explicit true/false)
|
|
69
|
+
if (isRateLimit === true) {
|
|
70
|
+
return 'rate_limit';
|
|
71
|
+
}
|
|
72
|
+
if (isRateLimit === false) {
|
|
73
|
+
return 'provider_failure';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Priority 5: Manual pain tool
|
|
77
|
+
if (toolName === 'pain' || toolName === 'skill:pain') {
|
|
78
|
+
// Match resolveSourceKindFromToolFailure behavior:
|
|
79
|
+
// openclaw_context_bound → agent_on_owner_request
|
|
80
|
+
// other provenance or undefined → owner_reported
|
|
81
|
+
if (observation.provenance === 'openclaw_context_bound') {
|
|
82
|
+
return 'agent_on_owner_request';
|
|
83
|
+
}
|
|
84
|
+
return 'owner_reported';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Priority 6: GFI threshold (must check before failure source for LLM detection path)
|
|
88
|
+
if (isGfiTriggered) {
|
|
89
|
+
return 'gfi_threshold';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Priority 7: Tool failure / dispatch error
|
|
93
|
+
if (failureSource) {
|
|
94
|
+
// Match resolveSourceKindFromToolFailure behavior:
|
|
95
|
+
// dispatch_error → dispatch_error, anything else → tool_failure
|
|
96
|
+
if (failureSource === 'dispatch_error') {
|
|
97
|
+
return 'dispatch_error';
|
|
98
|
+
}
|
|
99
|
+
return 'tool_failure';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Infer failureSource from tool failure indicators if not explicitly set
|
|
103
|
+
if (toolNotFound) {
|
|
104
|
+
return 'dispatch_error';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Match classifyToolFailureSource behavior: unknown tool name → dispatch_error
|
|
108
|
+
// BUT only if this looks like a tool failure context (has other tool fields)
|
|
109
|
+
// Otherwise, this is likely a non-tool observation (e.g., LLM detection)
|
|
110
|
+
const hasToolContext = toolName !== undefined || nonZeroExit || timedOut || toolNotFound;
|
|
111
|
+
if (hasToolContext && (!toolName || toolName.trim() === '')) {
|
|
112
|
+
return 'dispatch_error';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Exit code-based detection: non-zero exit or timeout → tool_failure
|
|
116
|
+
if (nonZeroExit || timedOut) {
|
|
117
|
+
return 'tool_failure';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Priority 8: LLM detection source
|
|
121
|
+
if (detectionSource) {
|
|
122
|
+
// Match resolveSourceKindFromLlmDetection behavior:
|
|
123
|
+
if (detectionSource === 'llm_paralysis') {
|
|
124
|
+
return 'llm_paralysis';
|
|
125
|
+
}
|
|
126
|
+
if (detectionSource.startsWith('llm_')) {
|
|
127
|
+
return 'semantic';
|
|
128
|
+
}
|
|
129
|
+
if (detectionSource === 'user_empathy') {
|
|
130
|
+
return 'empathy_inferred';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback: unknown
|
|
135
|
+
return 'unknown';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve SourceKind from tool failure context (legacy wrapper).
|
|
140
|
+
*
|
|
141
|
+
* This is a thin wrapper around resolveSourceKind for compatibility.
|
|
142
|
+
* It constructs a RawObservation from the old function signature.
|
|
143
|
+
*
|
|
144
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveSourceKindFromToolFailure(
|
|
147
|
+
toolName: string | undefined,
|
|
148
|
+
failureSource: 'tool_failure' | 'dispatch_error',
|
|
149
|
+
provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook',
|
|
150
|
+
): SourceKind {
|
|
151
|
+
const observation: RawObservation = {
|
|
152
|
+
observedAt: new Date().toISOString(),
|
|
153
|
+
toolName,
|
|
154
|
+
failureSource,
|
|
155
|
+
provenance,
|
|
156
|
+
};
|
|
157
|
+
return resolveSourceKind(observation);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve SourceKind from LLM detection context (legacy wrapper).
|
|
162
|
+
*
|
|
163
|
+
* This is a thin wrapper around resolveSourceKind for compatibility.
|
|
164
|
+
*
|
|
165
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveSourceKindFromLlmDetection(
|
|
168
|
+
detectionSource: string,
|
|
169
|
+
isGfiTriggered: boolean,
|
|
170
|
+
): SourceKind {
|
|
171
|
+
const observation: RawObservation = {
|
|
172
|
+
observedAt: new Date().toISOString(),
|
|
173
|
+
detectionSource,
|
|
174
|
+
isGfiTriggered,
|
|
175
|
+
};
|
|
176
|
+
return resolveSourceKind(observation);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve SourceKind from gate block context (legacy wrapper).
|
|
181
|
+
*
|
|
182
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
183
|
+
*/
|
|
184
|
+
export function resolveSourceKindFromGateBlock(): SourceKind {
|
|
185
|
+
const observation: RawObservation = {
|
|
186
|
+
observedAt: new Date().toISOString(),
|
|
187
|
+
isGateBlock: true,
|
|
188
|
+
};
|
|
189
|
+
return resolveSourceKind(observation);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve SourceKind from manual command context (legacy wrapper).
|
|
194
|
+
*
|
|
195
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
196
|
+
*/
|
|
197
|
+
export function resolveSourceKindFromCommand(): SourceKind {
|
|
198
|
+
const observation: RawObservation = {
|
|
199
|
+
observedAt: new Date().toISOString(),
|
|
200
|
+
isManualEntry: true,
|
|
201
|
+
};
|
|
202
|
+
return resolveSourceKind(observation);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve SourceKind from provider context (legacy wrapper).
|
|
207
|
+
*
|
|
208
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveSourceKindFromProvider(
|
|
211
|
+
isRateLimit: boolean,
|
|
212
|
+
): SourceKind {
|
|
213
|
+
const observation: RawObservation = {
|
|
214
|
+
observedAt: new Date().toISOString(),
|
|
215
|
+
isRateLimit,
|
|
216
|
+
};
|
|
217
|
+
return resolveSourceKind(observation);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Resolve SourceKind from subagent context (legacy wrapper).
|
|
222
|
+
*
|
|
223
|
+
* @deprecated Use resolveSourceKind directly with RawObservation.
|
|
224
|
+
*/
|
|
225
|
+
export function resolveSourceKindFromSubagent(): SourceKind {
|
|
226
|
+
const observation: RawObservation = {
|
|
227
|
+
observedAt: new Date().toISOString(),
|
|
228
|
+
isSubagentError: true,
|
|
229
|
+
};
|
|
230
|
+
return resolveSourceKind(observation);
|
|
231
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw Observation Types — PRI-362
|
|
3
|
+
*
|
|
4
|
+
* Source adapter layer that normalizes diverse hook contexts into a unified
|
|
5
|
+
* observation model before mapping to SourceKind.
|
|
6
|
+
*
|
|
7
|
+
* This replaces scattered resolveSourceKindFrom* functions with a single
|
|
8
|
+
* field-driven adapter.
|
|
9
|
+
*
|
|
10
|
+
* ERR checklist:
|
|
11
|
+
* - ERR-001: No `as` casts; validate unknown payload field-by-field.
|
|
12
|
+
* - ERR-002: Every decision carries reason + nextAction.
|
|
13
|
+
* - EP-01: Source adapter validates before use.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Raw observation from a source adapter.
|
|
18
|
+
*
|
|
19
|
+
* This is the input to resolveSourceKind. It contains all possible
|
|
20
|
+
* context fields that different sources may provide. The adapter
|
|
21
|
+
* reads only the fields it needs based on the observation source.
|
|
22
|
+
*/
|
|
23
|
+
export interface RawObservation {
|
|
24
|
+
/** When the observation was made (ISO timestamp) */
|
|
25
|
+
readonly observedAt: string;
|
|
26
|
+
/** Workspace identifier */
|
|
27
|
+
readonly workspaceId?: string;
|
|
28
|
+
/** Session identifier */
|
|
29
|
+
readonly sessionId?: string;
|
|
30
|
+
/** Trace identifier for correlation */
|
|
31
|
+
readonly traceId?: string;
|
|
32
|
+
|
|
33
|
+
// ── Tool Failure Fields ────────────────────────────────────────────────
|
|
34
|
+
/** Tool name (for after_tool_call hook) */
|
|
35
|
+
readonly toolName?: string;
|
|
36
|
+
/** Failure source classification */
|
|
37
|
+
readonly failureSource?: 'tool_failure' | 'dispatch_error';
|
|
38
|
+
/** Whether the tool call exited with non-zero code */
|
|
39
|
+
readonly nonZeroExit?: boolean;
|
|
40
|
+
/** Whether the tool call timed out */
|
|
41
|
+
readonly timedOut?: boolean;
|
|
42
|
+
/** Whether the tool does not exist */
|
|
43
|
+
readonly toolNotFound?: boolean;
|
|
44
|
+
|
|
45
|
+
// ── LLM Detection Fields ───────────────────────────────────────────────
|
|
46
|
+
/** Detection source identifier */
|
|
47
|
+
readonly detectionSource?: string;
|
|
48
|
+
/** Whether GFI threshold was crossed */
|
|
49
|
+
readonly isGfiTriggered?: boolean;
|
|
50
|
+
|
|
51
|
+
// ── Provider Fields ───────────────────────────────────────────────────
|
|
52
|
+
/** Whether the failure was a rate limit (429) */
|
|
53
|
+
readonly isRateLimit?: boolean;
|
|
54
|
+
|
|
55
|
+
// ── Gate Block Fields ────────────────────────────────────────────────
|
|
56
|
+
/** Whether this observation came from a gate block */
|
|
57
|
+
readonly isGateBlock?: boolean;
|
|
58
|
+
|
|
59
|
+
// ── Manual Entry Fields ───────────────────────────────────────────────
|
|
60
|
+
/** Whether this was a manual CLI entry */
|
|
61
|
+
readonly isManualEntry?: boolean;
|
|
62
|
+
/** Provenance: how trustworthy and context-bound is the observation */
|
|
63
|
+
readonly provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook';
|
|
64
|
+
|
|
65
|
+
// ── Subagent Fields ───────────────────────────────────────────────────
|
|
66
|
+
/** Whether this observation came from a subagent error */
|
|
67
|
+
readonly isSubagentError?: boolean;
|
|
68
|
+
|
|
69
|
+
// ── Raw Payload ──────────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Raw payload from the source.
|
|
72
|
+
*
|
|
73
|
+
* This is always `unknown` (ERR-005). Source adapters validate only
|
|
74
|
+
* enough to identify the source and capture bounded context.
|
|
75
|
+
*/
|
|
76
|
+
readonly payload?: unknown;
|
|
77
|
+
}
|