@reactive-skills/runtime 0.2.0 → 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.
- package/dist/core/event-store.d.ts +8 -1
- package/dist/core/event-store.js +97 -6
- package/dist/core/fsm-engine.d.ts +9 -0
- package/dist/core/fsm-engine.js +52 -2
- package/dist/core/job-manager.d.ts +50 -0
- package/dist/core/job-manager.js +213 -0
- package/dist/core/projection-engine.d.ts +4 -1
- package/dist/core/projection-engine.js +29 -7
- package/dist/core/types.d.ts +34 -0
- package/dist/core/types.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/server.js +69 -20
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ export interface EventStoreOptions {
|
|
|
7
7
|
sqlitePath?: string;
|
|
8
8
|
workspaceDir?: string;
|
|
9
9
|
skillId?: string;
|
|
10
|
+
jobId?: string;
|
|
10
11
|
runId?: string;
|
|
11
12
|
run_id?: string;
|
|
12
13
|
correlationId?: string;
|
|
@@ -23,6 +24,7 @@ export interface EventStoreOptions {
|
|
|
23
24
|
enableSqlite?: boolean;
|
|
24
25
|
maxInMemoryEvents?: number;
|
|
25
26
|
maxJsonlBytes?: number;
|
|
27
|
+
acquireLock?: boolean;
|
|
26
28
|
}
|
|
27
29
|
export interface EventQueryOptions {
|
|
28
30
|
type?: string;
|
|
@@ -36,6 +38,7 @@ export interface EventQueryOptions {
|
|
|
36
38
|
*/
|
|
37
39
|
export declare class SQLiteStorageDriver {
|
|
38
40
|
private db;
|
|
41
|
+
private isClosed;
|
|
39
42
|
constructor(dbPath: string);
|
|
40
43
|
private initTables;
|
|
41
44
|
private ensureSchemaVersion;
|
|
@@ -93,7 +96,11 @@ export declare class EventStore {
|
|
|
93
96
|
private projectionWatermarks;
|
|
94
97
|
private latestSnapshot;
|
|
95
98
|
private maxJsonlBytes;
|
|
99
|
+
private lockFd;
|
|
100
|
+
private lockPath;
|
|
96
101
|
constructor(options?: EventStoreOptions);
|
|
102
|
+
private acquireJobLock;
|
|
103
|
+
private releaseJobLock;
|
|
97
104
|
private initializeStorage;
|
|
98
105
|
/**
|
|
99
106
|
* Scan all JSONL files for the authoritative max seq and event count.
|
|
@@ -168,7 +175,7 @@ export declare class EventStore {
|
|
|
168
175
|
*/
|
|
169
176
|
query(filter?: EventQueryOptions): SignalEvent[];
|
|
170
177
|
/**
|
|
171
|
-
* Close storage drivers and release file handles
|
|
178
|
+
* Close storage drivers and release file handles and locks
|
|
172
179
|
*/
|
|
173
180
|
close(): void;
|
|
174
181
|
/**
|
package/dist/core/event-store.js
CHANGED
|
@@ -20,6 +20,7 @@ export function createSortableId() {
|
|
|
20
20
|
*/
|
|
21
21
|
export class SQLiteStorageDriver {
|
|
22
22
|
db;
|
|
23
|
+
isClosed = false;
|
|
23
24
|
constructor(dbPath) {
|
|
24
25
|
if (dbPath !== ':memory:') {
|
|
25
26
|
const dir = path.dirname(dbPath);
|
|
@@ -237,7 +238,17 @@ export class SQLiteStorageDriver {
|
|
|
237
238
|
return row ? row.content : null;
|
|
238
239
|
}
|
|
239
240
|
close() {
|
|
240
|
-
this.
|
|
241
|
+
if (this.isClosed)
|
|
242
|
+
return;
|
|
243
|
+
try {
|
|
244
|
+
this.db.close();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// already closed
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
this.isClosed = true;
|
|
251
|
+
}
|
|
241
252
|
}
|
|
242
253
|
static getSchemaVersion(dbPath) {
|
|
243
254
|
const db = new DatabaseSync(dbPath);
|
|
@@ -334,12 +345,15 @@ export class EventStore {
|
|
|
334
345
|
projectionWatermarks = new Map();
|
|
335
346
|
latestSnapshot = null;
|
|
336
347
|
maxJsonlBytes;
|
|
348
|
+
lockFd = null;
|
|
349
|
+
lockPath = null;
|
|
337
350
|
constructor(options = {}) {
|
|
338
351
|
this.maxInMemoryEvents = options.maxInMemoryEvents || 1000;
|
|
339
352
|
this.maxJsonlBytes = options.maxJsonlBytes || 10 * 1024 * 1024;
|
|
353
|
+
const effectiveJobId = options.jobId || options.runId || options.run_id;
|
|
340
354
|
this.eventContext = {
|
|
341
355
|
skill_id: options.skillId,
|
|
342
|
-
run_id:
|
|
356
|
+
run_id: effectiveJobId || createSortableId(),
|
|
343
357
|
correlation_id: options.correlationId || createSortableId(),
|
|
344
358
|
request_id: options.requestId,
|
|
345
359
|
trace_parent: options.traceParent,
|
|
@@ -351,9 +365,21 @@ export class EventStore {
|
|
|
351
365
|
const scopeDir = options.skillId
|
|
352
366
|
? path.join(workspaceDir, '.reactive', 'skills', options.skillId)
|
|
353
367
|
: path.join(workspaceDir, '.reactive');
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
let runScopedDir = scopeDir;
|
|
369
|
+
if (options.skillId && effectiveJobId) {
|
|
370
|
+
const isLegacyFallback = effectiveJobId === 'default' &&
|
|
371
|
+
!fs.existsSync(path.join(scopeDir, 'jobs', effectiveJobId)) &&
|
|
372
|
+
fs.existsSync(path.join(scopeDir, 'events.jsonl'));
|
|
373
|
+
if (!isLegacyFallback) {
|
|
374
|
+
runScopedDir = path.join(scopeDir, 'jobs', effectiveJobId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (effectiveJobId) {
|
|
378
|
+
runScopedDir = path.join(scopeDir, effectiveJobId);
|
|
379
|
+
}
|
|
380
|
+
if (options.acquireLock && runScopedDir) {
|
|
381
|
+
this.acquireJobLock(runScopedDir);
|
|
382
|
+
}
|
|
357
383
|
this.storagePath = options.storagePath || path.join(runScopedDir, 'events.jsonl');
|
|
358
384
|
if (options.enableSqlite || options.sqlitePath) {
|
|
359
385
|
const dbPath = options.sqlitePath || path.join(runScopedDir, 'events.db');
|
|
@@ -366,6 +392,69 @@ export class EventStore {
|
|
|
366
392
|
this.sqliteDriver = new SQLiteStorageDriver(':memory:');
|
|
367
393
|
}
|
|
368
394
|
}
|
|
395
|
+
acquireJobLock(dir) {
|
|
396
|
+
if (!fs.existsSync(dir)) {
|
|
397
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
398
|
+
}
|
|
399
|
+
this.lockPath = path.join(dir, '.lock');
|
|
400
|
+
try {
|
|
401
|
+
this.lockFd = fs.openSync(this.lockPath, 'wx');
|
|
402
|
+
fs.writeSync(this.lockFd, JSON.stringify({ pid: process.pid, time: new Date().toISOString() }));
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
if (err.code === 'EEXIST') {
|
|
406
|
+
try {
|
|
407
|
+
const content = fs.readFileSync(this.lockPath, 'utf8');
|
|
408
|
+
const data = JSON.parse(content);
|
|
409
|
+
if (data.pid) {
|
|
410
|
+
try {
|
|
411
|
+
process.kill(data.pid, 0);
|
|
412
|
+
const lockError = new Error(`JOB_LOCKED: Job directory is currently locked by PID ${data.pid}`);
|
|
413
|
+
lockError.code = 'JOB_LOCKED';
|
|
414
|
+
throw lockError;
|
|
415
|
+
}
|
|
416
|
+
catch (killErr) {
|
|
417
|
+
if (killErr.code === 'ESRCH') {
|
|
418
|
+
fs.unlinkSync(this.lockPath);
|
|
419
|
+
this.lockFd = fs.openSync(this.lockPath, 'wx');
|
|
420
|
+
fs.writeSync(this.lockFd, JSON.stringify({ pid: process.pid, time: new Date().toISOString() }));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
throw killErr;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (readErr) {
|
|
428
|
+
if (readErr.code === 'JOB_LOCKED')
|
|
429
|
+
throw readErr;
|
|
430
|
+
}
|
|
431
|
+
const lockError = new Error(`JOB_LOCKED: Job directory is currently locked`);
|
|
432
|
+
lockError.code = 'JOB_LOCKED';
|
|
433
|
+
throw lockError;
|
|
434
|
+
}
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
releaseJobLock() {
|
|
439
|
+
if (this.lockFd !== null) {
|
|
440
|
+
try {
|
|
441
|
+
fs.closeSync(this.lockFd);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// ignore
|
|
445
|
+
}
|
|
446
|
+
this.lockFd = null;
|
|
447
|
+
}
|
|
448
|
+
if (this.lockPath && fs.existsSync(this.lockPath)) {
|
|
449
|
+
try {
|
|
450
|
+
fs.unlinkSync(this.lockPath);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// ignore
|
|
454
|
+
}
|
|
455
|
+
this.lockPath = null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
369
458
|
initializeStorage() {
|
|
370
459
|
// SQLite is authoritative. If it already has events, load the recent
|
|
371
460
|
// sliding window and check JSONL for divergence before returning.
|
|
@@ -727,9 +816,10 @@ export class EventStore {
|
|
|
727
816
|
return filter.limit ? filtered.slice(0, filter.limit) : filtered;
|
|
728
817
|
}
|
|
729
818
|
/**
|
|
730
|
-
* Close storage drivers and release file handles
|
|
819
|
+
* Close storage drivers and release file handles and locks
|
|
731
820
|
*/
|
|
732
821
|
close() {
|
|
822
|
+
this.releaseJobLock();
|
|
733
823
|
if (this.sqliteDriver) {
|
|
734
824
|
this.sqliteDriver.close();
|
|
735
825
|
}
|
|
@@ -748,6 +838,7 @@ export class EventStore {
|
|
|
748
838
|
this.seqCounter = 0;
|
|
749
839
|
this.projectionWatermarks.clear();
|
|
750
840
|
this.latestSnapshot = null;
|
|
841
|
+
this.releaseJobLock();
|
|
751
842
|
if (this.sqliteDriver) {
|
|
752
843
|
this.sqliteDriver.clear();
|
|
753
844
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { SkillManifest, StateDefinition, SignalEvent, PromptSlice, EventContext, ChildRunSummary, DecisionRecord } from './types.js';
|
|
2
2
|
import { EventStore } from './event-store.js';
|
|
3
|
+
import { JobManager } from './job-manager.js';
|
|
3
4
|
export interface FSMEngineOptions {
|
|
4
5
|
skillDir: string;
|
|
5
6
|
workspaceDir?: string;
|
|
6
7
|
eventStore?: EventStore;
|
|
7
8
|
eventContext?: EventContext;
|
|
9
|
+
jobId?: string;
|
|
8
10
|
runId?: string;
|
|
9
11
|
initialContext?: Record<string, any>;
|
|
10
12
|
autoRehydrate?: boolean;
|
|
@@ -23,6 +25,9 @@ export declare class FSMEngine {
|
|
|
23
25
|
private turnsSinceLastSignal;
|
|
24
26
|
private inBypassState;
|
|
25
27
|
private readonly strictExecution;
|
|
28
|
+
private jobId?;
|
|
29
|
+
private isActiveJob;
|
|
30
|
+
private jobManager;
|
|
26
31
|
constructor(options: FSMEngineOptions);
|
|
27
32
|
/**
|
|
28
33
|
* Rehydrate state machine state and context from immutable event history,
|
|
@@ -92,4 +97,8 @@ export declare class FSMEngine {
|
|
|
92
97
|
private enterPath;
|
|
93
98
|
private executeEntryHook;
|
|
94
99
|
private executeExitHook;
|
|
100
|
+
getJobId(): string | undefined;
|
|
101
|
+
isJobActive(): boolean;
|
|
102
|
+
getJobManager(): JobManager;
|
|
103
|
+
close(): void;
|
|
95
104
|
}
|
package/dist/core/fsm-engine.js
CHANGED
|
@@ -7,6 +7,7 @@ import { EventStore, createSortableId } from './event-store.js';
|
|
|
7
7
|
import { GuardEvaluator } from './guard-evaluator.js';
|
|
8
8
|
import { LegacySkillAdapter } from './legacy-adapter.js';
|
|
9
9
|
import { ProjectionEngine } from './projection-engine.js';
|
|
10
|
+
import { JobManager } from './job-manager.js';
|
|
10
11
|
/** Maximum lifecycle-signal drain steps per top-level handleSignal call.
|
|
11
12
|
* Prevents cyclic on_enter/on_exit emissions from overflowing the call stack (REL-02). */
|
|
12
13
|
const MAX_QUEUE_DRAIN_DEPTH = 50;
|
|
@@ -24,6 +25,9 @@ export class FSMEngine {
|
|
|
24
25
|
turnsSinceLastSignal;
|
|
25
26
|
inBypassState;
|
|
26
27
|
strictExecution;
|
|
28
|
+
jobId;
|
|
29
|
+
isActiveJob;
|
|
30
|
+
jobManager;
|
|
27
31
|
constructor(options) {
|
|
28
32
|
this.skillDir = path.resolve(options.skillDir);
|
|
29
33
|
this.workspaceDir = options.workspaceDir || process.cwd();
|
|
@@ -31,10 +35,29 @@ export class FSMEngine {
|
|
|
31
35
|
this.strictExecution = this.manifest.strict_execution === true;
|
|
32
36
|
this.turnsSinceLastSignal = 0;
|
|
33
37
|
this.inBypassState = false;
|
|
38
|
+
const effectiveJobId = options.jobId || options.runId;
|
|
39
|
+
this.jobManager = new JobManager(this.workspaceDir);
|
|
40
|
+
const activeJobId = this.jobManager.getActiveJobId(this.manifest.name);
|
|
41
|
+
const resolvedJobId = effectiveJobId || activeJobId;
|
|
42
|
+
const isActiveJob = (resolvedJobId === activeJobId);
|
|
43
|
+
this.jobId = resolvedJobId;
|
|
44
|
+
this.isActiveJob = isActiveJob;
|
|
45
|
+
if (this.jobId) {
|
|
46
|
+
const existingJob = this.jobManager.getJob(this.manifest.name, this.jobId);
|
|
47
|
+
if (!existingJob) {
|
|
48
|
+
this.jobManager.createJob(this.manifest.name, {
|
|
49
|
+
id: this.jobId,
|
|
50
|
+
name: this.jobId,
|
|
51
|
+
initialState: this.manifest.initial_state,
|
|
52
|
+
setActive: this.isActiveJob,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
34
56
|
this.eventStore = options.eventStore || new EventStore({
|
|
35
57
|
skillId: this.manifest.name,
|
|
36
58
|
workspaceDir: this.workspaceDir,
|
|
37
|
-
|
|
59
|
+
jobId: this.jobId,
|
|
60
|
+
runId: this.jobId,
|
|
38
61
|
enableSqlite: true,
|
|
39
62
|
...options.eventContext,
|
|
40
63
|
});
|
|
@@ -42,7 +65,7 @@ export class FSMEngine {
|
|
|
42
65
|
...(this.manifest.default_context || {}),
|
|
43
66
|
...(options.initialContext || {}),
|
|
44
67
|
};
|
|
45
|
-
this.projectionEngine = new ProjectionEngine(this.skillDir, this.manifest.deliverable_projections || [], options.workspaceDir || process.cwd());
|
|
68
|
+
this.projectionEngine = new ProjectionEngine(this.skillDir, this.manifest.deliverable_projections || [], options.workspaceDir || process.cwd(), this.jobId, this.isActiveJob);
|
|
46
69
|
const autoRehydrate = options.autoRehydrate !== false;
|
|
47
70
|
const latestSnapshot = autoRehydrate ? this.eventStore.getLatestSnapshot() : null;
|
|
48
71
|
const history = autoRehydrate
|
|
@@ -50,6 +73,11 @@ export class FSMEngine {
|
|
|
50
73
|
: [];
|
|
51
74
|
if (autoRehydrate && (latestSnapshot || history.length > 0)) {
|
|
52
75
|
this.rehydrate(history, latestSnapshot);
|
|
76
|
+
if (this.jobId) {
|
|
77
|
+
this.jobManager.updateJob(this.manifest.name, this.jobId, {
|
|
78
|
+
currentState: this.getCurrentState(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
53
81
|
}
|
|
54
82
|
else {
|
|
55
83
|
const initialPath = this.resolveInitialPath([this.manifest.initial_state]);
|
|
@@ -62,6 +90,11 @@ export class FSMEngine {
|
|
|
62
90
|
}, { state: initialPath.join('.') });
|
|
63
91
|
this.enterPath(initialPath, [], 'INITIAL_BOOT');
|
|
64
92
|
this.eventStore.saveSnapshot(this.eventStore.getLatestSequence(), this.getCurrentState(), this.context);
|
|
93
|
+
if (this.jobId) {
|
|
94
|
+
this.jobManager.updateJob(this.manifest.name, this.jobId, {
|
|
95
|
+
currentState: this.getCurrentState(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
99
|
}
|
|
67
100
|
/**
|
|
@@ -531,6 +564,11 @@ export class FSMEngine {
|
|
|
531
564
|
this.signalQueueDepth = 0;
|
|
532
565
|
}
|
|
533
566
|
}
|
|
567
|
+
if (this.jobId) {
|
|
568
|
+
this.jobManager.updateJob(this.manifest.name, this.jobId, {
|
|
569
|
+
currentState: this.getCurrentState(),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
534
572
|
return {
|
|
535
573
|
transitioned: true,
|
|
536
574
|
previousState,
|
|
@@ -648,4 +686,16 @@ export class FSMEngine {
|
|
|
648
686
|
}
|
|
649
687
|
}
|
|
650
688
|
}
|
|
689
|
+
getJobId() {
|
|
690
|
+
return this.jobId;
|
|
691
|
+
}
|
|
692
|
+
isJobActive() {
|
|
693
|
+
return this.isActiveJob;
|
|
694
|
+
}
|
|
695
|
+
getJobManager() {
|
|
696
|
+
return this.jobManager;
|
|
697
|
+
}
|
|
698
|
+
close() {
|
|
699
|
+
this.eventStore.close();
|
|
700
|
+
}
|
|
651
701
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { JobMetadata } from './types.js';
|
|
2
|
+
export declare const DEFAULT_JOB_ID = "default";
|
|
3
|
+
/**
|
|
4
|
+
* Converts arbitrary human strings (e.g. 'Auth Slice v1.0') into safe filesystem slugs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeJobSlug(name: string): string;
|
|
7
|
+
export interface CreateJobOptions {
|
|
8
|
+
id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
initialState?: string;
|
|
11
|
+
parentRunId?: string;
|
|
12
|
+
setActive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* JobManager handles execution run isolation, active job pointer resolution,
|
|
16
|
+
* and job metadata persistence.
|
|
17
|
+
*/
|
|
18
|
+
export declare class JobManager {
|
|
19
|
+
private workspaceDir;
|
|
20
|
+
constructor(workspaceDir?: string);
|
|
21
|
+
getSkillDir(skillId: string): string;
|
|
22
|
+
getActivePointerPath(skillId: string): string;
|
|
23
|
+
getJobsDir(skillId: string): string;
|
|
24
|
+
getJobDir(skillId: string, jobId: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the current active job ID for a skill.
|
|
27
|
+
* Falls back to DEFAULT_JOB_ID ('default') if no pointer exists.
|
|
28
|
+
*/
|
|
29
|
+
getActiveJobId(skillId: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Sets the active job pointer for a skill.
|
|
32
|
+
*/
|
|
33
|
+
setActiveJobId(skillId: string, jobId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new isolated job and persists its job.json metadata.
|
|
36
|
+
*/
|
|
37
|
+
createJob(skillId: string, options?: CreateJobOptions): JobMetadata;
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves a job's metadata by ID.
|
|
40
|
+
*/
|
|
41
|
+
getJob(skillId: string, jobId: string): JobMetadata | null;
|
|
42
|
+
/**
|
|
43
|
+
* Updates a job's metadata (e.g. status, currentState, completedAt).
|
|
44
|
+
*/
|
|
45
|
+
updateJob(skillId: string, jobId: string, updates: Partial<JobMetadata>): JobMetadata;
|
|
46
|
+
/**
|
|
47
|
+
* Lists all jobs for a skill.
|
|
48
|
+
*/
|
|
49
|
+
listJobs(skillId: string): JobMetadata[];
|
|
50
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createSortableId } from './event-store.js';
|
|
4
|
+
import { JobMetadataSchema } from './types.js';
|
|
5
|
+
export const DEFAULT_JOB_ID = 'default';
|
|
6
|
+
/**
|
|
7
|
+
* Converts arbitrary human strings (e.g. 'Auth Slice v1.0') into safe filesystem slugs.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeJobSlug(name) {
|
|
10
|
+
const cleaned = name
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '');
|
|
15
|
+
if (!cleaned) {
|
|
16
|
+
return `job-${createSortableId().slice(0, 8)}`;
|
|
17
|
+
}
|
|
18
|
+
return cleaned;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* JobManager handles execution run isolation, active job pointer resolution,
|
|
22
|
+
* and job metadata persistence.
|
|
23
|
+
*/
|
|
24
|
+
export class JobManager {
|
|
25
|
+
workspaceDir;
|
|
26
|
+
constructor(workspaceDir = process.cwd()) {
|
|
27
|
+
this.workspaceDir = path.resolve(workspaceDir);
|
|
28
|
+
}
|
|
29
|
+
getSkillDir(skillId) {
|
|
30
|
+
return path.join(this.workspaceDir, '.reactive', 'skills', skillId);
|
|
31
|
+
}
|
|
32
|
+
getActivePointerPath(skillId) {
|
|
33
|
+
return path.join(this.getSkillDir(skillId), 'active_job');
|
|
34
|
+
}
|
|
35
|
+
getJobsDir(skillId) {
|
|
36
|
+
return path.join(this.getSkillDir(skillId), 'jobs');
|
|
37
|
+
}
|
|
38
|
+
getJobDir(skillId, jobId) {
|
|
39
|
+
return path.join(this.getJobsDir(skillId), jobId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the current active job ID for a skill.
|
|
43
|
+
* Falls back to DEFAULT_JOB_ID ('default') if no pointer exists.
|
|
44
|
+
*/
|
|
45
|
+
getActiveJobId(skillId) {
|
|
46
|
+
const pointerFile = this.getActivePointerPath(skillId);
|
|
47
|
+
if (fs.existsSync(pointerFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const id = fs.readFileSync(pointerFile, 'utf8').trim();
|
|
50
|
+
if (id)
|
|
51
|
+
return id;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Fall back to default
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return DEFAULT_JOB_ID;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Sets the active job pointer for a skill.
|
|
61
|
+
*/
|
|
62
|
+
setActiveJobId(skillId, jobId) {
|
|
63
|
+
const targetDir = this.getSkillDir(skillId);
|
|
64
|
+
if (!fs.existsSync(targetDir)) {
|
|
65
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
const pointerFile = this.getActivePointerPath(skillId);
|
|
68
|
+
fs.writeFileSync(pointerFile, jobId.trim(), 'utf8');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates a new isolated job and persists its job.json metadata.
|
|
72
|
+
*/
|
|
73
|
+
createJob(skillId, options = {}) {
|
|
74
|
+
const id = options.id || createSortableId();
|
|
75
|
+
const name = options.name ? normalizeJobSlug(options.name) : (options.id ? normalizeJobSlug(options.id) : DEFAULT_JOB_ID);
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const metadata = {
|
|
78
|
+
id,
|
|
79
|
+
name,
|
|
80
|
+
skillId,
|
|
81
|
+
status: 'active',
|
|
82
|
+
currentState: options.initialState || 'INIT',
|
|
83
|
+
parentRunId: options.parentRunId,
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
};
|
|
87
|
+
const jobDir = this.getJobDir(skillId, id);
|
|
88
|
+
if (!fs.existsSync(jobDir)) {
|
|
89
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
const metadataPath = path.join(jobDir, 'job.json');
|
|
92
|
+
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
|
|
93
|
+
if (options.setActive) {
|
|
94
|
+
this.setActiveJobId(skillId, id);
|
|
95
|
+
}
|
|
96
|
+
return metadata;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Retrieves a job's metadata by ID.
|
|
100
|
+
*/
|
|
101
|
+
getJob(skillId, jobId) {
|
|
102
|
+
const jobDir = this.getJobDir(skillId, jobId);
|
|
103
|
+
const metadataPath = path.join(jobDir, 'job.json');
|
|
104
|
+
if (fs.existsSync(metadataPath)) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
107
|
+
return JobMetadataSchema.parse(raw);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Fallback: if jobDir exists (e.g. created by EventStore), synthesize metadata
|
|
114
|
+
if (fs.existsSync(jobDir)) {
|
|
115
|
+
const stat = fs.statSync(jobDir);
|
|
116
|
+
return {
|
|
117
|
+
id: jobId,
|
|
118
|
+
name: normalizeJobSlug(jobId),
|
|
119
|
+
skillId,
|
|
120
|
+
status: 'active',
|
|
121
|
+
currentState: 'INIT',
|
|
122
|
+
createdAt: stat.birthtime && !isNaN(stat.birthtime.getTime()) ? stat.birthtime.toISOString() : new Date().toISOString(),
|
|
123
|
+
updatedAt: stat.mtime && !isNaN(stat.mtime.getTime()) ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Check if it's the active job pointer
|
|
127
|
+
if (jobId === this.getActiveJobId(skillId)) {
|
|
128
|
+
const pointerFile = this.getActivePointerPath(skillId);
|
|
129
|
+
if (fs.existsSync(pointerFile)) {
|
|
130
|
+
const stat = fs.statSync(pointerFile);
|
|
131
|
+
return {
|
|
132
|
+
id: jobId,
|
|
133
|
+
name: normalizeJobSlug(jobId),
|
|
134
|
+
skillId,
|
|
135
|
+
status: 'active',
|
|
136
|
+
currentState: 'INIT',
|
|
137
|
+
createdAt: stat.birthtime && !isNaN(stat.birthtime.getTime()) ? stat.birthtime.toISOString() : new Date().toISOString(),
|
|
138
|
+
updatedAt: stat.mtime && !isNaN(stat.mtime.getTime()) ? stat.mtime.toISOString() : new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Legacy fallback: if requesting 'default' and legacy events.jsonl exists at skill root
|
|
143
|
+
if (jobId === DEFAULT_JOB_ID) {
|
|
144
|
+
const legacyEventStore = path.join(this.getSkillDir(skillId), 'events.jsonl');
|
|
145
|
+
if (fs.existsSync(legacyEventStore)) {
|
|
146
|
+
return {
|
|
147
|
+
id: DEFAULT_JOB_ID,
|
|
148
|
+
name: DEFAULT_JOB_ID,
|
|
149
|
+
skillId,
|
|
150
|
+
status: 'active',
|
|
151
|
+
currentState: 'ACTIVE',
|
|
152
|
+
createdAt: new Date().toISOString(),
|
|
153
|
+
updatedAt: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Updates a job's metadata (e.g. status, currentState, completedAt).
|
|
161
|
+
*/
|
|
162
|
+
updateJob(skillId, jobId, updates) {
|
|
163
|
+
const existing = this.getJob(skillId, jobId) || this.createJob(skillId, { id: jobId, name: jobId });
|
|
164
|
+
const updated = {
|
|
165
|
+
...existing,
|
|
166
|
+
...updates,
|
|
167
|
+
updatedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
const validated = JobMetadataSchema.parse(updated);
|
|
170
|
+
const jobDir = this.getJobDir(skillId, jobId);
|
|
171
|
+
if (!fs.existsSync(jobDir)) {
|
|
172
|
+
fs.mkdirSync(jobDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
const metadataPath = path.join(jobDir, 'job.json');
|
|
175
|
+
fs.writeFileSync(metadataPath, JSON.stringify(validated, null, 2), 'utf8');
|
|
176
|
+
return validated;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Lists all jobs for a skill.
|
|
180
|
+
*/
|
|
181
|
+
listJobs(skillId) {
|
|
182
|
+
const jobs = [];
|
|
183
|
+
const jobsDir = this.getJobsDir(skillId);
|
|
184
|
+
if (fs.existsSync(jobsDir)) {
|
|
185
|
+
const entries = fs.readdirSync(jobsDir, { withFileTypes: true });
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
const job = this.getJob(skillId, entry.name);
|
|
189
|
+
if (job) {
|
|
190
|
+
jobs.push(job);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const activeJobId = this.getActiveJobId(skillId);
|
|
196
|
+
if (!jobs.some(j => j.id === activeJobId)) {
|
|
197
|
+
const activeJob = this.getJob(skillId, activeJobId);
|
|
198
|
+
if (activeJob) {
|
|
199
|
+
jobs.push(activeJob);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Check for legacy root event store if no jobs found
|
|
203
|
+
if (jobs.length === 0) {
|
|
204
|
+
const legacyEventStore = path.join(this.getSkillDir(skillId), 'events.jsonl');
|
|
205
|
+
if (fs.existsSync(legacyEventStore)) {
|
|
206
|
+
const defaultJob = this.getJob(skillId, DEFAULT_JOB_ID);
|
|
207
|
+
if (defaultJob)
|
|
208
|
+
jobs.push(defaultJob);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return jobs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -30,7 +30,10 @@ export declare class ProjectionEngine {
|
|
|
30
30
|
private projections;
|
|
31
31
|
private compiledTemplates;
|
|
32
32
|
private eventCaches;
|
|
33
|
-
|
|
33
|
+
private jobId?;
|
|
34
|
+
private isActiveJob;
|
|
35
|
+
constructor(skillDir: string, projections?: DeliverableProjection[], workspaceDir?: string, jobId?: string, isActiveJob?: boolean);
|
|
36
|
+
setJob(jobId?: string, isActiveJob?: boolean): void;
|
|
34
37
|
private resolveOutputPath;
|
|
35
38
|
private getProjectionEvents;
|
|
36
39
|
private matchesTrigger;
|
|
@@ -11,13 +11,21 @@ export class ProjectionEngine {
|
|
|
11
11
|
projections;
|
|
12
12
|
compiledTemplates = new Map();
|
|
13
13
|
eventCaches = new WeakMap();
|
|
14
|
-
|
|
14
|
+
jobId;
|
|
15
|
+
isActiveJob;
|
|
16
|
+
constructor(skillDir, projections = [], workspaceDir = process.cwd(), jobId, isActiveJob = true) {
|
|
15
17
|
this.skillDir = skillDir;
|
|
16
18
|
this.workspaceDir = path.resolve(workspaceDir);
|
|
17
19
|
this.projections = projections;
|
|
20
|
+
this.jobId = jobId;
|
|
21
|
+
this.isActiveJob = isActiveJob;
|
|
18
22
|
this.registerHelpers();
|
|
19
23
|
this.compileTemplates();
|
|
20
24
|
}
|
|
25
|
+
setJob(jobId, isActiveJob = true) {
|
|
26
|
+
this.jobId = jobId;
|
|
27
|
+
this.isActiveJob = isActiveJob;
|
|
28
|
+
}
|
|
21
29
|
resolveOutputPath(output) {
|
|
22
30
|
if (path.isAbsolute(output)) {
|
|
23
31
|
throw new Error('Projection output must be relative to the workspace.');
|
|
@@ -138,13 +146,27 @@ export class ProjectionEngine {
|
|
|
138
146
|
}
|
|
139
147
|
if (templateFn) {
|
|
140
148
|
const outputContent = templateFn(projContext);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
const canonicalOutputPath = this.resolveOutputPath(proj.output);
|
|
150
|
+
const canonicalDir = path.dirname(canonicalOutputPath);
|
|
151
|
+
const fileName = path.basename(canonicalOutputPath);
|
|
152
|
+
// 1. If jobId is present, write to historical job archive:
|
|
153
|
+
if (this.jobId) {
|
|
154
|
+
const archiveDir = path.join(canonicalDir, 'jobs', this.jobId);
|
|
155
|
+
if (!fs.existsSync(archiveDir)) {
|
|
156
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
const archivePath = path.join(archiveDir, fileName);
|
|
159
|
+
fs.writeFileSync(archivePath, outputContent, 'utf8');
|
|
160
|
+
writtenFiles.push(archivePath);
|
|
161
|
+
}
|
|
162
|
+
// 2. If active job or no jobId (legacy mode), mirror to canonical root:
|
|
163
|
+
if (this.isActiveJob || !this.jobId) {
|
|
164
|
+
if (!fs.existsSync(canonicalDir)) {
|
|
165
|
+
fs.mkdirSync(canonicalDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
fs.writeFileSync(canonicalOutputPath, outputContent, 'utf8');
|
|
168
|
+
writtenFiles.push(canonicalOutputPath);
|
|
145
169
|
}
|
|
146
|
-
fs.writeFileSync(outputPath, outputContent, 'utf8');
|
|
147
|
-
writtenFiles.push(outputPath);
|
|
148
170
|
eventStore.saveProjectionWatermark(proj.template, eventStore.getLatestSequence(), `${this.skillDir}:${proj.template}`);
|
|
149
171
|
}
|
|
150
172
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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>;
|
package/dist/core/types.js
CHANGED
|
@@ -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,6 +6,7 @@ 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';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ 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';
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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(
|
|
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({
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactive-skills/runtime",
|
|
3
|
-
"version": "0.
|
|
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",
|