agent-state-machine 1.0.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.
@@ -0,0 +1,387 @@
1
+ /**
2
+ * File: /lib/runtime/runtime.js
3
+ */
4
+
5
+ /**
6
+ * WorkflowRuntime - Native JavaScript workflow execution engine
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import readline from 'readline';
12
+ import { createMemoryProxy } from './memory.js';
13
+
14
+ // Global runtime reference for agent() and memory access
15
+ let currentRuntime = null;
16
+
17
+ export function getCurrentRuntime() {
18
+ return currentRuntime;
19
+ }
20
+
21
+ export function setCurrentRuntime(runtime) {
22
+ currentRuntime = runtime;
23
+ }
24
+
25
+ export function clearCurrentRuntime() {
26
+ currentRuntime = null;
27
+ }
28
+
29
+ /**
30
+ * WorkflowRuntime class - manages native JS workflow execution
31
+ */
32
+ export class WorkflowRuntime {
33
+ constructor(workflowDir) {
34
+ this.workflowDir = workflowDir;
35
+ this.workflowName = path.basename(workflowDir);
36
+ this.stateDir = path.join(workflowDir, 'state');
37
+ this.agentsDir = path.join(workflowDir, 'agents');
38
+ this.interactionsDir = path.join(workflowDir, 'interactions');
39
+ this.steeringDir = path.join(workflowDir, 'steering');
40
+ this.generatedPromptFile = path.join(this.stateDir, 'generated-prompt.md');
41
+ this.historyFile = path.join(this.stateDir, 'history.jsonl');
42
+ this.stateFile = path.join(this.stateDir, 'current.json');
43
+
44
+ // Ensure directories exist
45
+ this.ensureDirectories();
46
+
47
+ // Load persisted state
48
+ const savedState = this.loadState();
49
+ this._rawMemory = savedState.memory || {};
50
+ this._error = savedState._error || null;
51
+ this.status = savedState.status || 'IDLE';
52
+ this.startedAt = savedState.startedAt || null;
53
+
54
+ // Create memory proxy for auto-persistence
55
+ this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
56
+
57
+ // Native workflow config (populated when workflow module is loaded)
58
+ this.workflowConfig = {
59
+ models: {},
60
+ apiKeys: {},
61
+ description: ''
62
+ };
63
+
64
+ // Load steering
65
+ this.steering = this.loadSteering();
66
+ }
67
+
68
+ ensureDirectories() {
69
+ const dirs = [this.stateDir, this.interactionsDir];
70
+ for (const dir of dirs) {
71
+ if (!fs.existsSync(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load state from current.json
79
+ */
80
+ loadState() {
81
+ if (fs.existsSync(this.stateFile)) {
82
+ try {
83
+ return JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
84
+ } catch (err) {
85
+ console.warn(`Warning: Failed to load state: ${err.message}`);
86
+ }
87
+ }
88
+ return {};
89
+ }
90
+
91
+ /**
92
+ * Load steering configuration and global prompt
93
+ */
94
+ loadSteering() {
95
+ const configPath = path.join(this.steeringDir, 'config.json');
96
+ const globalPath = path.join(this.steeringDir, 'global.md');
97
+
98
+ const steering = {
99
+ enabled: true,
100
+ global: ''
101
+ };
102
+
103
+ if (fs.existsSync(configPath)) {
104
+ try {
105
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
106
+ steering.enabled = config.enabled !== false;
107
+ } catch (err) {
108
+ console.warn(`Warning: Failed to load steering config: ${err.message}`);
109
+ }
110
+ }
111
+
112
+ if (fs.existsSync(globalPath)) {
113
+ steering.global = fs.readFileSync(globalPath, 'utf-8');
114
+ }
115
+
116
+ return steering;
117
+ }
118
+
119
+ /**
120
+ * Persist state to disk
121
+ */
122
+ persist() {
123
+ const state = {
124
+ format: 'native',
125
+ status: this.status,
126
+ memory: this._rawMemory,
127
+ _error: this._error,
128
+ startedAt: this.startedAt,
129
+ lastUpdatedAt: new Date().toISOString()
130
+ };
131
+
132
+ fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
133
+ }
134
+
135
+ /**
136
+ * Prepend an event to history.jsonl (newest first)
137
+ */
138
+ prependHistory(event) {
139
+ const entry = {
140
+ timestamp: new Date().toISOString(),
141
+ ...event
142
+ };
143
+
144
+ const line = JSON.stringify(entry) + '\n';
145
+
146
+ // Prepend to file (read existing, write new + existing)
147
+ let existing = '';
148
+ if (fs.existsSync(this.historyFile)) {
149
+ existing = fs.readFileSync(this.historyFile, 'utf-8');
150
+ }
151
+ fs.writeFileSync(this.historyFile, line + existing);
152
+ }
153
+
154
+ /**
155
+ * Run workflow (load and execute workflow.js)
156
+ */
157
+ async runWorkflow(workflowPath) {
158
+ setCurrentRuntime(this);
159
+
160
+ try {
161
+ this.status = 'RUNNING';
162
+ this._error = null;
163
+ if (!this.startedAt) this.startedAt = new Date().toISOString();
164
+ this.persist();
165
+
166
+ this.prependHistory({ event: 'WORKFLOW_STARTED' });
167
+
168
+ // Import workflow module
169
+ const workflowModule = await import(workflowPath);
170
+ const runFn = workflowModule.default || workflowModule.run || workflowModule;
171
+
172
+ // Load workflow config from module export
173
+ const cfg = workflowModule.config || {};
174
+ this.workflowConfig = {
175
+ models: cfg.models || {},
176
+ apiKeys: cfg.apiKeys || {},
177
+ description: cfg.description || ''
178
+ };
179
+
180
+ if (typeof runFn !== 'function') {
181
+ throw new Error('Workflow module must export a default async function');
182
+ }
183
+
184
+ // Run workflow - interactions block inline, no re-running needed
185
+ await runFn();
186
+
187
+ this.status = 'COMPLETED';
188
+ this.persist();
189
+ this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
190
+
191
+ console.log(`\n✓ Workflow '${this.workflowName}' completed successfully!`);
192
+ } catch (err) {
193
+ this.status = 'FAILED';
194
+ this._error = err.message;
195
+ this.persist();
196
+
197
+ this.prependHistory({
198
+ event: 'WORKFLOW_FAILED',
199
+ error: err.message
200
+ });
201
+
202
+ console.error(`\n✗ Workflow '${this.workflowName}' failed: ${err.message}`);
203
+ throw err;
204
+ } finally {
205
+ clearCurrentRuntime();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Wait for user to confirm interaction is complete, then return the response
211
+ */
212
+ async waitForInteraction(filePath, slug, targetKey) {
213
+ console.log(`\n⏸ Interaction required. Edit: ${filePath}`);
214
+ console.log(` Enter 'y' to proceed or 'q' to quit.\n`);
215
+
216
+ const rl = readline.createInterface({
217
+ input: process.stdin,
218
+ output: process.stdout
219
+ });
220
+
221
+ return new Promise((resolve, reject) => {
222
+ const ask = () => {
223
+ rl.question('> ', (answer) => {
224
+ const a = answer.trim().toLowerCase();
225
+ if (a === 'y') {
226
+ rl.close();
227
+ // Read and return the response
228
+ try {
229
+ const response = this.readInteractionResponse(filePath, slug, targetKey);
230
+ resolve(response);
231
+ } catch (err) {
232
+ reject(err);
233
+ }
234
+ } else if (a === 'q') {
235
+ rl.close();
236
+ reject(new Error('User quit workflow'));
237
+ } else {
238
+ ask();
239
+ }
240
+ });
241
+ };
242
+ ask();
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Read the user's response from an interaction file
248
+ */
249
+ readInteractionResponse(filePath, slug, targetKey) {
250
+ if (!fs.existsSync(filePath)) {
251
+ throw new Error(`Interaction file not found: ${filePath}`);
252
+ }
253
+
254
+ const content = fs.readFileSync(filePath, 'utf-8');
255
+
256
+ // Extract response after separator line
257
+ const separator = '---';
258
+ const parts = content.split(separator);
259
+ if (parts.length < 2) {
260
+ throw new Error(`Interaction file missing separator: ${filePath}`);
261
+ }
262
+
263
+ const response = parts.slice(1).join(separator).trim();
264
+ if (!response || response === 'Enter your response below:') {
265
+ throw new Error(`Interaction response is empty. Please fill in: ${filePath}`);
266
+ }
267
+
268
+ // Store in memory for reference
269
+ this._rawMemory[targetKey] = response;
270
+ this.persist();
271
+
272
+ this.prependHistory({
273
+ event: 'INTERACTION_RESOLVED',
274
+ slug,
275
+ targetKey
276
+ });
277
+
278
+ console.log(`\n✓ Interaction resolved: ${slug}`);
279
+ return response;
280
+ }
281
+
282
+ /**
283
+ * Show workflow status
284
+ */
285
+ showStatus() {
286
+ console.log(`\nWorkflow: ${this.workflowName}`);
287
+ console.log('─'.repeat(40));
288
+ console.log(`Status: ${this.status}`);
289
+
290
+ if (this.startedAt) {
291
+ console.log(`Started: ${this.startedAt}`);
292
+ }
293
+
294
+ if (this._error) {
295
+ console.log(`Error: ${this._error}`);
296
+ }
297
+
298
+ const memoryKeys = Object.keys(this._rawMemory).filter((k) => !k.startsWith('_'));
299
+ console.log(`\nMemory Keys: ${memoryKeys.length}`);
300
+ if (memoryKeys.length > 0) {
301
+ console.log(` ${memoryKeys.slice(0, 10).join(', ')}${memoryKeys.length > 10 ? '...' : ''}`);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Show execution history
307
+ */
308
+ showHistory(limit = 20) {
309
+ console.log(`\nHistory: ${this.workflowName}`);
310
+ console.log('─'.repeat(40));
311
+
312
+ if (!fs.existsSync(this.historyFile)) {
313
+ console.log('No history found.');
314
+ return;
315
+ }
316
+
317
+ const lines = fs
318
+ .readFileSync(this.historyFile, 'utf-8')
319
+ .trim()
320
+ .split('\n')
321
+ .filter(Boolean)
322
+ .slice(0, limit);
323
+
324
+ for (const line of lines) {
325
+ try {
326
+ const entry = JSON.parse(line);
327
+ const time = entry.timestamp ? entry.timestamp.split('T')[1]?.split('.')[0] : '';
328
+ console.log(`[${time}] ${entry.event}`);
329
+ if (entry.agent) console.log(` Agent: ${entry.agent}`);
330
+ if (entry.slug) console.log(` Slug: ${entry.slug}`);
331
+ if (entry.error) console.log(` Error: ${entry.error}`);
332
+ } catch {}
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Reset workflow state (clears memory)
338
+ */
339
+ reset() {
340
+ this._rawMemory = {};
341
+ this._error = null;
342
+ this.status = 'IDLE';
343
+ this.startedAt = null;
344
+
345
+ // Recreate memory proxy
346
+ this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
347
+
348
+ this.persist();
349
+ this.prependHistory({ event: 'WORKFLOW_RESET' });
350
+
351
+ console.log(`\n✓ Workflow '${this.workflowName}' reset`);
352
+ }
353
+
354
+ /**
355
+ * Hard reset workflow state (clears history, interactions, and memory)
356
+ */
357
+ resetHard() {
358
+ // 1. Delete history file
359
+ if (fs.existsSync(this.historyFile)) {
360
+ fs.unlinkSync(this.historyFile);
361
+ }
362
+
363
+ // 2. Delete generated-prompt.md file
364
+ if (fs.existsSync(this.generatedPromptFile)) {
365
+ fs.unlinkSync(this.generatedPromptFile);
366
+ }
367
+
368
+ // 3. Clear interactions directory
369
+ if (fs.existsSync(this.interactionsDir)) {
370
+ fs.rmSync(this.interactionsDir, { recursive: true, force: true });
371
+ fs.mkdirSync(this.interactionsDir, { recursive: true });
372
+ }
373
+
374
+ // 4. Reset internal state and overwrite current.json
375
+ this._rawMemory = {};
376
+ this._error = null;
377
+ this.status = 'IDLE';
378
+ this.startedAt = null;
379
+
380
+ // Recreate memory proxy
381
+ this.memory = createMemoryProxy(this._rawMemory, () => this.persist());
382
+
383
+ this.persist();
384
+
385
+ console.log(`\n✓ Workflow '${this.workflowName}' hard reset (history and interactions cleared)`);
386
+ }
387
+ }