@unrdf/hooks 26.4.4 → 26.4.7
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/README.md +41 -37
- package/examples/hook-chains/package.json +2 -2
- package/examples/policy-hooks/README.md +5 -9
- package/examples/policy-hooks/package.json +2 -2
- package/package.json +2 -1
- package/src/hooks/builtin-hooks.mjs +4 -2
- package/src/hooks/condition-evaluator.mjs +2 -5
- package/src/hooks/effect-executor.mjs +9 -21
- package/src/hooks/hook-chain-compiler.mjs +0 -1
- package/src/hooks/hook-executor.mjs +5 -1
- package/src/hooks/self-play-autonomics.mjs +239 -198
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
 
|
|
4
4
|
|
|
5
5
|
> Production-grade policy definition and execution framework for RDF knowledge graphs
|
|
6
|
-
>
|
|
6
|
+
>
|
|
7
7
|
> Implements 9 condition kinds, deterministic receipt chaining, SPARQL transformations, SHACL enforcement, N3 forward-chaining inference, and Datalog logic programming
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
@@ -36,7 +36,7 @@ const store = createStore();
|
|
|
36
36
|
const engine = new KnowledgeHookEngine(store);
|
|
37
37
|
const context = createContext({
|
|
38
38
|
nodeId: 'my-app',
|
|
39
|
-
t_ns: BigInt(Date.now() * 1000000)
|
|
39
|
+
t_ns: BigInt(Date.now() * 1000000),
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
// Define a hook with SHACL condition (soft-fail annotation)
|
|
@@ -45,11 +45,12 @@ const hook = createKnowledgeHook({
|
|
|
45
45
|
condition: {
|
|
46
46
|
kind: 'shacl',
|
|
47
47
|
ref: { uri: 'file:///shapes/compliance.ttl' },
|
|
48
|
-
enforcementMode: 'annotate'
|
|
48
|
+
enforcementMode: 'annotate', // Log violations but don't block
|
|
49
49
|
},
|
|
50
|
-
effects: [
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
effects: [
|
|
51
|
+
{
|
|
52
|
+
kind: 'sparql-construct',
|
|
53
|
+
query: `
|
|
53
54
|
CONSTRUCT {
|
|
54
55
|
?s ex:status ex:Valid ;
|
|
55
56
|
ex:validatedAt ?now .
|
|
@@ -58,17 +59,18 @@ const hook = createKnowledgeHook({
|
|
|
58
59
|
?s ?p ?o .
|
|
59
60
|
BIND (NOW() as ?now)
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
`,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
// Execute with receipt chaining
|
|
66
68
|
const result = await engine.execute(context, [hook]);
|
|
67
69
|
|
|
68
70
|
// Access deterministic receipt chain
|
|
69
|
-
console.log('Receipt Hash:', result.receipt.receiptHash);
|
|
70
|
-
console.log('Input Hash:', result.receipt.input_hash);
|
|
71
|
-
console.log('Output Hash:', result.receipt.output_hash);
|
|
71
|
+
console.log('Receipt Hash:', result.receipt.receiptHash); // BLAKE3(entire receipt)
|
|
72
|
+
console.log('Input Hash:', result.receipt.input_hash); // BLAKE3(store before)
|
|
73
|
+
console.log('Output Hash:', result.receipt.output_hash); // BLAKE3(store after)
|
|
72
74
|
console.log('Previous Hash:', result.receipt.previousReceiptHash); // Links to prior op
|
|
73
75
|
```
|
|
74
76
|
|
|
@@ -114,6 +116,7 @@ Validates store against SHACL shape. Three enforcement modes for soft-fail gover
|
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
**Enforcement Modes**:
|
|
119
|
+
|
|
117
120
|
- `block`: Fail if violations exist (strict governance)
|
|
118
121
|
- `annotate`: Execute but add violations as RDF triples (soft-fail + audit trail)
|
|
119
122
|
- `repair`: Auto-fix via SPARQL CONSTRUCT then re-validate (self-healing)
|
|
@@ -265,10 +268,10 @@ Execute custom logic. Included for backwards compatibility.
|
|
|
265
268
|
Prevents hook execution if SHACL validation fails. Strict governance.
|
|
266
269
|
|
|
267
270
|
```javascript
|
|
268
|
-
{
|
|
269
|
-
kind: 'shacl',
|
|
271
|
+
{
|
|
272
|
+
kind: 'shacl',
|
|
270
273
|
ref: { uri: 'file:///shapes/strict.ttl' },
|
|
271
|
-
enforcementMode: 'block'
|
|
274
|
+
enforcementMode: 'block'
|
|
272
275
|
}
|
|
273
276
|
// Hook blocked if shape violations exist
|
|
274
277
|
// Result: Clean state or error
|
|
@@ -306,9 +309,9 @@ Auto-repairs violations via SPARQL CONSTRUCT, then re-validates.
|
|
|
306
309
|
?violation ex:repaired true .
|
|
307
310
|
?entity ex:status ex:Repaired .
|
|
308
311
|
}
|
|
309
|
-
WHERE {
|
|
310
|
-
?violation a sh:ValidationResult .
|
|
311
|
-
?violation sh:focusNode ?entity
|
|
312
|
+
WHERE {
|
|
313
|
+
?violation a sh:ValidationResult .
|
|
314
|
+
?violation sh:focusNode ?entity
|
|
312
315
|
}
|
|
313
316
|
`
|
|
314
317
|
}
|
|
@@ -348,7 +351,7 @@ const receipt1 = await engine.execute(ctx1, hooks1);
|
|
|
348
351
|
const receipt2 = await engine.execute(
|
|
349
352
|
createContext({
|
|
350
353
|
...ctx2,
|
|
351
|
-
previousReceiptHash: receipt1.receipt.receiptHash
|
|
354
|
+
previousReceiptHash: receipt1.receipt.receiptHash,
|
|
352
355
|
}),
|
|
353
356
|
hooks2
|
|
354
357
|
);
|
|
@@ -423,7 +426,7 @@ const bridge = new HooksBridge(store);
|
|
|
423
426
|
const hookId = await bridge.registerHook({
|
|
424
427
|
name: 'erlang-compliance',
|
|
425
428
|
condition: { kind: 'shacl', ref: { uri: '...' } },
|
|
426
|
-
effects: [{ kind: 'sparql-construct', query: '...' }]
|
|
429
|
+
effects: [{ kind: 'sparql-construct', query: '...' }],
|
|
427
430
|
});
|
|
428
431
|
|
|
429
432
|
// From Erlang: evaluate condition
|
|
@@ -431,7 +434,7 @@ const result = await bridge.evaluateCondition({
|
|
|
431
434
|
kind: 'datalog',
|
|
432
435
|
facts: ['user(alice)'],
|
|
433
436
|
rules: ['allowed(X) :- user(X)'],
|
|
434
|
-
goal: 'allowed(alice)'
|
|
437
|
+
goal: 'allowed(alice)',
|
|
435
438
|
});
|
|
436
439
|
|
|
437
440
|
// Full workflow with receipt
|
|
@@ -440,15 +443,15 @@ const receipt = await bridge.executeHooks(context, [hook]);
|
|
|
440
443
|
|
|
441
444
|
## Performance Characteristics
|
|
442
445
|
|
|
443
|
-
| Operation
|
|
444
|
-
|
|
445
|
-
| Receipt Creation
|
|
446
|
-
| SPARQL ASK
|
|
447
|
-
| SPARQL SELECT
|
|
448
|
-
| SHACL Validation
|
|
449
|
-
| N3 Inference
|
|
450
|
-
| Datalog Goal
|
|
451
|
-
| Full Hook Execution | <150ms
|
|
446
|
+
| Operation | Latency | Notes |
|
|
447
|
+
| ------------------- | -------- | ---------------------------------- |
|
|
448
|
+
| Receipt Creation | <1ms | BLAKE3 hashing |
|
|
449
|
+
| SPARQL ASK | 1-5ms | Depends on query complexity |
|
|
450
|
+
| SPARQL SELECT | 2-8ms | Result binding overhead |
|
|
451
|
+
| SHACL Validation | 5-15ms | Shape size dependent |
|
|
452
|
+
| N3 Inference | 10-100ms | Rule complexity & triple count |
|
|
453
|
+
| Datalog Goal | 1-30ms | Fixpoint iterations |
|
|
454
|
+
| Full Hook Execution | <150ms | All conditions + effects + receipt |
|
|
452
455
|
|
|
453
456
|
## Architecture
|
|
454
457
|
|
|
@@ -529,18 +532,18 @@ pnpm benchmark
|
|
|
529
532
|
class KnowledgeHookEngine {
|
|
530
533
|
// Register a hook with 6 priorities
|
|
531
534
|
registerHook(hook: KnowledgeHook): string;
|
|
532
|
-
|
|
535
|
+
|
|
533
536
|
// Evaluate any of 9 condition kinds
|
|
534
537
|
async evaluateCondition(
|
|
535
538
|
condition: Condition
|
|
536
539
|
): Promise<boolean>;
|
|
537
|
-
|
|
540
|
+
|
|
538
541
|
// Execute hooks with receipt chaining
|
|
539
542
|
async execute(
|
|
540
543
|
context: ExecutionContext,
|
|
541
544
|
hooks: KnowledgeHook[]
|
|
542
545
|
): Promise<ExecutionResult>;
|
|
543
|
-
|
|
546
|
+
|
|
544
547
|
// Get receipt chain (BLAKE3 linked)
|
|
545
548
|
getReceiptChain(): Receipt[];
|
|
546
549
|
}
|
|
@@ -564,6 +567,7 @@ See [API-REFERENCE.md](./API-REFERENCE.md) for complete schema.
|
|
|
564
567
|
See [CONTRIBUTING.md](./docs/CONTRIBUTING.md) for development guidelines.
|
|
565
568
|
|
|
566
569
|
All code follows:
|
|
570
|
+
|
|
567
571
|
- 100% ESM (.mjs)
|
|
568
572
|
- JSDoc documentation
|
|
569
573
|
- Zod validation
|
|
@@ -586,10 +590,10 @@ MIT — see [LICENSE](./LICENSE) for full text.
|
|
|
586
590
|
|
|
587
591
|
This package depends on the following notable third-party libraries:
|
|
588
592
|
|
|
589
|
-
| Dependency
|
|
590
|
-
|
|
591
|
-
| [eyereasoner](https://github.com/eyereasoner/eye-js)
|
|
592
|
-
| [swipl-wasm](https://github.com/nickmcdowall/swipl-wasm)
|
|
593
|
-
| [@noble/hashes](https://github.com/paulmillr/noble-hashes) | MIT
|
|
593
|
+
| Dependency | License | Purpose |
|
|
594
|
+
| ---------------------------------------------------------- | ------------ | ------------------------------------------------------------ |
|
|
595
|
+
| [eyereasoner](https://github.com/eyereasoner/eye-js) | MIT | N3 forward-chaining inference via EYE reasoner (WebAssembly) |
|
|
596
|
+
| [swipl-wasm](https://github.com/nickmcdowall/swipl-wasm) | BSD-2-Clause | SWI-Prolog runtime bundled by eyereasoner |
|
|
597
|
+
| [@noble/hashes](https://github.com/paulmillr/noble-hashes) | MIT | BLAKE3 cryptographic hashing for receipt chains |
|
|
594
598
|
|
|
595
599
|
All dependencies use permissive open-source licenses compatible with MIT.
|
|
@@ -80,14 +80,10 @@ const aclPolicy = defineHook({
|
|
|
80
80
|
name: 'acl-policy',
|
|
81
81
|
trigger: 'before-add',
|
|
82
82
|
validate: quad => {
|
|
83
|
-
const trustedNamespaces = [
|
|
84
|
-
'http://example.org/',
|
|
85
|
-
'http://xmlns.com/foaf/0.1/',
|
|
86
|
-
];
|
|
83
|
+
const trustedNamespaces = ['http://example.org/', 'http://xmlns.com/foaf/0.1/'];
|
|
87
84
|
// Check subject and predicate IRIs
|
|
88
|
-
return trustedNamespaces.some(
|
|
89
|
-
quad.subject.value.startsWith(ns) ||
|
|
90
|
-
quad.predicate.value.startsWith(ns)
|
|
85
|
+
return trustedNamespaces.some(
|
|
86
|
+
ns => quad.subject.value.startsWith(ns) || quad.predicate.value.startsWith(ns)
|
|
91
87
|
);
|
|
92
88
|
},
|
|
93
89
|
});
|
|
@@ -162,8 +158,8 @@ registerHook(registry, privacyPolicy);
|
|
|
162
158
|
registerHook(registry, provenancePolicy);
|
|
163
159
|
|
|
164
160
|
// Execute all policies
|
|
165
|
-
const
|
|
166
|
-
console.log(`
|
|
161
|
+
const result = await executeHooksByTrigger(registry, 'before-add', quad);
|
|
162
|
+
console.log(`Valid: ${result.valid}, hooks run: ${result.results.length}`);
|
|
167
163
|
```
|
|
168
164
|
|
|
169
165
|
## Testing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unrdf/hooks",
|
|
3
|
-
"version": "26.4.
|
|
3
|
+
"version": "26.4.7",
|
|
4
4
|
"description": "UNRDF Knowledge Hooks - Policy Definition and Execution Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^24.10.1",
|
|
60
|
+
"@unrdf/test-utils": "workspace:*",
|
|
60
61
|
"vitest": "^4.0.15"
|
|
61
62
|
},
|
|
62
63
|
"engines": {
|
|
@@ -256,7 +256,8 @@ export const normalizeLanguageTagPooled = defineHook({
|
|
|
256
256
|
quad.subject,
|
|
257
257
|
quad.predicate,
|
|
258
258
|
{
|
|
259
|
-
|
|
259
|
+
value: quad.object.value,
|
|
260
|
+
datatype: quad.object.datatype,
|
|
260
261
|
language: quad.object.language.toLowerCase(),
|
|
261
262
|
},
|
|
262
263
|
quad.graph
|
|
@@ -287,8 +288,9 @@ export const trimLiteralsPooled = defineHook({
|
|
|
287
288
|
quad.subject,
|
|
288
289
|
quad.predicate,
|
|
289
290
|
{
|
|
290
|
-
...quad.object,
|
|
291
291
|
value: trimmed,
|
|
292
|
+
datatype: quad.object.datatype,
|
|
293
|
+
language: quad.object.language,
|
|
292
294
|
},
|
|
293
295
|
quad.graph
|
|
294
296
|
);
|
|
@@ -47,8 +47,7 @@ export const SparqlParamSchema = z.union([
|
|
|
47
47
|
export function validateSparqlVariableName(name) {
|
|
48
48
|
if (typeof name !== 'string' || !SAFE_SPARQL_VAR_RE.test(name)) {
|
|
49
49
|
throw new Error(
|
|
50
|
-
`Invalid SPARQL variable name: "${String(name)}". ` +
|
|
51
|
-
'Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.'
|
|
50
|
+
`Invalid SPARQL variable name: "${String(name)}". ` + 'Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.'
|
|
52
51
|
);
|
|
53
52
|
}
|
|
54
53
|
return name;
|
|
@@ -107,9 +106,7 @@ export function bindSparqlParams(queryTemplate, params = {}) {
|
|
|
107
106
|
|
|
108
107
|
const result = SparqlParamSchema.safeParse(value);
|
|
109
108
|
if (!result.success) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`Invalid SPARQL parameter value for ?${varName}: ${result.error.message}`
|
|
112
|
-
);
|
|
109
|
+
throw new Error(`Invalid SPARQL parameter value for ?${varName}: ${result.error.message}`);
|
|
113
110
|
}
|
|
114
111
|
replacements.set(varName, toRdfTerm(value));
|
|
115
112
|
}
|
|
@@ -477,14 +477,10 @@ export function createHardenedContext() {
|
|
|
477
477
|
export function createSafeFunctionProxy() {
|
|
478
478
|
const handler = {
|
|
479
479
|
construct() {
|
|
480
|
-
throw new Error(
|
|
481
|
-
'SecurityError: Function constructor is disabled in sandbox'
|
|
482
|
-
);
|
|
480
|
+
throw new Error('SecurityError: Function constructor is disabled in sandbox');
|
|
483
481
|
},
|
|
484
482
|
apply() {
|
|
485
|
-
throw new Error(
|
|
486
|
-
'SecurityError: Function execution is disabled in sandbox'
|
|
487
|
-
);
|
|
483
|
+
throw new Error('SecurityError: Function execution is disabled in sandbox');
|
|
488
484
|
},
|
|
489
485
|
get(target, prop) {
|
|
490
486
|
if (prop === 'constructor' || prop === 'prototype' || prop === '__proto__') {
|
|
@@ -496,27 +492,19 @@ export function createSafeFunctionProxy() {
|
|
|
496
492
|
return undefined;
|
|
497
493
|
},
|
|
498
494
|
set() {
|
|
499
|
-
throw new Error(
|
|
500
|
-
'SecurityError: Cannot modify SafeFunction proxy'
|
|
501
|
-
);
|
|
495
|
+
throw new Error('SecurityError: Cannot modify SafeFunction proxy');
|
|
502
496
|
},
|
|
503
497
|
defineProperty() {
|
|
504
|
-
throw new Error(
|
|
505
|
-
'SecurityError: Cannot define properties on SafeFunction proxy'
|
|
506
|
-
);
|
|
498
|
+
throw new Error('SecurityError: Cannot define properties on SafeFunction proxy');
|
|
507
499
|
},
|
|
508
500
|
deleteProperty() {
|
|
509
|
-
throw new Error(
|
|
510
|
-
'SecurityError: Cannot delete properties on SafeFunction proxy'
|
|
511
|
-
);
|
|
501
|
+
throw new Error('SecurityError: Cannot delete properties on SafeFunction proxy');
|
|
512
502
|
},
|
|
513
503
|
getPrototypeOf() {
|
|
514
504
|
return null;
|
|
515
505
|
},
|
|
516
506
|
setPrototypeOf() {
|
|
517
|
-
throw new Error(
|
|
518
|
-
'SecurityError: Cannot set prototype on SafeFunction proxy'
|
|
519
|
-
);
|
|
507
|
+
throw new Error('SecurityError: Cannot set prototype on SafeFunction proxy');
|
|
520
508
|
},
|
|
521
509
|
};
|
|
522
510
|
|
|
@@ -555,9 +543,9 @@ export function logInjectionAttempt(code, violations, log = []) {
|
|
|
555
543
|
|
|
556
544
|
console.warn(
|
|
557
545
|
`[EffectExecutor] INJECTION ATTEMPT BLOCKED:\n` +
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
546
|
+
` Violations: ${violations.join(', ')}\n` +
|
|
547
|
+
` Code hash: ${attempt.codeHash}\n` +
|
|
548
|
+
` Timestamp: ${attempt.timestamp.toISOString()}`
|
|
561
549
|
);
|
|
562
550
|
|
|
563
551
|
return attempt;
|
|
@@ -219,7 +219,11 @@ export function executeHooksByTrigger(hooksOrRegistry, trigger, quad) {
|
|
|
219
219
|
let hooks;
|
|
220
220
|
|
|
221
221
|
// Handle registry object
|
|
222
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
hooksOrRegistry &&
|
|
224
|
+
typeof hooksOrRegistry === 'object' &&
|
|
225
|
+
hooksOrRegistry.hooks instanceof Map
|
|
226
|
+
) {
|
|
223
227
|
// Extract hooks for the specific trigger from registry
|
|
224
228
|
const triggerSet = hooksOrRegistry.triggerIndex.get(trigger);
|
|
225
229
|
if (!triggerSet) {
|
|
@@ -14,6 +14,7 @@ import { randomUUID } from 'crypto';
|
|
|
14
14
|
import { evaluateCondition } from './condition-evaluator.mjs';
|
|
15
15
|
import { KnowledgeHookEngine } from './knowledge-hook-engine.mjs';
|
|
16
16
|
import { ask, select, construct } from './query.mjs';
|
|
17
|
+
import { withPipelineSpan, withLLMSpan } from '../../../../src/lib/telemetry.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Build a self-play toolRegistry from knowledge hooks
|
|
@@ -29,81 +30,114 @@ export function buildHooksToolRegistry(store, _hooks = []) {
|
|
|
29
30
|
return {
|
|
30
31
|
hooks_evaluate_conditions: {
|
|
31
32
|
handler: async ({ store: evalStore, hooks: evalHooks }) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
return withLLMSpan('hooks-evaluate-conditions', async span => {
|
|
34
|
+
span.setAttributes({
|
|
35
|
+
'llm.provider': 'unrdf-hooks',
|
|
36
|
+
'llm.model': 'condition-evaluator',
|
|
37
|
+
'gen_ai.operation.name': 'hooks-evaluate-conditions',
|
|
38
|
+
'unrdf.hook.hooks_total': evalHooks.length,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Evaluate all conditions and collect results
|
|
42
|
+
const conditionResults = [];
|
|
43
|
+
const satisfied = [];
|
|
44
|
+
|
|
45
|
+
for (const hook of evalHooks) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await evaluateCondition(hook.condition, evalStore);
|
|
48
|
+
conditionResults.push({
|
|
49
|
+
hookName: hook.name,
|
|
50
|
+
condition: hook.condition,
|
|
51
|
+
satisfied: result,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (result) {
|
|
55
|
+
satisfied.push(hook);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
conditionResults.push({
|
|
59
|
+
hookName: hook.name,
|
|
60
|
+
condition: hook.condition,
|
|
61
|
+
satisfied: false,
|
|
62
|
+
error: err.message,
|
|
63
|
+
});
|
|
47
64
|
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
conditionResults.push({
|
|
50
|
-
hookName: hook.name,
|
|
51
|
-
condition: hook.condition,
|
|
52
|
-
satisfied: false,
|
|
53
|
-
error: err.message,
|
|
54
|
-
});
|
|
55
65
|
}
|
|
56
|
-
}
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
const successRate = evalHooks.length > 0 ? satisfied.length / evalHooks.length : 0;
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
span.setAttributes({
|
|
70
|
+
'unrdf.hook.hooks_satisfied': satisfied.length,
|
|
71
|
+
'unrdf.hook.satisfied': satisfied.length > 0,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
conditionResults,
|
|
76
|
+
satisfied,
|
|
77
|
+
successRate,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
65
80
|
},
|
|
66
81
|
},
|
|
67
82
|
|
|
68
83
|
hooks_execute_effects: {
|
|
69
84
|
handler: async ({ store: _execStore, hooks: execHooks, delta: _delta }) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
return withLLMSpan('hooks-execute-effects', async span => {
|
|
86
|
+
span.setAttributes({
|
|
87
|
+
'llm.provider': 'unrdf-hooks',
|
|
88
|
+
'llm.model': 'knowledge-hook-engine',
|
|
89
|
+
'gen_ai.operation.name': 'hooks-execute-effects',
|
|
90
|
+
'unrdf.hook.hooks_total': execHooks.length,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Execute hooks with receipt chaining
|
|
94
|
+
const context = {
|
|
95
|
+
nodeId: 'self-play-autonomics',
|
|
96
|
+
t_ns: BigInt(Date.now() * 1000000),
|
|
97
|
+
};
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
try {
|
|
100
|
+
const result = await engine.execute(context, execHooks);
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
span.setAttributes({
|
|
103
|
+
'pipeline.repairs.applied': result.successful,
|
|
104
|
+
'pipeline.repairs.failed': result.failed,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
executionResults: execHooks.map(h => ({
|
|
109
|
+
hookName: h.name,
|
|
110
|
+
status: 'executed',
|
|
111
|
+
})),
|
|
112
|
+
receipt: {
|
|
113
|
+
receiptHash: result.receipt.receiptHash,
|
|
114
|
+
input_hash: result.receipt.input_hash,
|
|
115
|
+
output_hash: result.receipt.output_hash,
|
|
116
|
+
previousReceiptHash: result.receipt.previousReceiptHash || null,
|
|
117
|
+
hooksExecuted: result.successful + result.failed,
|
|
118
|
+
successful: result.successful,
|
|
119
|
+
failed: result.failed,
|
|
120
|
+
delta: result.receipt.delta,
|
|
121
|
+
timestamp: result.receipt.timestamp,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
} catch (err) {
|
|
125
|
+
span.setAttributes({
|
|
126
|
+
'pipeline.repairs.applied': 0,
|
|
127
|
+
'pipeline.repairs.failed': execHooks.length,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
executionResults: execHooks.map(h => ({
|
|
132
|
+
hookName: h.name,
|
|
133
|
+
status: 'failed',
|
|
134
|
+
error: err.message,
|
|
135
|
+
})),
|
|
136
|
+
receipt: null,
|
|
101
137
|
error: err.message,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
107
141
|
},
|
|
108
142
|
},
|
|
109
143
|
|
|
@@ -260,164 +294,171 @@ class ReceiptChainNode {
|
|
|
260
294
|
* @returns {Promise<Object>} { episodes, finalStore, receiptChain, stats }
|
|
261
295
|
*/
|
|
262
296
|
export async function runHooksAutonomics(store, hookDefinitions = [], options = {}) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const context = {
|
|
294
|
-
store,
|
|
295
|
-
hooks: hookDefinitions,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
let previousResult = null;
|
|
297
|
+
return withPipelineSpan('observe', async span => {
|
|
298
|
+
const {
|
|
299
|
+
goalCondition = async () => false,
|
|
300
|
+
episodeCount = 3,
|
|
301
|
+
maxStepsPerEpisode = 10,
|
|
302
|
+
onEpisodeEnd = () => {},
|
|
303
|
+
} = options;
|
|
304
|
+
|
|
305
|
+
// Build tool registry
|
|
306
|
+
const toolRegistry = buildHooksToolRegistry(store, hookDefinitions);
|
|
307
|
+
|
|
308
|
+
// Create decision policy
|
|
309
|
+
const decisionFn = createHooksAwarePolicy(goalCondition);
|
|
310
|
+
|
|
311
|
+
// Track receipt chain
|
|
312
|
+
const receiptChain = [];
|
|
313
|
+
let previousReceiptNode = null;
|
|
314
|
+
|
|
315
|
+
const episodes = [];
|
|
316
|
+
|
|
317
|
+
for (let e = 0; e < episodeCount; e++) {
|
|
318
|
+
const episode = {
|
|
319
|
+
episodeId: randomUUID(),
|
|
320
|
+
stepCount: 0,
|
|
321
|
+
stepResults: [],
|
|
322
|
+
feedback: [],
|
|
323
|
+
terminated: false,
|
|
324
|
+
terminationReason: null,
|
|
325
|
+
timestamp: new Date().toISOString(),
|
|
326
|
+
};
|
|
299
327
|
|
|
300
|
-
|
|
301
|
-
|
|
328
|
+
const context = {
|
|
329
|
+
store,
|
|
330
|
+
hooks: hookDefinitions,
|
|
331
|
+
};
|
|
302
332
|
|
|
303
|
-
|
|
304
|
-
const decision = await decisionFn(
|
|
305
|
-
{
|
|
306
|
-
steps: episode.stepResults,
|
|
307
|
-
context,
|
|
308
|
-
recordFeedback: (s, r) => episode.feedback.push({ signal: s, reason: r }),
|
|
309
|
-
},
|
|
310
|
-
previousResult
|
|
311
|
-
);
|
|
333
|
+
let previousResult = null;
|
|
312
334
|
|
|
313
|
-
|
|
314
|
-
episode.terminated
|
|
315
|
-
episode.terminationReason = 'no more decisions';
|
|
316
|
-
episode.feedback.push({ signal: 0, reason: 'terminated: no more decisions' });
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
335
|
+
for (let step = 0; step < maxStepsPerEpisode; step++) {
|
|
336
|
+
if (episode.terminated) break;
|
|
319
337
|
|
|
320
|
-
|
|
338
|
+
// Decide next tool
|
|
339
|
+
const decision = await decisionFn(
|
|
340
|
+
{
|
|
341
|
+
steps: episode.stepResults,
|
|
342
|
+
context,
|
|
343
|
+
recordFeedback: (s, r) => episode.feedback.push({ signal: s, reason: r }),
|
|
344
|
+
},
|
|
345
|
+
previousResult
|
|
346
|
+
);
|
|
321
347
|
|
|
322
|
-
|
|
323
|
-
const stepStartTime = Date.now();
|
|
324
|
-
try {
|
|
325
|
-
const tool = toolRegistry[toolName];
|
|
326
|
-
if (!tool) {
|
|
348
|
+
if (!decision) {
|
|
327
349
|
episode.terminated = true;
|
|
328
|
-
episode.terminationReason =
|
|
329
|
-
episode.feedback.push({ signal:
|
|
350
|
+
episode.terminationReason = 'no more decisions';
|
|
351
|
+
episode.feedback.push({ signal: 0, reason: 'terminated: no more decisions' });
|
|
330
352
|
break;
|
|
331
353
|
}
|
|
332
354
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
toolName
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
const { toolName, input } = decision;
|
|
356
|
+
|
|
357
|
+
// Execute tool
|
|
358
|
+
const stepStartTime = Date.now();
|
|
359
|
+
try {
|
|
360
|
+
const tool = toolRegistry[toolName];
|
|
361
|
+
if (!tool) {
|
|
362
|
+
episode.terminated = true;
|
|
363
|
+
episode.terminationReason = `unknown tool: ${toolName}`;
|
|
364
|
+
episode.feedback.push({ signal: -1, reason: `unknown tool: ${toolName}` });
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const result = await tool.handler(input);
|
|
369
|
+
const duration = Date.now() - stepStartTime;
|
|
370
|
+
|
|
371
|
+
episode.stepResults.push({
|
|
372
|
+
stepId: randomUUID(),
|
|
373
|
+
toolName,
|
|
374
|
+
input,
|
|
375
|
+
output: result,
|
|
376
|
+
duration,
|
|
377
|
+
timestamp: new Date().toISOString(),
|
|
378
|
+
success: true,
|
|
379
|
+
});
|
|
345
380
|
|
|
346
|
-
|
|
381
|
+
previousResult = result;
|
|
382
|
+
|
|
383
|
+
// Record receipt if present
|
|
384
|
+
if (result?.receipt) {
|
|
385
|
+
const receiptNode = new ReceiptChainNode(result.receipt, previousReceiptNode);
|
|
386
|
+
receiptChain.push(receiptNode.toJSON());
|
|
387
|
+
previousReceiptNode = receiptNode;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Compute feedback
|
|
391
|
+
const feedback = computeHooksFeedback(result, previousResult);
|
|
392
|
+
episode.feedback.push({
|
|
393
|
+
signal: feedback,
|
|
394
|
+
reason: `${toolName} succeeded`,
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
const duration = Date.now() - stepStartTime;
|
|
398
|
+
|
|
399
|
+
episode.stepResults.push({
|
|
400
|
+
stepId: randomUUID(),
|
|
401
|
+
toolName,
|
|
402
|
+
input,
|
|
403
|
+
output: null,
|
|
404
|
+
duration,
|
|
405
|
+
timestamp: new Date().toISOString(),
|
|
406
|
+
success: false,
|
|
407
|
+
error: err.message,
|
|
408
|
+
});
|
|
347
409
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
receiptChain.push(receiptNode.toJSON());
|
|
352
|
-
previousReceiptNode = receiptNode;
|
|
410
|
+
episode.terminated = true;
|
|
411
|
+
episode.terminationReason = `tool failed: ${toolName}`;
|
|
412
|
+
episode.feedback.push({ signal: -0.5, reason: `${toolName} failed: ${err.message}` });
|
|
353
413
|
}
|
|
354
414
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
episode.feedback.push({
|
|
358
|
-
signal: feedback,
|
|
359
|
-
reason: `${toolName} succeeded`,
|
|
360
|
-
});
|
|
361
|
-
} catch (err) {
|
|
362
|
-
const duration = Date.now() - stepStartTime;
|
|
363
|
-
|
|
364
|
-
episode.stepResults.push({
|
|
365
|
-
stepId: randomUUID(),
|
|
366
|
-
toolName,
|
|
367
|
-
input,
|
|
368
|
-
output: null,
|
|
369
|
-
duration,
|
|
370
|
-
timestamp: new Date().toISOString(),
|
|
371
|
-
success: false,
|
|
372
|
-
error: err.message,
|
|
373
|
-
});
|
|
415
|
+
episode.stepCount++;
|
|
416
|
+
}
|
|
374
417
|
|
|
418
|
+
if (!episode.terminated && episode.stepCount >= maxStepsPerEpisode) {
|
|
375
419
|
episode.terminated = true;
|
|
376
|
-
episode.terminationReason =
|
|
377
|
-
episode.feedback.push({ signal:
|
|
420
|
+
episode.terminationReason = 'max steps reached';
|
|
421
|
+
episode.feedback.push({ signal: 0, reason: 'max steps reached' });
|
|
378
422
|
}
|
|
379
423
|
|
|
380
|
-
|
|
381
|
-
|
|
424
|
+
// Calculate metrics
|
|
425
|
+
const totalFeedback = episode.feedback.reduce((sum, f) => sum + f.signal, 0);
|
|
426
|
+
const avgFeedback = episode.feedback.length > 0 ? totalFeedback / episode.feedback.length : 0;
|
|
427
|
+
|
|
428
|
+
episode.metrics = {
|
|
429
|
+
stepCount: episode.stepCount,
|
|
430
|
+
feedbackCount: episode.feedback.length,
|
|
431
|
+
totalFeedback,
|
|
432
|
+
avgFeedback,
|
|
433
|
+
};
|
|
382
434
|
|
|
383
|
-
|
|
384
|
-
episode
|
|
385
|
-
episode.terminationReason = 'max steps reached';
|
|
386
|
-
episode.feedback.push({ signal: 0, reason: 'max steps reached' });
|
|
435
|
+
episodes.push(episode);
|
|
436
|
+
await onEpisodeEnd(episode);
|
|
387
437
|
}
|
|
388
438
|
|
|
389
|
-
// Calculate
|
|
390
|
-
const totalFeedback =
|
|
391
|
-
const
|
|
439
|
+
// Calculate stats
|
|
440
|
+
const totalFeedback = episodes.reduce((sum, ep) => sum + ep.metrics.totalFeedback, 0);
|
|
441
|
+
const successCount = episodes.filter(ep => ep.metrics.totalFeedback > 0).length;
|
|
392
442
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
443
|
+
const stats = {
|
|
444
|
+
totalEpisodes: episodes.length,
|
|
445
|
+
successCount,
|
|
446
|
+
successRate: episodes.length > 0 ? successCount / episodes.length : 0,
|
|
396
447
|
totalFeedback,
|
|
397
|
-
avgFeedback,
|
|
448
|
+
avgFeedback: episodes.length > 0 ? totalFeedback / episodes.length : 0,
|
|
449
|
+
receiptChainLength: receiptChain.length,
|
|
398
450
|
};
|
|
399
451
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// Calculate stats
|
|
405
|
-
const totalFeedback = episodes.reduce((sum, ep) => sum + ep.metrics.totalFeedback, 0);
|
|
406
|
-
const successCount = episodes.filter(ep => ep.metrics.totalFeedback > 0).length;
|
|
407
|
-
|
|
408
|
-
const stats = {
|
|
409
|
-
totalEpisodes: episodes.length,
|
|
410
|
-
successCount,
|
|
411
|
-
successRate: episodes.length > 0 ? successCount / episodes.length : 0,
|
|
412
|
-
totalFeedback,
|
|
413
|
-
avgFeedback: episodes.length > 0 ? totalFeedback / episodes.length : 0,
|
|
414
|
-
receiptChainLength: receiptChain.length,
|
|
415
|
-
};
|
|
452
|
+
span.setAttributes({
|
|
453
|
+
'pipeline.stage': 'observe',
|
|
454
|
+
'pipeline.input.count': hookDefinitions.length,
|
|
455
|
+
});
|
|
416
456
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
457
|
+
return {
|
|
458
|
+
episodes,
|
|
459
|
+
finalStore: store,
|
|
460
|
+
receiptChain,
|
|
461
|
+
stats,
|
|
462
|
+
};
|
|
463
|
+
});
|
|
423
464
|
}
|