@reactive-skills/runtime 0.4.1 โ†’ 0.4.3

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/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
 
2
+ ## [0.4.3] - 2026-09-14
3
+
4
+ - feat(telemetry): embed standalone live web dashboard at GET / (4ffb3a9)
5
+ - docs: add comprehensive job management documentation across readmes (e1a626a)
6
+ - docs: add practical telemetry inspection and metrics querying guide (ef37fb6)
7
+ - chore: release v0.4.2 (28c16b9)
8
+ - feat(runtime): add in-engine telemetry, template caching, and performance budget verification (7dfae37)
9
+
10
+ ## [0.4.2] - 2026-09-14
11
+
12
+ - feat(runtime): add in-engine telemetry, template caching, and performance budget verification (7dfae37)
13
+ - feat(telemetry): record real-time transition_duration_ms, slice_duration_ms, and slice_tokens_est in STATE_TRANSITION events
14
+ - feat(fsm): L1 Handlebars compiled template delegate cache for sub-millisecond prompt slicing
15
+ - feat(alarms): emit PERF_DEGRADATION event on slice (>10ms) or transition (>25ms) threshold breaches
16
+ - test(perf): add automated Vitest test suite enforcing P0 (<5ms) and P1 (<25ms) latency budgets
17
+
2
18
  ## [0.4.1] - 2026-09-14
3
19
 
4
20
  - feat(runtime,axi): add mcp job tools, template jobId context, and flexible jobs cli arguments (2b31451)
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reactive Skills Architecture (RSA) core runtime โ€” FSM engine, event store, guard evaluator, projection engine, job manager, and MCP server.
4
4
 
5
- > ๐Ÿš€ **What's New in v0.4.1:** MCP job management tools (`reactive_switch_job`, `reactive_archive_job`), template-level `jobId` context, and `PERFORMANCE-STANDARDS.md`. [Read Full Release Notes โ†’](https://github.com/Reactive-Skills/reactive-skills/releases/tag/v0.4.1) ยท [View Changelog](https://github.com/Reactive-Skills/reactive-skills/blob/main/CHANGELOG.md)
5
+ > ๐Ÿš€ **What's New in v0.4.2:** In-engine telemetry, sub-millisecond Handlebars template caching, automated performance budget tests, and degradation detection alarms. [Read Full Release Notes โ†’](https://github.com/Reactive-Skills/reactive-skills/releases/tag/v0.4.2) ยท [View Changelog](https://github.com/Reactive-Skills/reactive-skills/blob/main/CHANGELOG.md)
6
6
 
7
7
  ## Installation
8
8
 
@@ -69,7 +69,46 @@ Dual-mode event sourcing:
69
69
  1. **JSONL** (`.reactive/skills/<skill>/events.jsonl`) - Human-readable append-only log
70
70
  2. **SQLite** (`.reactive/skills/<skill>/events.db`) - Indexed relational database
71
71
 
72
+ ## Job & Run Management
73
+
74
+ The runtime isolates execution runs through `JobManager`:
75
+
76
+ ```ts
77
+ import { JobManager, FSMEngine } from '@reactive-skills/runtime';
78
+
79
+ // 1. Manage isolated runs:
80
+ const jobManager = new JobManager();
81
+ jobManager.createJob('my-skill', { id: 'sprint-1', name: 'Sprint 1 Run', setActive: true });
82
+
83
+ // 2. Instantiate engine targeted to a specific run:
84
+ const engine = new FSMEngine({
85
+ skillDir: './skills/my-skill',
86
+ jobId: 'sprint-1',
87
+ });
88
+
89
+ // Deliverables automatically mirror to .docs/my-skill/jobs/sprint-1/ and canonical .docs/
90
+ ```
91
+
92
+ ## Performance & Telemetry
93
+
94
+ The runtime captures execution telemetry and token estimates with zero latency penalty:
95
+
96
+ ```ts
97
+ // 1. In-turn prompt slice telemetry:
98
+ const slice = engine.generatePromptSlice();
99
+ console.log(slice.metrics);
100
+ // => { slice_duration_ms: 0.23, slice_tokens_est: 282, allowed_tools_count: 4 }
101
+
102
+ // 2. State transition telemetry:
103
+ const res = await engine.handleSignal('TEST_RAN', { exit_code: 0 });
104
+ console.log(res.metrics);
105
+ // => { transition_duration_ms: 1.05, slice_duration_ms: 0.23, slice_tokens_est: 282 }
106
+ ```
107
+
108
+ See [PERFORMANCE-STANDARDS.md](../../PERFORMANCE-STANDARDS.md) for full latency budgets and caching architecture.
109
+
72
110
  ## License
