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.
- package/README.md +299 -0
- package/bin/cli.js +246 -0
- package/lib/index.js +110 -0
- package/lib/index.mjs +9 -0
- package/lib/llm.js +472 -0
- package/lib/runtime/agent.js +359 -0
- package/lib/runtime/index.js +35 -0
- package/lib/runtime/memory.js +88 -0
- package/lib/runtime/parallel.js +66 -0
- package/lib/runtime/prompt.js +118 -0
- package/lib/runtime/runtime.js +387 -0
- package/lib/setup.js +398 -0
- package/lib/state-machine.js +1359 -0
- package/package.json +32 -0
|
@@ -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
|
+
}
|