@reactive-skills/runtime 0.1.1 → 0.3.0

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.
@@ -236,3 +236,37 @@ export declare const SkillManifestSchema: z.ZodObject<{
236
236
  trigger_on?: string[] | undefined;
237
237
  }[] | undefined;
238
238
  }>;
239
+ export declare const JobStatusSchema: z.ZodEnum<["active", "completed", "failed", "archived"]>;
240
+ export type JobStatus = z.infer<typeof JobStatusSchema>;
241
+ export declare const JobMetadataSchema: z.ZodObject<{
242
+ id: z.ZodString;
243
+ name: z.ZodString;
244
+ skillId: z.ZodString;
245
+ status: z.ZodEnum<["active", "completed", "failed", "archived"]>;
246
+ currentState: z.ZodString;
247
+ parentRunId: z.ZodOptional<z.ZodString>;
248
+ createdAt: z.ZodString;
249
+ updatedAt: z.ZodString;
250
+ completedAt: z.ZodOptional<z.ZodString>;
251
+ }, "strip", z.ZodTypeAny, {
252
+ status: "completed" | "failed" | "active" | "archived";
253
+ name: string;
254
+ id: string;
255
+ skillId: string;
256
+ currentState: string;
257
+ createdAt: string;
258
+ updatedAt: string;
259
+ parentRunId?: string | undefined;
260
+ completedAt?: string | undefined;
261
+ }, {
262
+ status: "completed" | "failed" | "active" | "archived";
263
+ name: string;
264
+ id: string;
265
+ skillId: string;
266
+ currentState: string;
267
+ createdAt: string;
268
+ updatedAt: string;
269
+ parentRunId?: string | undefined;
270
+ completedAt?: string | undefined;
271
+ }>;
272
+ export type JobMetadata = z.infer<typeof JobMetadataSchema>;
@@ -53,3 +53,15 @@ export const SkillManifestSchema = z.object({
53
53
  trigger_on: z.array(z.string()).optional(),
54
54
  })).optional(),
55
55
  });
56
+ export const JobStatusSchema = z.enum(['active', 'completed', 'failed', 'archived']);
57
+ export const JobMetadataSchema = z.object({
58
+ id: z.string(),
59
+ name: z.string(),
60
+ skillId: z.string(),
61
+ status: JobStatusSchema,
62
+ currentState: z.string(),
63
+ parentRunId: z.string().optional(),
64
+ createdAt: z.string(),
65
+ updatedAt: z.string(),
66
+ completedAt: z.string().optional(),
67
+ });
package/dist/index.d.ts CHANGED
@@ -6,7 +6,10 @@ export * from './core/fsm-engine.js';
6
6
  export * from './core/runtime-hooks.js';
7
7
  export * from './core/legacy-adapter.js';
8
8
  export * from './core/migration.js';
9
+ export * from './core/job-manager.js';
9
10
  export * from './mcp/server.js';
10
11
  export * from './sync/types.js';
11
12
  export { runSync } from './sync/engine.js';
12
13
  export { syncEngineCommand } from './sync/cli.js';
14
+ export * from './telemetry/types.js';
15
+ export * from './telemetry/server.js';
package/dist/index.js CHANGED
@@ -6,7 +6,10 @@ export * from './core/fsm-engine.js';
6
6
  export * from './core/runtime-hooks.js';
7
7
  export * from './core/legacy-adapter.js';
8
8
  export * from './core/migration.js';
9
+ export * from './core/job-manager.js';
9
10
  export * from './mcp/server.js';
10
11
  export * from './sync/types.js';
11
12
  export { runSync } from './sync/engine.js';
12
13
  export { syncEngineCommand } from './sync/cli.js';
14
+ export * from './telemetry/types.js';
15
+ export * from './telemetry/server.js';
@@ -7,6 +7,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
7
  import { z } from 'zod';
8
8
  import { FSMEngine } from '../core/fsm-engine.js';