73
111
 
74
112
  MIT
75
113
 
114
+
@@ -1,6 +1,5 @@
1
- import { SkillManifest, StateDefinition, SignalEvent, PromptSlice, EventContext, ChildRunSummary, DecisionRecord } from './types.js';
1
+ import { SkillManifest, StateDefinition, SignalEvent, PromptSlice, ExecutionMetrics, EventContext, ChildRunSummary, DecisionRecord } from './types.js';
2
2
  import { EventStore } from './event-store.js';
3
- import { JobManager } from './job-manager.js';
4
3
  export interface FSMEngineOptions {
5
4
  skillDir: string;
6
5
  workspaceDir?: string;
@@ -10,6 +9,10 @@ export interface FSMEngineOptions {
10
9
  runId?: string;
11
10
  initialContext?: Record<string, any>;
12
11
  autoRehydrate?: boolean;
12
+ perfThresholds?: {
13
+ maxSliceDurationMs?: number;
14
+ maxTransitionDurationMs?: number;
15
+ };
13
16
  }
14
17
  export declare class FSMEngine {
15
18
  private skillDir;
@@ -28,6 +31,10 @@ export declare class FSMEngine {
28
31
  private jobId?;
29
32
  private isActiveJob;
30
33
  private jobManager;
34
+ private templateCache;
35
+ private lastSliceMetrics?;
36
+ private maxSliceDurationMs;
37
+ private maxTransitionDurationMs;
31
38
  constructor(options: FSMEngineOptions);
32
39
  /**
33
40
  * Rehydrate state machine state and context from immutable event history,
@@ -86,6 +93,7 @@ export declare class FSMEngine {
86
93
  event: SignalEvent;
87
94
  handledAtDepth?: number;
88
95
  deliverablesWritten: string[];
96
+ metrics?: ExecutionMetrics;
89
97
  }>;
90
98
  /**
91
99
  * Transition between two hierarchical paths executing exit, transition, and entry hooks
@@ -99,6 +107,7 @@ export declare class FSMEngine {
99
107
  private executeExitHook;
100
108
  getJobId(): string | undefined;
101
109
  isJobActive(): boolean;
102
- getJobManager(): JobManager;
110
+ getLastMetrics(): ExecutionMetrics | undefined;
111
+ clearTemplateCache(): void;
103
112
  close(): void;
104
113
  }
@@ -28,6 +28,10 @@ export class FSMEngine {
28
28
  jobId;
29
29
  isActiveJob;
30
30
  jobManager;
31
+ templateCache = new Map();
32
+ lastSliceMetrics;
33
+ maxSliceDurationMs = 10;
34
+ maxTransitionDurationMs = 25;
31
35
  constructor(options) {
32
36
  this.skillDir = path.resolve(options.skillDir);
33
37
  this.workspaceDir = options.workspaceDir || process.cwd();
@@ -35,6 +39,12 @@ export class FSMEngine {
35
39
  this.strictExecution = this.manifest.strict_execution === true;
36
40
  this.turnsSinceLastSignal = 0;
37
41
  this.inBypassState = false;
42
+ if (options.perfThresholds?.maxSliceDurationMs !== undefined) {
43
+ this.maxSliceDurationMs = options.perfThresholds.maxSliceDurationMs;
44
+ }
45
+ if (options.perfThresholds?.maxTransitionDurationMs !== undefined) {
46
+ this.maxTransitionDurationMs = options.perfThresholds.maxTransitionDurationMs;
47
+ }
38
48
  const effectiveJobId = options.jobId || options.runId;
39
49
  this.jobManager = new JobManager(this.workspaceDir);
40
50
  const activeJobId = this.jobManager.getActiveJobId(this.manifest.name);
@@ -357,6 +367,7 @@ export class FSMEngine {
357
367
  * Generates the prompt slice for the active state hierarchy
358
368
  */
359
369
  generatePromptSlice() {
370
+ const startTime = performance.now();
360
371
  const activeLeaf = this.getStateDefinition(this.activeStatePath);
361
372
  if (!activeLeaf) {
362
373
  throw new Error(`Active state definition not found: ${this.getCurrentState()}`);
@@ -392,8 +403,12 @@ export class FSMEngine {
392
403
  }
393
404
  let rawPrompt = '';
394
405
  if (templatePath && fs.existsSync(templatePath)) {
395
- const templateContent = fs.readFileSync(templatePath, 'utf8');
396
- const compiled = Handlebars.compile(templateContent);
406
+ let compiled = this.templateCache.get(templatePath);
407
+ if (!compiled) {
408
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
409
+ compiled = Handlebars.compile(templateContent);
410
+ this.templateCache.set(templatePath, compiled);
411
+ }
397
412
  rawPrompt = compiled({
398
413
  state: this.getCurrentState(),
399
414
  activeStatePath: this.activeStatePath,
@@ -458,6 +473,22 @@ export class FSMEngine {
458
473
  ` </transition_contracts>`,
459
474
  `</reactive_skill_state>`,
460
475
  ].filter(Boolean).join('\n');
476
+ const durationMs = Number((performance.now() - startTime).toFixed(3));
477
+ const estTokens = Math.ceil(formattedXml.length / 4);
478
+ const metrics = {
479
+ slice_duration_ms: durationMs,
480
+ slice_tokens_est: estTokens,
481
+ allowed_tools_count: allowedTools.length,
482
+ };
483
+ this.lastSliceMetrics = metrics;
484
+ if (durationMs > this.maxSliceDurationMs) {
485
+ this.eventStore.append('PERF_DEGRADATION', {
486
+ operation: 'generatePromptSlice',
487
+ duration_ms: durationMs,
488
+ threshold_ms: this.maxSliceDurationMs,
489
+ state: this.getCurrentState(),
490
+ }, { state: this.getCurrentState() });
491
+ }
461
492
  return {
462
493
  state: this.getCurrentState(),
463
494
  rawPrompt,
@@ -465,6 +496,7 @@ export class FSMEngine {
465
496
  allowedTools,
466
497
  context: { ...this.context },
467
498
  exitConditions,
499
+ metrics,
468
500
  };
469
501
  }
470
502
  /**
@@ -478,6 +510,7 @@ export class FSMEngine {
478
510
  * Process an incoming signal event, evaluating transitions with HSM bubbling and draining queued signals
479
511
  */
480
512
  async handleSignal(signalName, payload = {}, metadata = {}) {
513
+ const startTime = performance.now();
481
514
  const previousState = this.getCurrentState();
482
515
  const prevPath = [...this.activeStatePath];
483
516
  const event = this.eventStore.append('SIGNAL_EMITTED', { signal: signalName, ...payload }, { source: metadata.source, causationId: metadata.causationId, state: previousState });
@@ -531,8 +564,24 @@ export class FSMEngine {
531
564
  // Parse target path (supports dot notation, e.g. "REFACTOR.EXTRACT_METHOD" or "GREEN_CODE")
532
565
  const targetSegments = transDef.target.split('.');
533
566
  const fullTargetPath = this.resolveInitialPath(targetSegments);
567
+ const transitionDurationMs = Number((performance.now() - startTime).toFixed(3));
568
+ const metrics = {
569
+ transition_duration_ms: transitionDurationMs,
570
+ slice_duration_ms: this.lastSliceMetrics?.slice_duration_ms,
571
+ slice_tokens_est: this.lastSliceMetrics?.slice_tokens_est,
572
+ };
573
+ if (transitionDurationMs > this.maxTransitionDurationMs) {
574
+ this.eventStore.append('PERF_DEGRADATION', {
575
+ operation: 'handleSignal',
576
+ duration_ms: transitionDurationMs,
577
+ threshold_ms: this.maxTransitionDurationMs,
578
+ signal: signalName,
579
+ from: previousState,
580
+ to: fullTargetPath.join('.'),
581
+ }, { state: fullTargetPath.join('.') });
582
+ }
534
583
  // Execute exit hooks and entry hooks along the transition path
535
- this.transitionBetweenPaths(prevPath, fullTargetPath, signalName, event.id, payload);
584
+ this.transitionBetweenPaths(prevPath, fullTargetPath, signalName, event.id, payload, metrics);
536
585
  // PERF-02 / INV-08: Persist state snapshot for fast cold-boot rehydration
537
586
  this.eventStore.saveSnapshot(this.eventStore.getLatestSequence(), this.getCurrentState(), this.context);
538
587
  // Render deliverables
@@ -576,6 +625,7 @@ export class FSMEngine {
576
625
  event,
577
626
  handledAtDepth: depth,
578
627
  deliverablesWritten,
628
+ metrics,
579
629
  };
580
630
  }
581
631
  }
@@ -591,7 +641,7 @@ export class FSMEngine {
591
641
  /**
592
642
  * Transition between two hierarchical paths executing exit, transition, and entry hooks
593
643
  */
594
- transitionBetweenPaths(fromPath, toPath, signalName, causationId, payload) {
644
+ transitionBetweenPaths(fromPath, toPath, signalName, causationId, payload, metrics) {
595
645
  // 1. Find Lowest Common Ancestor (LCA)
596
646
  let lcaDepth = 0;
597
647
  const maxDepth = Math.min(fromPath.length, toPath.length);
@@ -609,6 +659,7 @@ export class FSMEngine {
609
659
  to: toPath.join('.'),
610
660
  signal: signalName,
611
661
  payload,
662
+ metrics,
612
663
  }, {
613
664
  state: toPath.join('.'),
614
665
  causationId,
@@ -692,8 +743,11 @@ export class FSMEngine {
692
743
  isJobActive() {
693
744
  return this.isActiveJob;
694
745
  }
695
- getJobManager() {
696
- return this.jobManager;
746
+ getLastMetrics() {
747
+ return this.lastSliceMetrics;
748
+ }
749
+ clearTemplateCache() {
750
+ this.templateCache.clear();
697
751
  }
698
752
  close() {
699
753
  this.eventStore.close();
@@ -116,6 +116,16 @@ export interface SkillManifest {
116
116
  states: Record<string, StateDefinition>;
117
117
  deliverable_projections?: DeliverableProjection[];
118
118
  }
119
+ /**
120
+ * Real-time execution performance metrics
121
+ */
122
+ export interface ExecutionMetrics {
123
+ slice_duration_ms?: number;
124
+ slice_tokens_est?: number;
125
+ transition_duration_ms?: number;
126
+ allowed_tools_count?: number;
127
+ [key: string]: any;
128
+ }
119
129
  /**
120
130
  * Hydrated prompt slice generated for an active LLM turn
121
131
  */
@@ -126,6 +136,7 @@ export interface PromptSlice {
126
136
  allowedTools: string[];
127
137
  context: Record<string, any>;
128
138
  exitConditions: string[];
139
+ metrics?: ExecutionMetrics;
129
140
  }
130
141
  /**
131
142
  * Zod Schema for validation of skill.yaml
package/dist/index.d.ts CHANGED
@@ -13,3 +13,4 @@ export { runSync } from './sync/engine.js';
13
13
  export { syncEngineCommand } from './sync/cli.js';
14
14
  export * from './telemetry/types.js';
15
15
  export * from './telemetry/server.js';
16
+ export * from './telemetry/dashboard.js';
package/dist/index.js CHANGED
@@ -13,3 +13,4 @@ export { runSync } from './sync/engine.js';
13
13
  export { syncEngineCommand } from './sync/cli.js';
14
14
  export * from './telemetry/types.js';
15
15
  export * from './telemetry/server.js';
16
+ export * from './telemetry/dashboard.js';
@@ -0,0 +1,10 @@
1
+ export interface DashboardOptions {
2
+ skillName?: string;
3
+ port: number;
4
+ host: string;
5
+ }
6
+ /**
7
+ * Generates a self-contained, zero-dependency HTML dashboard for the TelemetryServer.
8
+ * Provides live SSE event streaming, state & context inspection, and signal dispatching.
9
+ */
10
+ export declare function renderDashboardHtml(options: DashboardOptions): string;