@reactive-skills/runtime 0.4.1 โ†’ 0.4.2

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,12 @@
1
1
 
2
+ ## [0.4.2] - 2026-09-14
3
+
4
+ - feat(runtime): add in-engine telemetry, template caching, and performance budget verification (7dfae37)
5
+ - feat(telemetry): record real-time transition_duration_ms, slice_duration_ms, and slice_tokens_est in STATE_TRANSITION events
6
+ - feat(fsm): L1 Handlebars compiled template delegate cache for sub-millisecond prompt slicing
7
+ - feat(alarms): emit PERF_DEGRADATION event on slice (>10ms) or transition (>25ms) threshold breaches
8
+ - test(perf): add automated Vitest test suite enforcing P0 (<5ms) and P1 (<25ms) latency budgets
9
+
2
10
  ## [0.4.1] - 2026-09-14
3
11
 
4
12
  - 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
 
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactive-skills/runtime",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Reactive Skills Architecture (RSA) core runtime โ€” FSM engine, event store, guard evaluator, projection engine, MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",