9
9
  import { EventStore } from '../core/event-store.js';
10
+ import { JobManager } from '../core/job-manager.js';
10
11
  import { SkillManifestSchema } from '../core/types.js';
11
12
  export function createReactiveMcpServer(options = {}) {
12
13
  const workspaceDir = options.workspaceDir || process.cwd();
@@ -15,7 +16,7 @@ export function createReactiveMcpServer(options = {}) {
15
16
  name: 'reactive-skills-server',
16
17
  version: '1.0.0',
17
18
  });
18
- // Cached active engine instance per skill
19
+ // Cached active engine instance per skill and job
19
20
  const engines = new Map();
20
21
  function normalizeDeliverableName(name) {
21
22
  const trimmed = String(name || '').trim();
@@ -27,13 +28,16 @@ export function createReactiveMcpServer(options = {}) {
27
28
  }
28
29
  return trimmed;
29
30
  }
30
- function getEngine(skillName = defaultSkill) {
31
- if (engines.has(skillName)) {
32
- const cached = engines.get(skillName);
31
+ function getEngine(skillName = defaultSkill, jobId) {
32
+ const jobManager = new JobManager(workspaceDir);
33
+ const resolvedJobId = jobId || jobManager.getActiveJobId(skillName);
34
+ const cacheKey = `${skillName}::${resolvedJobId}`;
35
+ if (engines.has(cacheKey)) {
36
+ const cached = engines.get(cacheKey);
33
37
  if (fs.existsSync(cached.getSkillDir())) {
34
38
  return cached;
35
39
  }
36
- engines.delete(skillName);
40
+ engines.delete(cacheKey);
37
41
  }
38
42
  const candidatePaths = [
39
43
  path.resolve(workspaceDir, 'skills', skillName),
@@ -74,18 +78,26 @@ export function createReactiveMcpServer(options = {}) {
74
78
  const eventStore = new EventStore({
75
79
  workspaceDir,
76
80
  skillId: skillName,
81
+ jobId: resolvedJobId,
82
+ runId: resolvedJobId,
77
83
  enableSqlite: true,
78
84
  });
79
- const engine = new FSMEngine({ skillDir, workspaceDir, eventStore });
80
- engines.set(skillName, engine);
85
+ const engine = new FSMEngine({
86
+ skillDir,
87
+ workspaceDir,
88
+ eventStore,
89
+ jobId: resolvedJobId,
90
+ });
91
+ engines.set(cacheKey, engine);
81
92
  return engine;
82
93
  }
83
94
  // 1. TOOL: reactive_state
84
95
  server.tool('reactive_state', 'Get current state, prompt slice, and allowed tools for the active reactive skill', {
85
96
  skill: z.string().optional().describe('Skill name (defaults to active skill)'),
86
- }, async ({ skill }) => {
97
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
98
+ }, async ({ skill, job_id }) => {
87
99
  try {
88
- const engine = getEngine(skill || defaultSkill);
100
+ const engine = getEngine(skill || defaultSkill, job_id);
89
101
  if (engine.isBypassDetected()) {
90
102
  return {
91
103
  content: [
@@ -114,6 +126,7 @@ export function createReactiveMcpServer(options = {}) {
114
126
  type: 'text',
115
127
  text: JSON.stringify({
116
128
  skill: engine.getManifest().name,
129
+ job_id: engine.getJobId(),
117
130
  activeState,
118
131
  isWaitingForHuman: isWaiting,
119
132
  allowedTools: slice.allowedTools,
@@ -145,15 +158,18 @@ export function createReactiveMcpServer(options = {}) {
145
158
  signal: z.string().describe('Signal name (e.g. CHECK_PASSED, CHARTER_DRAFTED)'),
146
159
  payload: z.record(z.any()).optional().describe('Signal payload data (e.g. exit_code, file_path)'),
147
160
  skill: z.string().optional().describe('Target skill name'),
148
- }, async ({ signal, payload = {}, skill }) => {
161
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
162
+ }, async ({ signal, payload = {}, skill, job_id }) => {
149
163
  try {
150
- const engine = getEngine(skill || defaultSkill);
164
+ const engine = getEngine(skill || defaultSkill, job_id);
151
165
  const result = await engine.handleSignal(signal, payload);
152
166
  return {
153
167
  content: [
154
168
  {
155
169
  type: 'text',
156
170
  text: JSON.stringify({
171
+ skill: engine.getManifest().name,
172
+ job_id: engine.getJobId(),
157
173
  transitioned: result.transitioned,
158
174
  previousState: result.previousState,
159
175
  newState: result.newState,
@@ -175,9 +191,10 @@ export function createReactiveMcpServer(options = {}) {
175
191
  server.tool('reactive_query', 'Execute a read-only SQL query against the SQLite event store (.reactive/events.db)', {
176
192
  sql: z.string().describe('SQL query string (e.g. SELECT * FROM events ORDER BY seq DESC LIMIT 10)'),
177
193
  skill: z.string().optional().describe('Skill context for event store'),
178
- }, async ({ sql, skill }) => {
194
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
195
+ }, async ({ sql, skill, job_id }) => {
179
196
  try {
180
- const engine = getEngine(skill || defaultSkill);
197
+ const engine = getEngine(skill || defaultSkill, job_id);
181
198
  const driver = engine.getEventStore().getSqliteDriver();
182
199
  if (!driver) {
183
200
  throw new Error('SQLite storage driver is not active.');
@@ -206,9 +223,10 @@ export function createReactiveMcpServer(options = {}) {
206
223
  sinceSeq: z.number().int().nonnegative().optional().describe('Return events after this sequence'),
207
224
  limit: z.number().int().positive().max(1000).optional().describe('Maximum number of events'),
208
225
  skill: z.string().optional().describe('Skill context for event store'),
209
- }, async ({ type, state, sinceSeq, limit, skill }) => {
226
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
227
+ }, async ({ type, state, sinceSeq, limit, skill, job_id }) => {
210
228
  try {
211
- const engine = getEngine(skill || defaultSkill);
229
+ const engine = getEngine(skill || defaultSkill, job_id);
212
230
  const rows = engine.getEventStore().query({ type, state, sinceSeq, limit });
213
231
  return {
214
232
  content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
@@ -268,9 +286,10 @@ export function createReactiveMcpServer(options = {}) {
268
286
  // 5. TOOL: reactive_inspect
269
287
  server.tool('reactive_inspect', 'Inspect the full statechart, transitions, and guard criteria of a reactive skill', {
270
288
  skill: z.string().optional().describe('Skill name to inspect'),
271
- }, async ({ skill }) => {
289
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
290
+ }, async ({ skill, job_id }) => {
272
291
  try {
273
- const engine = getEngine(skill || defaultSkill);
292
+ const engine = getEngine(skill || defaultSkill, job_id);
274
293
  const manifest = engine.getManifest();
275
294
  return {
276
295
  content: [
@@ -319,15 +338,16 @@ export function createReactiveMcpServer(options = {}) {
319
338
  };
320
339
  }
321
340
  });
322
- // 6. TOOL: reactive_respond_human
341
+ // 7. TOOL: reactive_respond_human
323
342
  server.tool('reactive_respond_human', 'Submit user approval or feedback to unpause a Human-in-the-Loop (HITL) gate', {
324
343
  choice: z.string().describe('User selected choice (e.g. Approve Plan)'),
325
344
  approved: z.boolean().optional().describe('Explicit approval boolean flag'),
326
345
  feedback: z.string().optional().describe('Optional feedback text'),
327
346
  skill: z.string().optional().describe('Target skill name'),
328
- }, async ({ choice, approved = true, feedback, skill }) => {
347
+ job_id: z.string().optional().describe('Optional job/run ID (defaults to active job)'),
348
+ }, async ({ choice, approved = true, feedback, skill, job_id }) => {
329
349
  try {
330
- const engine = getEngine(skill || defaultSkill);
350
+ const engine = getEngine(skill || defaultSkill, job_id);
331
351
  const signalsEmitted = [];
332
352
  let transitioned = false;
333
353
  let deliverablesWritten = [];
@@ -427,6 +447,35 @@ export function createReactiveMcpServer(options = {}) {
427
447
  };
428
448
  }
429
449
  });
450
+ // 9. TOOL: reactive_list_jobs
451
+ server.tool('reactive_list_jobs', 'List all historical and active execution jobs for a skill with status metadata', {
452
+ skill: z.string().optional().describe('Skill name (defaults to active skill)'),
453
+ }, async ({ skill }) => {
454
+ try {
455
+ const targetSkill = skill || defaultSkill;
456
+ const jobManager = new JobManager(workspaceDir);
457
+ const activeJobId = jobManager.getActiveJobId(targetSkill);
458
+ const jobs = jobManager.listJobs(targetSkill);
459
+ return {
460
+ content: [
461
+ {
462
+ type: 'text',
463
+ text: JSON.stringify({
464
+ skill: targetSkill,
465
+ activeJobId,
466
+ jobs,
467
+ }, null, 2),
468
+ },
469
+ ],
470
+ };
471
+ }
472
+ catch (err) {
473
+ return {
474
+ content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
475
+ isError: true,
476
+ };
477
+ }
478
+ });
430
479
  // RESOURCE 1: reactive://events
431
480
  server.resource('reactive-events', 'reactive://events', async (uri) => {
432
481
  const store = new EventStore({ enableSqlite: true });
@@ -0,0 +1,31 @@
1
+ import { TelemetryServerOptions } from './types.js';
2
+ export declare class TelemetryServer {
3
+ private server;
4
+ private eventStore;
5
+ private fsmEngine?;
6
+ private port;
7
+ private host;
8
+ private heartbeatIntervalMs;
9
+ private skillName?;
10
+ private startTime;
11
+ private activeClients;
12
+ private activeSockets;
13
+ private unsubscribeEventStore?;
14
+ constructor(options: TelemetryServerOptions);
15
+ getPort(): number;
16
+ getUrl(): string;
17
+ start(): Promise<{
18
+ port: number;
19
+ url: string;
20
+ }>;
21
+ stop(): Promise<void>;
22
+ private setCorsHeaders;
23
+ private handleRequest;
24
+ private handleHealth;
25
+ private handleState;
26
+ private handleEventsHistory;
27
+ private handleSseEvents;
28
+ private sendSseEvent;
29
+ private broadcastEvent;
30
+ private handleSignal;
31
+ }
@@ -0,0 +1,275 @@
1
+ import http from 'node:http';
2
+ import { URL } from 'node:url';
3
+ export class TelemetryServer {
4
+ server = null;
5
+ eventStore;
6
+ fsmEngine;
7
+ port;
8
+ host;
9
+ heartbeatIntervalMs;
10
+ skillName;
11
+ startTime = Date.now();
12
+ activeClients = new Set();
13
+ activeSockets = new Set();
14
+ unsubscribeEventStore;
15
+ constructor(options) {
16
+ this.eventStore = options.eventStore;
17
+ this.fsmEngine = options.fsmEngine;
18
+ this.port = options.port ?? 4242;
19
+ this.host = options.host ?? '127.0.0.1';
20
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15000;
21
+ this.skillName = options.skillName ?? (options.fsmEngine ? options.fsmEngine.getManifest().name : undefined);
22
+ }
23
+ getPort() {
24
+ if (!this.server)
25
+ return this.port;
26
+ const addr = this.server.address();
27
+ if (typeof addr === 'object' && addr !== null) {
28
+ return addr.port;
29
+ }
30
+ return this.port;
31
+ }
32
+ getUrl() {
33
+ return `http://${this.host}:${this.getPort()}`;
34
+ }
35
+ async start() {
36
+ if (this.server) {
37
+ return { port: this.getPort(), url: this.getUrl() };
38
+ }
39
+ this.server = http.createServer((req, res) => {
40
+ this.handleRequest(req, res);
41
+ });
42
+ this.server.on('connection', (socket) => {
43
+ this.activeSockets.add(socket);
44
+ socket.on('close', () => {
45
+ this.activeSockets.delete(socket);
46
+ });
47
+ });
48
+ // Subscribe to EventStore updates to broadcast to all connected SSE clients
49
+ this.unsubscribeEventStore = this.eventStore.subscribe((event) => {
50
+ this.broadcastEvent(event);
51
+ });
52
+ return new Promise((resolve, reject) => {
53
+ this.server.once('error', reject);
54
+ this.server.listen(this.port, this.host, () => {
55
+ this.server.removeListener('error', reject);
56
+ const actualPort = this.getPort();
57
+ resolve({ port: actualPort, url: this.getUrl() });
58
+ });
59
+ });
60
+ }
61
+ async stop() {
62
+ if (this.unsubscribeEventStore) {
63
+ this.unsubscribeEventStore();
64
+ this.unsubscribeEventStore = undefined;
65
+ }
66
+ for (const client of this.activeClients) {
67
+ try {
68
+ client.end();
69
+ }
70
+ catch {
71
+ // ignore client close errors
72
+ }
73
+ }
74
+ this.activeClients.clear();
75
+ for (const socket of this.activeSockets) {
76
+ try {
77
+ socket.destroy();
78
+ }
79
+ catch {
80
+ // ignore socket destruction errors
81
+ }
82
+ }
83
+ this.activeSockets.clear();
84
+ if (this.server) {
85
+ const serverToClose = this.server;
86
+ this.server = null;
87
+ await new Promise((resolve, reject) => {
88
+ serverToClose.close((err) => {
89
+ if (err)
90
+ reject(err);
91
+ else
92
+ resolve();
93
+ });
94
+ });
95
+ }
96
+ }
97
+ setCorsHeaders(res) {
98
+ res.setHeader('Access-Control-Allow-Origin', '*');
99
+ res.setHeader('Access-Control-Allow-Private-Network', 'true');
100
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
101
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Last-Event-ID, Cache-Control');
102
+ }
103
+ handleRequest(req, res) {
104
+ this.setCorsHeaders(res);
105
+ if (req.method === 'OPTIONS') {
106
+ res.writeHead(204);
107
+ res.end();
108
+ return;
109
+ }
110
+ const hostHeader = req.headers.host || `${this.host}:${this.port}`;
111
+ const parsedUrl = new URL(req.url || '/', `http://${hostHeader}`);
112
+ const pathname = parsedUrl.pathname;
113
+ if (req.method === 'GET' && (pathname === '/health' || pathname === '/status')) {
114
+ this.handleHealth(res);
115
+ return;
116
+ }
117
+ if (req.method === 'GET' && pathname === '/state') {
118
+ this.handleState(res);
119
+ return;
120
+ }
121
+ if (req.method === 'GET' && pathname === '/events/history') {
122
+ this.handleEventsHistory(parsedUrl, res);
123
+ return;
124
+ }
125
+ if (req.method === 'GET' && pathname === '/events') {
126
+ this.handleSseEvents(req, parsedUrl, res);
127
+ return;
128
+ }
129
+ if (req.method === 'POST' && pathname === '/signal') {
130
+ this.handleSignal(req, res);
131
+ return;
132
+ }
133
+ res.writeHead(404, { 'Content-Type': 'application/json' });
134
+ res.end(JSON.stringify({ error: `Not found: ${pathname}` }));
135
+ }
136
+ handleHealth(res) {
137
+ const payload = {
138
+ status: 'ok',
139
+ skillName: this.skillName,
140
+ latestSeq: this.eventStore.getLatestSequence(),
141
+ uptimeSeconds: Math.floor((Date.now() - this.startTime) / 1000),
142
+ };
143
+ res.writeHead(200, { 'Content-Type': 'application/json' });
144
+ res.end(JSON.stringify(payload));
145
+ }
146
+ handleState(res) {
147
+ const latestSeq = this.eventStore.getLatestSequence();
148
+ const snapshot = this.eventStore.getLatestSnapshot();
149
+ const activeState = this.fsmEngine ? this.fsmEngine.getCurrentState() : snapshot?.state;
150
+ const context = this.fsmEngine ? this.fsmEngine.getContext() : snapshot?.context;
151
+ const payload = {
152
+ skillName: this.skillName,
153
+ latestSeq,
154
+ activeState,
155
+ context,
156
+ snapshot,
157
+ };
158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
159
+ res.end(JSON.stringify(payload));
160
+ }
161
+ handleEventsHistory(url, res) {
162
+ const sinceSeqParam = url.searchParams.get('sinceSeq');
163
+ const limitParam = url.searchParams.get('limit');
164
+ const sinceSeq = sinceSeqParam !== null ? parseInt(sinceSeqParam, 10) : undefined;
165
+ const limit = limitParam !== null ? parseInt(limitParam, 10) : undefined;
166
+ let events;
167
+ if (sinceSeq !== undefined && !isNaN(sinceSeq)) {
168
+ events = this.eventStore.getSince(sinceSeq);
169
+ }
170
+ else {
171
+ events = this.eventStore.getAll();
172
+ }
173
+ if (limit !== undefined && !isNaN(limit) && limit > 0) {
174
+ events = events.slice(-limit);
175
+ }
176
+ res.writeHead(200, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ count: events.length, events }));
178
+ }
179
+ handleSseEvents(req, url, res) {
180
+ res.writeHead(200, {
181
+ 'Content-Type': 'text/event-stream',
182
+ 'Cache-Control': 'no-cache, no-transform',
183
+ 'Connection': 'keep-alive',
184
+ 'X-Accel-Buffering': 'no',
185
+ });
186
+ this.activeClients.add(res);
187
+ // Initial greeting / connect event
188
+ res.write(`event: connected\ndata: ${JSON.stringify({ skillName: this.skillName, connectedAt: new Date().toISOString() })}\n\n`);
189
+ // Determine initial backlog sequence
190
+ const sinceSeqParam = url.searchParams.get('sinceSeq') || req.headers['last-event-id'];
191
+ if (sinceSeqParam) {
192
+ const sinceSeq = parseInt(sinceSeqParam, 10);
193
+ if (!isNaN(sinceSeq)) {
194
+ const backlog = this.eventStore.getSince(sinceSeq);
195
+ for (const event of backlog) {
196
+ this.sendSseEvent(res, event);
197
+ }
198
+ }
199
+ }
200
+ // Keepalive heartbeat
201
+ const heartbeatTimer = setInterval(() => {
202
+ try {
203
+ res.write(': heartbeat\n\n');
204
+ }
205
+ catch {
206
+ clearInterval(heartbeatTimer);
207
+ }
208
+ }, this.heartbeatIntervalMs);
209
+ req.on('close', () => {
210
+ clearInterval(heartbeatTimer);
211
+ this.activeClients.delete(res);
212
+ });
213
+ }
214
+ sendSseEvent(client, event) {
215
+ try {
216
+ client.write(`id: ${event.seq}\n`);
217
+ client.write(`event: signal_event\n`);
218
+ client.write(`data: ${JSON.stringify(event)}\n\n`);
219
+ }
220
+ catch {
221
+ this.activeClients.delete(client);
222
+ }
223
+ }
224
+ broadcastEvent(event) {
225
+ for (const client of this.activeClients) {
226
+ this.sendSseEvent(client, event);
227
+ }
228
+ }
229
+ handleSignal(req, res) {
230
+ let rawBody = '';
231
+ req.setEncoding('utf8');
232
+ req.on('data', (chunk) => {
233
+ rawBody += chunk;
234
+ if (rawBody.length > 1024 * 1024) {
235
+ // 1MB safety guard
236
+ res.writeHead(413, { 'Content-Type': 'application/json' });
237
+ res.end(JSON.stringify({ error: 'Payload too large' }));
238
+ req.destroy();
239
+ }
240
+ });
241
+ req.on('end', async () => {
242
+ try {
243
+ const parsed = JSON.parse(rawBody || '{}');
244
+ if (!parsed.signal) {
245
+ res.writeHead(400, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({ error: 'Missing required field: signal' }));
247
+ return;
248
+ }
249
+ if (this.fsmEngine) {
250
+ const transition = await this.fsmEngine.handleSignal(parsed.signal, parsed.payload || {});
251
+ const response = {
252
+ success: true,
253
+ transition,
254
+ };
255
+ res.writeHead(200, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify(response));
257
+ }
258
+ else {
259
+ // If no FSM engine attached, append signal directly to event store
260
+ const event = this.eventStore.append(parsed.signal, parsed.payload || {}, { source: 'telemetry_bridge' });
261
+ const response = {
262
+ success: true,
263
+ event: event,
264
+ };
265
+ res.writeHead(200, { 'Content-Type': 'application/json' });
266
+ res.end(JSON.stringify(response));
267
+ }
268
+ }
269
+ catch (err) {
270
+ res.writeHead(500, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify({ error: err.message || 'Signal dispatch failure' }));
272
+ }
273
+ });
274
+ }
275
+ }
@@ -0,0 +1,57 @@
1
+ import { EventStore } from '../core/event-store.js';
2
+ import { FSMEngine } from '../core/fsm-engine.js';
3
+ import { SignalEvent } from '../core/types.js';
4
+ export interface TelemetryServerOptions {
5
+ /**
6
+ * EventStore instance to observe
7
+ */
8
+ eventStore: EventStore;
9
+ /**
10
+ * Optional FSMEngine instance for executing state transitions on HITL signals
11
+ */
12
+ fsmEngine?: FSMEngine;
13
+ /**
14
+ * Port to listen on (0 for ephemeral port, default: 4242)
15
+ */
16
+ port?: number;
17
+ /**
18
+ * Host to bind to (default: '127.0.0.1')
19
+ */
20
+ host?: string;
21
+ /**
22
+ * Interval for SSE keep-alive heartbeats in ms (default: 15000)
23
+ */
24
+ heartbeatIntervalMs?: number;
25
+ /**
26
+ * Skill identifier for health/metadata reporting
27
+ */
28
+ skillName?: string;
29
+ }
30
+ export interface TelemetryHealthResponse {
31
+ status: 'ok';
32
+ skillName?: string;
33
+ latestSeq: number;
34
+ uptimeSeconds: number;
35
+ }
36
+ export interface TelemetryStateResponse {
37
+ skillName?: string;
38
+ latestSeq: number;
39
+ activeState?: string;
40
+ context?: Record<string, any>;
41
+ snapshot?: {
42
+ seq: number;
43
+ state: string;
44
+ context: Record<string, any>;
45
+ } | null;
46
+ }
47
+ export interface TelemetrySignalRequest {
48
+ signal: string;
49
+ payload?: Record<string, any>;
50
+ context?: Record<string, any>;
51
+ }
52
+ export interface TelemetrySignalResponse {
53
+ success: boolean;
54
+ event?: SignalEvent;
55
+ transition?: any;
56
+ error?: string;
57
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactive-skills/runtime",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
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",