@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 CHANGED
@@ -3,7 +3,7 @@
3
3
  ![Version](https://img.shields.io/badge/version-6.0.0-blue) ![Production Ready](https://img.shields.io/badge/production-ready-green)
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' // Log violations but don't block
48
+ enforcementMode: 'annotate', // Log violations but don't block
49
49
  },
50
- effects: [{
51
- kind: 'sparql-construct',
52
- query: `
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); // BLAKE3(entire receipt)
70
- console.log('Input Hash:', result.receipt.input_hash); // BLAKE3(store before)
71
- console.log('Output Hash:', result.receipt.output_hash); // BLAKE3(store after)
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 | Latency | Notes |
444
- |-----------|---------|-------|
445
- | Receipt Creation | <1ms | BLAKE3 hashing |
446
- | SPARQL ASK | 1-5ms | Depends on query complexity |
447
- | SPARQL SELECT | 2-8ms | Result binding overhead |
448
- | SHACL Validation | 5-15ms | Shape size dependent |
449
- | N3 Inference | 10-100ms | Rule complexity & triple count |
450
- | Datalog Goal | 1-30ms | Fixpoint iterations |
451
- | Full Hook Execution | <150ms | All conditions + effects + receipt |
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 | License | Purpose |
590
- |------------|---------|---------|
591
- | [eyereasoner](https://github.com/eyereasoner/eye-js) | MIT | N3 forward-chaining inference via EYE reasoner (WebAssembly) |
592
- | [swipl-wasm](https://github.com/nickmcdowall/swipl-wasm) | BSD-2-Clause | SWI-Prolog runtime bundled by eyereasoner |
593
- | [@noble/hashes](https://github.com/paulmillr/noble-hashes) | MIT | BLAKE3 cryptographic hashing for receipt chains |
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.
@@ -11,8 +11,8 @@
11
11
  "test:coverage": "vitest run --coverage"
12
12
  },
13
13
  "dependencies": {
14
- "@unrdf/core": "26.4.3",
15
- "@unrdf/hooks": "26.4.3",
14
+ "@unrdf/core": "26.4.4",
15
+ "@unrdf/hooks": "26.4.4",
16
16
  "n3": "^1.26.0",
17
17
  "zod": "^4.1.13"
18
18
  },
@@ -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(ns =>
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 results = executeHooksByTrigger(registry, store, 'before-add', quad);
166
- console.log(`Passed: ${results.passed.length}/${results.total}`);
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
@@ -11,8 +11,8 @@
11
11
  "test:coverage": "vitest run --coverage"
12
12
  },
13
13
  "dependencies": {
14
- "@unrdf/core": "26.4.3",
15
- "@unrdf/hooks": "26.4.3",
14
+ "@unrdf/core": "26.4.4",
15
+ "@unrdf/hooks": "26.4.4",
16
16
  "n3": "^1.26.0",
17
17
  "zod": "^4.1.13"
18
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unrdf/hooks",
3
- "version": "26.4.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
- ...quad.object,
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
- ` Violations: ${violations.join(', ')}\n` +
559
- ` Code hash: ${attempt.codeHash}\n` +
560
- ` Timestamp: ${attempt.timestamp.toISOString()}`
546
+ ` Violations: ${violations.join(', ')}\n` +
547
+ ` Code hash: ${attempt.codeHash}\n` +
548
+ ` Timestamp: ${attempt.timestamp.toISOString()}`
561
549
  );
562
550
 
563
551
  return attempt;
@@ -110,7 +110,6 @@ export function compileHookChain(hooks) {
110
110
  .filter(Boolean)
111
111
  .join('\n ');
112
112
 
113
-
114
113
  // Compile the chain function
115
114
  const fnBody = `
116
115
  ${validationSteps}
@@ -219,7 +219,11 @@ export function executeHooksByTrigger(hooksOrRegistry, trigger, quad) {
219
219
  let hooks;
220
220
 
221
221
  // Handle registry object
222
- if (hooksOrRegistry && typeof hooksOrRegistry === 'object' && hooksOrRegistry.hooks instanceof Map) {
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
- // Evaluate all conditions and collect results
33
- const conditionResults = [];
34
- const satisfied = [];
35
-
36
- for (const hook of evalHooks) {
37
- try {
38
- const result = await evaluateCondition(hook.condition, evalStore);
39
- conditionResults.push({
40
- hookName: hook.name,
41
- condition: hook.condition,
42
- satisfied: result,
43
- });
44
-
45
- if (result) {
46
- satisfied.push(hook);
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
- const successRate = evalHooks.length > 0 ? satisfied.length / evalHooks.length : 0;
67
+ const successRate = evalHooks.length > 0 ? satisfied.length / evalHooks.length : 0;
59
68
 
60
- return {
61
- conditionResults,
62
- satisfied,
63
- successRate,
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
- // Execute hooks with receipt chaining
71
- const context = {
72
- nodeId: 'self-play-autonomics',
73
- t_ns: BigInt(Date.now() * 1000000),
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
- try {
77
- const result = await engine.execute(context, execHooks);
99
+ try {
100
+ const result = await engine.execute(context, execHooks);
78
101
 
79
- return {
80
- executionResults: execHooks.map(h => ({
81
- hookName: h.name,
82
- status: 'executed',
83
- })),
84
- receipt: {
85
- receiptHash: result.receipt.receiptHash,
86
- input_hash: result.receipt.input_hash,
87
- output_hash: result.receipt.output_hash,
88
- previousReceiptHash: result.receipt.previousReceiptHash || null,
89
- hooksExecuted: result.successful + result.failed,
90
- successful: result.successful,
91
- failed: result.failed,
92
- delta: result.receipt.delta,
93
- timestamp: result.receipt.timestamp,
94
- },
95
- };
96
- } catch (err) {
97
- return {
98
- executionResults: execHooks.map(h => ({
99
- hookName: h.name,
100
- status: 'failed',
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
- receipt: null,
104
- error: err.message,
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
- const {
264
- goalCondition = async () => false,
265
- episodeCount = 3,
266
- maxStepsPerEpisode = 10,
267
- onEpisodeEnd = () => {},
268
- } = options;
269
-
270
- // Build tool registry
271
- const toolRegistry = buildHooksToolRegistry(store, hookDefinitions);
272
-
273
- // Create decision policy
274
- const decisionFn = createHooksAwarePolicy(goalCondition);
275
-
276
- // Track receipt chain
277
- const receiptChain = [];
278
- let previousReceiptNode = null;
279
-
280
- const episodes = [];
281
-
282
- for (let e = 0; e < episodeCount; e++) {
283
- const episode = {
284
- episodeId: randomUUID(),
285
- stepCount: 0,
286
- stepResults: [],
287
- feedback: [],
288
- terminated: false,
289
- terminationReason: null,
290
- timestamp: new Date().toISOString(),
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
- for (let step = 0; step < maxStepsPerEpisode; step++) {
301
- if (episode.terminated) break;
328
+ const context = {
329
+ store,
330
+ hooks: hookDefinitions,
331
+ };
302
332
 
303
- // Decide next tool
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
- if (!decision) {
314
- episode.terminated = true;
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
- const { toolName, input } = decision;
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
- // Execute tool
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 = `unknown tool: ${toolName}`;
329
- episode.feedback.push({ signal: -1, reason: `unknown tool: ${toolName}` });
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 result = await tool.handler(input);
334
- const duration = Date.now() - stepStartTime;
335
-
336
- episode.stepResults.push({
337
- stepId: randomUUID(),
338
- toolName,
339
- input,
340
- output: result,
341
- duration,
342
- timestamp: new Date().toISOString(),
343
- success: true,
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
- previousResult = result;
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
- // Record receipt if present
349
- if (result?.receipt) {
350
- const receiptNode = new ReceiptChainNode(result.receipt, previousReceiptNode);
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
- // Compute feedback
356
- const feedback = computeHooksFeedback(result, previousResult);
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 = `tool failed: ${toolName}`;
377
- episode.feedback.push({ signal: -0.5, reason: `${toolName} failed: ${err.message}` });
420
+ episode.terminationReason = 'max steps reached';
421
+ episode.feedback.push({ signal: 0, reason: 'max steps reached' });
378
422
  }
379
423
 
380
- episode.stepCount++;
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
- if (!episode.terminated && episode.stepCount >= maxStepsPerEpisode) {
384
- episode.terminated = true;
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 metrics
390
- const totalFeedback = episode.feedback.reduce((sum, f) => sum + f.signal, 0);
391
- const avgFeedback = episode.feedback.length > 0 ? totalFeedback / episode.feedback.length : 0;
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
- episode.metrics = {
394
- stepCount: episode.stepCount,
395
- feedbackCount: episode.feedback.length,
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
- episodes.push(episode);
401
- await onEpisodeEnd(episode);
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
- return {
418
- episodes,
419
- finalStore: store,
420
- receiptChain,
421
- stats,
422
- };
457
+ return {
458
+ episodes,
459
+ finalStore: store,
460
+ receiptChain,
461
+ stats,
462
+ };
463
+ });
423
464
  }