agent-state-machine 2.0.11 → 2.0.13
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 +4 -15
- package/bin/cli.js +14 -11
- package/lib/llm.js +14 -4
- package/lib/runtime/agent.js +124 -36
- package/lib/runtime/runtime.js +95 -6
- package/lib/setup.js +25 -17
- package/package.json +1 -1
- package/vercel-server/ui/index.html +37 -3
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Workflows live in:
|
|
|
56
56
|
```text
|
|
57
57
|
workflows/<name>/
|
|
58
58
|
├── workflow.js # Native JS workflow (async/await)
|
|
59
|
+
├── config.js # Model/API key configuration
|
|
59
60
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
60
61
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
61
62
|
├── interactions/ # Human-in-the-loop files (auto-created)
|
|
@@ -67,6 +68,8 @@ workflows/<name>/
|
|
|
67
68
|
|
|
68
69
|
## Writing workflows (native JS)
|
|
69
70
|
|
|
71
|
+
Edit `config.js` to set models and API keys for the workflow.
|
|
72
|
+
|
|
70
73
|
```js
|
|
71
74
|
/**
|
|
72
75
|
/**
|
|
@@ -83,20 +86,6 @@ workflows/<name>/
|
|
|
83
86
|
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
84
87
|
import { notify } from './scripts/mac-notification.js';
|
|
85
88
|
|
|
86
|
-
// Model configuration (also supports models in a separate config export)
|
|
87
|
-
export const config = {
|
|
88
|
-
models: {
|
|
89
|
-
low: "gemini",
|
|
90
|
-
med: "codex --model gpt-5.2",
|
|
91
|
-
high: "claude -m claude-opus-4-20250514 -p",
|
|
92
|
-
},
|
|
93
|
-
apiKeys: {
|
|
94
|
-
gemini: process.env.GEMINI_API_KEY,
|
|
95
|
-
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
96
|
-
openai: process.env.OPENAI_API_KEY,
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
89
|
export default async function() {
|
|
101
90
|
console.log('Starting project-builder workflow...');
|
|
102
91
|
|
|
@@ -298,7 +287,7 @@ export const config = {
|
|
|
298
287
|
};
|
|
299
288
|
```
|
|
300
289
|
|
|
301
|
-
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates when running with the `--local` flag or via the remote URL. Remote follow links persist across runs (stored in `
|
|
290
|
+
The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates when running with the `--local` flag or via the remote URL. Remote follow links persist across runs (stored in `config.js`) unless you pass `-n`/`--new` to regenerate.
|
|
302
291
|
|
|
303
292
|
---
|
|
304
293
|
|
package/bin/cli.js
CHANGED
|
@@ -63,6 +63,7 @@ Environment Variables:
|
|
|
63
63
|
Workflow Structure:
|
|
64
64
|
workflows/<name>/
|
|
65
65
|
├── workflow.js # Native JS workflow (async/await)
|
|
66
|
+
├── config.js # Model/API key configuration
|
|
66
67
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
67
68
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
68
69
|
├── interactions/ # Human-in-the-loop files (auto-created)
|
|
@@ -185,8 +186,8 @@ function findConfigObjectRange(source) {
|
|
|
185
186
|
return null;
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
function
|
|
189
|
-
const source = fs.readFileSync(
|
|
189
|
+
function readRemotePathFromConfig(configFile) {
|
|
190
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
190
191
|
const range = findConfigObjectRange(source);
|
|
191
192
|
if (!range) return null;
|
|
192
193
|
const configSource = source.slice(range.start, range.end + 1);
|
|
@@ -194,19 +195,19 @@ function readRemotePathFromWorkflow(workflowFile) {
|
|
|
194
195
|
return match ? match[2] : null;
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
function
|
|
198
|
-
const source = fs.readFileSync(
|
|
198
|
+
function writeRemotePathToConfig(configFile, remotePath) {
|
|
199
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
199
200
|
const range = findConfigObjectRange(source);
|
|
200
201
|
const remoteLine = `remotePath: "${remotePath}"`;
|
|
201
202
|
|
|
202
203
|
if (!range) {
|
|
203
204
|
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
204
205
|
if (hasConfigExport) {
|
|
205
|
-
throw new Error('
|
|
206
|
+
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
206
207
|
}
|
|
207
208
|
const trimmed = source.replace(/\s*$/, '');
|
|
208
209
|
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
209
|
-
fs.writeFileSync(
|
|
210
|
+
fs.writeFileSync(configFile, appended);
|
|
210
211
|
return;
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -247,15 +248,15 @@ function writeRemotePathToWorkflow(workflowFile, remotePath) {
|
|
|
247
248
|
source.slice(0, range.start) +
|
|
248
249
|
updatedConfigSource +
|
|
249
250
|
source.slice(range.end + 1);
|
|
250
|
-
fs.writeFileSync(
|
|
251
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
function ensureRemotePath(
|
|
254
|
-
const existing =
|
|
254
|
+
function ensureRemotePath(configFile, { forceNew = false } = {}) {
|
|
255
|
+
const existing = readRemotePathFromConfig(configFile);
|
|
255
256
|
if (existing && !forceNew) return existing;
|
|
256
257
|
|
|
257
258
|
const remotePath = generateSessionToken();
|
|
258
|
-
|
|
259
|
+
writeRemotePathToConfig(configFile, remotePath);
|
|
259
260
|
return remotePath;
|
|
260
261
|
}
|
|
261
262
|
|
|
@@ -276,6 +277,7 @@ function summarizeStatus(state) {
|
|
|
276
277
|
if (s === 'COMPLETED') return ' [completed]';
|
|
277
278
|
if (s === 'FAILED') return ' [failed - can resume]';
|
|
278
279
|
if (s === 'PAUSED') return ' [paused - can resume]';
|
|
280
|
+
if (s === 'STOPPED') return ' [stopped - can resume]';
|
|
279
281
|
if (s === 'RUNNING') return ' [running]';
|
|
280
282
|
if (s === 'IDLE') return ' [idle]';
|
|
281
283
|
return state.status ? ` [${state.status}]` : '';
|
|
@@ -361,6 +363,7 @@ async function runOrResume(
|
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
const workflowUrl = pathToFileURL(entry).href;
|
|
366
|
+
const configFile = path.join(workflowDir, 'config.js');
|
|
364
367
|
|
|
365
368
|
let localServer = null;
|
|
366
369
|
let remoteUrl = null;
|
|
@@ -382,7 +385,7 @@ async function runOrResume(
|
|
|
382
385
|
|
|
383
386
|
// Enable remote follow mode if we have a URL
|
|
384
387
|
if (remoteUrl) {
|
|
385
|
-
const sessionToken = ensureRemotePath(
|
|
388
|
+
const sessionToken = ensureRemotePath(configFile, { forceNew: forceNewRemotePath });
|
|
386
389
|
await runtime.enableRemote(remoteUrl, { sessionToken });
|
|
387
390
|
}
|
|
388
391
|
|
package/lib/llm.js
CHANGED
|
@@ -51,6 +51,7 @@ export function buildPrompt(context, options) {
|
|
|
51
51
|
delete cleanContext._steering;
|
|
52
52
|
delete cleanContext._loop;
|
|
53
53
|
delete cleanContext._config;
|
|
54
|
+
delete cleanContext._memory;
|
|
54
55
|
|
|
55
56
|
// Add the actual prompt
|
|
56
57
|
parts.push('# Task\n\n');
|
|
@@ -70,13 +71,22 @@ export function buildPrompt(context, options) {
|
|
|
70
71
|
parts.push('{ "interact": "your question here" }\n\n');
|
|
71
72
|
parts.push('Only use this format when you genuinely need user input to proceed.\n\n---\n');
|
|
72
73
|
|
|
73
|
-
// Add global steering if available
|
|
74
|
+
// Add global steering if available (always first)
|
|
74
75
|
if (context._steering?.global) {
|
|
75
76
|
parts.push('# System Instructions\n');
|
|
76
77
|
parts.push(context._steering.global);
|
|
77
78
|
parts.push('\n---\n');
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// Add additional steering files if available
|
|
82
|
+
if (context._steering?.additional && context._steering.additional.length > 0) {
|
|
83
|
+
parts.push('# Additional Guidelines\n');
|
|
84
|
+
for (const content of context._steering.additional) {
|
|
85
|
+
parts.push(content);
|
|
86
|
+
parts.push('\n---\n');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
return parts.join('\n');
|
|
81
91
|
}
|
|
82
92
|
|
|
@@ -314,7 +324,7 @@ async function executeAPI(provider, model, prompt, apiKey, options = {}) {
|
|
|
314
324
|
*
|
|
315
325
|
* @param {object} context - The workflow context (contains _config, _steering, etc.)
|
|
316
326
|
* @param {object} options - Options for the LLM call
|
|
317
|
-
* @param {string} options.model - Model key from
|
|
327
|
+
* @param {string} options.model - Model key from config.js models config
|
|
318
328
|
* @param {string} options.prompt - The prompt to send
|
|
319
329
|
* @param {boolean} options.includeContext - Whether to include context in prompt (default: true)
|
|
320
330
|
* @param {number} options.maxTokens - Max tokens for API calls (default: 4096)
|
|
@@ -359,7 +369,7 @@ export async function llm(context, options) {
|
|
|
359
369
|
|
|
360
370
|
if (!apiKey) {
|
|
361
371
|
throw new Error(
|
|
362
|
-
`No API key found for ${provider}. Set in
|
|
372
|
+
`No API key found for ${provider}. Set in config.js apiKeys or ${provider.toUpperCase()}_API_KEY env var`
|
|
363
373
|
);
|
|
364
374
|
}
|
|
365
375
|
|
|
@@ -459,4 +469,4 @@ export async function llmJSON(context, options) {
|
|
|
459
469
|
...response,
|
|
460
470
|
data: parseJSON(response.text)
|
|
461
471
|
};
|
|
462
|
-
}
|
|
472
|
+
}
|
package/lib/runtime/agent.js
CHANGED
|
@@ -17,50 +17,87 @@ const require = createRequire(import.meta.url);
|
|
|
17
17
|
/**
|
|
18
18
|
* Run an agent with context
|
|
19
19
|
* @param {string} name - Agent name (file basename)
|
|
20
|
-
* @param {object} params - Parameters passed to agent
|
|
20
|
+
* @param {object} params - Parameters passed to agent (default: {})
|
|
21
|
+
* @param {object} options - Agent execution options (default: {})
|
|
22
|
+
* @param {number|false} options.retry - Number of retries (default: 2, meaning 3 total attempts). Set to false to disable.
|
|
23
|
+
* @param {string|string[]} options.steering - Additional steering files to load from steering/ folder
|
|
21
24
|
*/
|
|
22
|
-
export async function agent(name, params = {}) {
|
|
25
|
+
export async function agent(name, params = {}, options = {}) {
|
|
23
26
|
const runtime = getCurrentRuntime();
|
|
24
27
|
if (!runtime) {
|
|
25
28
|
throw new Error('agent() must be called within a workflow context');
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
// Parse retry option: default is 2 retries (3 total attempts)
|
|
32
|
+
const retryCount = options.retry === false ? 0 : (options.retry ?? 2);
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
const result = await executeAgent(runtime, name, params);
|
|
34
|
+
let lastError;
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
if (attempt > 0) {
|
|
39
|
+
console.log(` [Agent: ${name}] Retry attempt ${attempt}/${retryCount}...`);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(` [Agent: ${name}] Starting...`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await executeAgent(runtime, name, params, options);
|
|
45
|
+
|
|
46
|
+
if (result && typeof result === 'object' && result._debug_prompt) {
|
|
47
|
+
delete result._debug_prompt;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(` [Agent: ${name}] Completed`);
|
|
51
|
+
if (runtime._agentSuppressCompletion?.has(name)) {
|
|
52
|
+
runtime._agentSuppressCompletion.delete(name);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
runtime.prependHistory({
|
|
57
|
+
event: 'AGENT_COMPLETED',
|
|
58
|
+
agent: name,
|
|
59
|
+
output: result,
|
|
60
|
+
attempts: attempt + 1
|
|
61
|
+
});
|
|
36
62
|
|
|
37
|
-
console.log(` [Agent: ${name}] Completed`);
|
|
38
|
-
if (runtime._agentSuppressCompletion?.has(name)) {
|
|
39
|
-
runtime._agentSuppressCompletion.delete(name);
|
|
40
63
|
return result;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
lastError = error;
|
|
66
|
+
|
|
67
|
+
if (attempt < retryCount) {
|
|
68
|
+
console.error(` [Agent: ${name}] Error (attempt ${attempt + 1}/${retryCount + 1}): ${error.message}`);
|
|
69
|
+
runtime.prependHistory({
|
|
70
|
+
event: 'AGENT_RETRY',
|
|
71
|
+
agent: name,
|
|
72
|
+
attempt: attempt + 1,
|
|
73
|
+
error: error.message
|
|
74
|
+
});
|
|
75
|
+
}
|
|
41
76
|
}
|
|
77
|
+
}
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
79
|
+
// All retries exhausted - record failure
|
|
80
|
+
runtime.prependHistory({
|
|
81
|
+
event: 'AGENT_FAILED',
|
|
82
|
+
agent: name,
|
|
83
|
+
error: lastError.message,
|
|
84
|
+
attempts: retryCount + 1
|
|
85
|
+
});
|
|
48
86
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
87
|
+
// Store error in accessible location (not auto-spread to context)
|
|
88
|
+
runtime._agentErrors.push({
|
|
89
|
+
agent: name,
|
|
90
|
+
error: lastError.message,
|
|
91
|
+
timestamp: new Date().toISOString()
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
throw lastError;
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
/**
|
|
61
98
|
* Execute an agent (load and run)
|
|
62
99
|
*/
|
|
63
|
-
export async function executeAgent(runtime, name, params) {
|
|
100
|
+
export async function executeAgent(runtime, name, params, options = {}) {
|
|
64
101
|
const agentsDir = runtime.agentsDir;
|
|
65
102
|
|
|
66
103
|
// Try JS agents (.js/.mjs/.cjs)
|
|
@@ -72,14 +109,14 @@ export async function executeAgent(runtime, name, params) {
|
|
|
72
109
|
|
|
73
110
|
for (const p of jsCandidates) {
|
|
74
111
|
if (fs.existsSync(p)) {
|
|
75
|
-
return executeJSAgent(runtime, p, name, params);
|
|
112
|
+
return executeJSAgent(runtime, p, name, params, options);
|
|
76
113
|
}
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
// Try Markdown agent
|
|
80
117
|
const mdPath = path.join(agentsDir, `${name}.md`);
|
|
81
118
|
if (fs.existsSync(mdPath)) {
|
|
82
|
-
return executeMDAgent(runtime, mdPath, name, params);
|
|
119
|
+
return executeMDAgent(runtime, mdPath, name, params, options);
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
throw new Error(
|
|
@@ -94,7 +131,7 @@ export async function executeAgent(runtime, name, params) {
|
|
|
94
131
|
* - ESM (.js/.mjs): loaded via dynamic import with cache-bust for hot reload
|
|
95
132
|
* - CJS (.cjs): loaded via require() with cache clear
|
|
96
133
|
*/
|
|
97
|
-
async function executeJSAgent(runtime, agentPath, name, params) {
|
|
134
|
+
async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
|
|
98
135
|
const ext = path.extname(agentPath).toLowerCase();
|
|
99
136
|
|
|
100
137
|
let agentModule;
|
|
@@ -119,11 +156,15 @@ async function executeJSAgent(runtime, agentPath, name, params) {
|
|
|
119
156
|
|
|
120
157
|
logAgentStart(runtime, name);
|
|
121
158
|
|
|
122
|
-
// Build context
|
|
159
|
+
// Build steering context (global + any additional files from options)
|
|
160
|
+
const steeringContext = options.steering
|
|
161
|
+
? runtime.loadSteeringFiles(options.steering)
|
|
162
|
+
: runtime.steering;
|
|
163
|
+
|
|
164
|
+
// Build context - only spread params, NOT memory (explicit context passing)
|
|
123
165
|
const context = {
|
|
124
|
-
...runtime._rawMemory,
|
|
125
166
|
...params,
|
|
126
|
-
_steering:
|
|
167
|
+
_steering: steeringContext,
|
|
127
168
|
_config: {
|
|
128
169
|
models: runtime.workflowConfig.models,
|
|
129
170
|
apiKeys: runtime.workflowConfig.apiKeys,
|
|
@@ -175,7 +216,7 @@ async function executeJSAgent(runtime, agentPath, name, params) {
|
|
|
175
216
|
/**
|
|
176
217
|
* Execute a Markdown agent (prompt-based)
|
|
177
218
|
*/
|
|
178
|
-
async function executeMDAgent(runtime, agentPath, name, params) {
|
|
219
|
+
async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
|
|
179
220
|
const { llm, buildPrompt, parseJSON, parseInteractionRequest } = await import('../llm.js');
|
|
180
221
|
|
|
181
222
|
const content = fs.readFileSync(agentPath, 'utf-8');
|
|
@@ -184,11 +225,28 @@ async function executeMDAgent(runtime, agentPath, name, params) {
|
|
|
184
225
|
const outputKey = config.output || 'result';
|
|
185
226
|
const targetKey = config.interactionKey || outputKey;
|
|
186
227
|
|
|
187
|
-
//
|
|
228
|
+
// Combine steering from options (runtime call) and frontmatter (static)
|
|
229
|
+
let steeringNames = [];
|
|
230
|
+
|
|
231
|
+
if (options.steering) {
|
|
232
|
+
const optSteering = Array.isArray(options.steering) ? options.steering : [options.steering];
|
|
233
|
+
steeringNames.push(...optSteering);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (config.steering) {
|
|
237
|
+
const fmSteering = parseSteeringFrontmatter(config.steering);
|
|
238
|
+
steeringNames.push(...fmSteering);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build steering context (global + any additional files)
|
|
242
|
+
const steeringContext = steeringNames.length > 0
|
|
243
|
+
? runtime.loadSteeringFiles(steeringNames)
|
|
244
|
+
: runtime.steering;
|
|
245
|
+
|
|
246
|
+
// Build context - only spread params, NOT memory (explicit context passing)
|
|
188
247
|
const context = {
|
|
189
|
-
...runtime._rawMemory,
|
|
190
248
|
...params,
|
|
191
|
-
_steering:
|
|
249
|
+
_steering: steeringContext,
|
|
192
250
|
_config: {
|
|
193
251
|
models: runtime.workflowConfig.models,
|
|
194
252
|
apiKeys: runtime.workflowConfig.apiKeys,
|
|
@@ -296,6 +354,36 @@ function parseMarkdownAgent(content) {
|
|
|
296
354
|
return { config: {}, prompt: content.trim() };
|
|
297
355
|
}
|
|
298
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Parse steering value from frontmatter
|
|
359
|
+
* Supports: "name", "name1, name2", "[name1, name2]", "['name1', 'name2']"
|
|
360
|
+
* @param {string} value - Frontmatter steering value
|
|
361
|
+
* @returns {string[]} Array of steering file names
|
|
362
|
+
*/
|
|
363
|
+
function parseSteeringFrontmatter(value) {
|
|
364
|
+
if (!value) return [];
|
|
365
|
+
|
|
366
|
+
const trimmed = value.trim();
|
|
367
|
+
|
|
368
|
+
// Handle array format: [a, b, c] or ["a", "b", "c"]
|
|
369
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
370
|
+
const inner = trimmed.slice(1, -1);
|
|
371
|
+
return inner.split(',')
|
|
372
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Handle comma-separated: a, b, c
|
|
377
|
+
if (trimmed.includes(',')) {
|
|
378
|
+
return trimmed.split(',')
|
|
379
|
+
.map(s => s.trim())
|
|
380
|
+
.filter(Boolean);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Single value
|
|
384
|
+
return [trimmed];
|
|
385
|
+
}
|
|
386
|
+
|
|
299
387
|
/**
|
|
300
388
|
* Interpolate {{variables}} in prompt template
|
|
301
389
|
*/
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import readline from 'readline';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
12
13
|
import { createMemoryProxy } from './memory.js';
|
|
13
14
|
import { RemoteClient } from '../remote/client.js';
|
|
14
15
|
|
|
@@ -88,6 +89,9 @@ export class WorkflowRuntime {
|
|
|
88
89
|
// Agent interaction tracking for history logging
|
|
89
90
|
this._agentResumeFlags = new Set();
|
|
90
91
|
this._agentSuppressCompletion = new Set();
|
|
92
|
+
|
|
93
|
+
// Agent error tracking (not persisted to memory, but accessible during run)
|
|
94
|
+
this._agentErrors = [];
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
ensureDirectories() {
|
|
@@ -141,6 +145,39 @@ export class WorkflowRuntime {
|
|
|
141
145
|
return steering;
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Load a specific steering file by name
|
|
150
|
+
* @param {string} name - Name of the steering file (without .md extension)
|
|
151
|
+
* @returns {string} Content of the steering file, or empty string if not found
|
|
152
|
+
*/
|
|
153
|
+
loadSteeringFile(name) {
|
|
154
|
+
const filePath = path.join(this.steeringDir, `${name}.md`);
|
|
155
|
+
|
|
156
|
+
if (fs.existsSync(filePath)) {
|
|
157
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.warn(`${C.yellow}Warning: Steering file not found: ${name}.md${C.reset}`);
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load multiple steering files and combine with global
|
|
166
|
+
* @param {string|string[]} steeringNames - Names of steering files to load
|
|
167
|
+
* @returns {{ enabled: boolean, global: string, additional: string[] }}
|
|
168
|
+
*/
|
|
169
|
+
loadSteeringFiles(steeringNames) {
|
|
170
|
+
const names = Array.isArray(steeringNames) ? steeringNames : [steeringNames];
|
|
171
|
+
const additional = names
|
|
172
|
+
.map(name => this.loadSteeringFile(name))
|
|
173
|
+
.filter(content => content.length > 0);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
...this.steering,
|
|
177
|
+
additional
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
144
181
|
/**
|
|
145
182
|
* Persist state to disk
|
|
146
183
|
*/
|
|
@@ -218,6 +255,50 @@ export class WorkflowRuntime {
|
|
|
218
255
|
async runWorkflow(workflowPath) {
|
|
219
256
|
setCurrentRuntime(this);
|
|
220
257
|
|
|
258
|
+
// Handle Ctrl+C and termination signals to update status before exit
|
|
259
|
+
const handleShutdown = async (signal) => {
|
|
260
|
+
this.status = 'STOPPED';
|
|
261
|
+
this._error = `Workflow interrupted by ${signal}`;
|
|
262
|
+
this.persist();
|
|
263
|
+
|
|
264
|
+
// Log to history (local file)
|
|
265
|
+
const historyEntry = {
|
|
266
|
+
timestamp: new Date().toISOString(),
|
|
267
|
+
event: 'WORKFLOW_STOPPED',
|
|
268
|
+
reason: signal
|
|
269
|
+
};
|
|
270
|
+
const line = JSON.stringify(historyEntry) + '\n';
|
|
271
|
+
let existing = '';
|
|
272
|
+
if (fs.existsSync(this.historyFile)) {
|
|
273
|
+
existing = fs.readFileSync(this.historyFile, 'utf-8');
|
|
274
|
+
}
|
|
275
|
+
fs.writeFileSync(this.historyFile, line + existing);
|
|
276
|
+
|
|
277
|
+
// Send to remote and wait for it to complete before exiting
|
|
278
|
+
if (this.remoteClient && this.remoteEnabled) {
|
|
279
|
+
try {
|
|
280
|
+
await this.remoteClient.sendEvent(historyEntry);
|
|
281
|
+
} catch {
|
|
282
|
+
// Ignore errors during shutdown
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(`\n${C.yellow}⚠ Workflow '${this.workflowName}' stopped (${signal})${C.reset}`);
|
|
287
|
+
cleanupSignalHandlers();
|
|
288
|
+
clearCurrentRuntime();
|
|
289
|
+
process.exit(130); // 128 + SIGINT (2)
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const sigintHandler = () => handleShutdown('SIGINT');
|
|
293
|
+
const sigtermHandler = () => handleShutdown('SIGTERM');
|
|
294
|
+
process.on('SIGINT', sigintHandler);
|
|
295
|
+
process.on('SIGTERM', sigtermHandler);
|
|
296
|
+
|
|
297
|
+
const cleanupSignalHandlers = () => {
|
|
298
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
299
|
+
process.removeListener('SIGTERM', sigtermHandler);
|
|
300
|
+
};
|
|
301
|
+
|
|
221
302
|
try {
|
|
222
303
|
this.status = 'RUNNING';
|
|
223
304
|
this._error = null;
|
|
@@ -226,18 +307,24 @@ export class WorkflowRuntime {
|
|
|
226
307
|
|
|
227
308
|
this.prependHistory({ event: 'WORKFLOW_STARTED' });
|
|
228
309
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
310
|
+
const configPath = path.join(this.workflowDir, 'config.js');
|
|
311
|
+
if (!fs.existsSync(configPath)) {
|
|
312
|
+
throw new Error(`config.js not found in ${this.workflowDir}`);
|
|
313
|
+
}
|
|
314
|
+
const configUrl = pathToFileURL(configPath);
|
|
315
|
+
configUrl.searchParams.set('t', Date.now().toString());
|
|
316
|
+
const configModule = await import(configUrl.href);
|
|
317
|
+
const cfg = configModule.config || configModule.default || {};
|
|
235
318
|
this.workflowConfig = {
|
|
236
319
|
models: cfg.models || {},
|
|
237
320
|
apiKeys: cfg.apiKeys || {},
|
|
238
321
|
description: cfg.description || ''
|
|
239
322
|
};
|
|
240
323
|
|
|
324
|
+
// Import workflow module
|
|
325
|
+
const workflowModule = await import(workflowPath);
|
|
326
|
+
const runFn = workflowModule.default || workflowModule.run || workflowModule;
|
|
327
|
+
|
|
241
328
|
if (typeof runFn !== 'function') {
|
|
242
329
|
throw new Error('Workflow module must export a default async function');
|
|
243
330
|
}
|
|
@@ -263,6 +350,7 @@ export class WorkflowRuntime {
|
|
|
263
350
|
console.error(`\n${C.red}✗ Workflow '${this.workflowName}' failed: ${err.message}${C.reset}`);
|
|
264
351
|
throw err;
|
|
265
352
|
} finally {
|
|
353
|
+
cleanupSignalHandlers();
|
|
266
354
|
clearCurrentRuntime();
|
|
267
355
|
}
|
|
268
356
|
}
|
|
@@ -406,6 +494,7 @@ export class WorkflowRuntime {
|
|
|
406
494
|
let statusColor = C.reset;
|
|
407
495
|
if (this.status === 'COMPLETED') statusColor = C.green;
|
|
408
496
|
if (this.status === 'FAILED') statusColor = C.red;
|
|
497
|
+
if (this.status === 'STOPPED') statusColor = C.yellow;
|
|
409
498
|
if (this.status === 'RUNNING') statusColor = C.blue;
|
|
410
499
|
if (this.status === 'IDLE') statusColor = C.gray;
|
|
411
500
|
|
package/lib/setup.js
CHANGED
|
@@ -62,20 +62,6 @@ async function setup(workflowName) {
|
|
|
62
62
|
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
63
63
|
import { notify } from './scripts/mac-notification.js';
|
|
64
64
|
|
|
65
|
-
// Model configuration (also supports models in a separate config export)
|
|
66
|
-
export const config = {
|
|
67
|
-
models: {
|
|
68
|
-
low: "gemini",
|
|
69
|
-
med: "codex --model gpt-5.2",
|
|
70
|
-
high: "claude -m claude-opus-4-20250514 -p",
|
|
71
|
-
},
|
|
72
|
-
apiKeys: {
|
|
73
|
-
gemini: process.env.GEMINI_API_KEY,
|
|
74
|
-
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
75
|
-
openai: process.env.OPENAI_API_KEY,
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
65
|
export default async function() {
|
|
80
66
|
console.log('Starting ${workflowName} workflow...');
|
|
81
67
|
|
|
@@ -119,6 +105,24 @@ export default async function() {
|
|
|
119
105
|
fs.writeFileSync(workflowFile, workflowJs);
|
|
120
106
|
console.log(` Created: ${path.relative(process.cwd(), workflowFile)}`);
|
|
121
107
|
|
|
108
|
+
const configJs = `export const config = {
|
|
109
|
+
models: {
|
|
110
|
+
low: "gemini",
|
|
111
|
+
med: "codex --model gpt-5.2",
|
|
112
|
+
high: "claude -m claude-opus-4-20250514 -p",
|
|
113
|
+
},
|
|
114
|
+
apiKeys: {
|
|
115
|
+
gemini: process.env.GEMINI_API_KEY,
|
|
116
|
+
anthropic: process.env.ANTHROPIC_API_KEY,
|
|
117
|
+
openai: process.env.OPENAI_API_KEY,
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const configFile = path.join(workflowDir, 'config.js');
|
|
123
|
+
fs.writeFileSync(configFile, configJs);
|
|
124
|
+
console.log(` Created: ${path.relative(process.cwd(), configFile)}`);
|
|
125
|
+
|
|
122
126
|
// Create example JS agent (ESM)
|
|
123
127
|
// Create example JS agent (ESM)
|
|
124
128
|
const exampleAgent = `/**
|
|
@@ -138,7 +142,7 @@ export default async function handler(context) {
|
|
|
138
142
|
console.log('[Agent: example] Steering loaded (' + context._steering.global.length + ' chars)');
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
// Example: Call an LLM (configure models in
|
|
145
|
+
// Example: Call an LLM (configure models in config.js)
|
|
142
146
|
// const response = await llm(context, {
|
|
143
147
|
// model: 'smart',
|
|
144
148
|
// prompt: 'Say hello and describe what you can help with.'
|
|
@@ -295,6 +299,7 @@ A workflow created with agent-state-machine (native JS format).
|
|
|
295
299
|
\\\`\\\`\\\`
|
|
296
300
|
${workflowName}/
|
|
297
301
|
├── workflow.js # Native JS workflow (async/await)
|
|
302
|
+
├── config.js # Model/API key configuration
|
|
298
303
|
├── package.json # Sets "type": "module" for this workflow folder
|
|
299
304
|
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
300
305
|
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
@@ -304,6 +309,8 @@ ${workflowName}/
|
|
|
304
309
|
|
|
305
310
|
## Usage
|
|
306
311
|
|
|
312
|
+
Edit \`config.js\` to set models and API keys for this workflow.
|
|
313
|
+
|
|
307
314
|
Run the workflow (or resume if interrupted):
|
|
308
315
|
\\\`\\\`\\\`bash
|
|
309
316
|
state-machine run ${workflowName}
|
|
@@ -411,8 +418,9 @@ Generate a greeting for {{name}}.
|
|
|
411
418
|
console.log(`\n✓ Workflow '${workflowName}' created successfully!\n`);
|
|
412
419
|
console.log('Next steps:');
|
|
413
420
|
console.log(` 1. Edit workflows/${workflowName}/workflow.js to implement your flow`);
|
|
414
|
-
console.log(` 2.
|
|
415
|
-
console.log(` 3.
|
|
421
|
+
console.log(` 2. Edit workflows/${workflowName}/config.js to set models/API keys`);
|
|
422
|
+
console.log(` 3. Add custom agents in workflows/${workflowName}/agents/`);
|
|
423
|
+
console.log(` 4. Run: state-machine run ${workflowName}\n`);
|
|
416
424
|
}
|
|
417
425
|
|
|
418
426
|
export { setup };
|
package/package.json
CHANGED
|
@@ -486,11 +486,24 @@
|
|
|
486
486
|
|
|
487
487
|
useEffect(() => localStorage.setItem("rf_theme", theme), [theme]);
|
|
488
488
|
|
|
489
|
+
// Helper to check if workflow is currently running based on history
|
|
490
|
+
const isWorkflowRunning = (entries) => {
|
|
491
|
+
// Find the most recent workflow lifecycle event (history is newest-first)
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (entry.event === "WORKFLOW_STARTED") return true;
|
|
494
|
+
if (entry.event === "WORKFLOW_STOPPED" ||
|
|
495
|
+
entry.event === "WORKFLOW_COMPLETED" ||
|
|
496
|
+
entry.event === "WORKFLOW_FAILED") return false;
|
|
497
|
+
}
|
|
498
|
+
return false; // No lifecycle events found
|
|
499
|
+
};
|
|
500
|
+
|
|
489
501
|
useEffect(() => {
|
|
490
502
|
if (history.length === 0) { setPendingInteraction(null); return; }
|
|
491
503
|
|
|
492
504
|
const resolvedSlugs = new Set();
|
|
493
505
|
let pending = null;
|
|
506
|
+
const workflowRunning = isWorkflowRunning(history);
|
|
494
507
|
|
|
495
508
|
for (const entry of history) {
|
|
496
509
|
const isResolution =
|
|
@@ -510,7 +523,8 @@
|
|
|
510
523
|
}
|
|
511
524
|
}
|
|
512
525
|
|
|
513
|
-
|
|
526
|
+
// Only show pending interaction if workflow is running
|
|
527
|
+
setPendingInteraction(workflowRunning ? pending : null);
|
|
514
528
|
}, [history]);
|
|
515
529
|
|
|
516
530
|
const fetchData = async () => {
|
|
@@ -520,8 +534,18 @@
|
|
|
520
534
|
if (data.entries) setHistory(data.entries);
|
|
521
535
|
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
522
536
|
|
|
523
|
-
if
|
|
524
|
-
|
|
537
|
+
// Check if workflow is currently running based on most recent lifecycle event
|
|
538
|
+
const workflowRunning = isWorkflowRunning(data.entries || []);
|
|
539
|
+
|
|
540
|
+
if (workflowRunning) {
|
|
541
|
+
setStatus("connected");
|
|
542
|
+
} else if (token && data.cliConnected !== undefined) {
|
|
543
|
+
setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
544
|
+
} else if (!token) {
|
|
545
|
+
setStatus("connected");
|
|
546
|
+
} else {
|
|
547
|
+
setStatus("disconnected");
|
|
548
|
+
}
|
|
525
549
|
|
|
526
550
|
setLoading(false);
|
|
527
551
|
return true;
|
|
@@ -569,6 +593,8 @@
|
|
|
569
593
|
break;
|
|
570
594
|
case "history":
|
|
571
595
|
setHistory(data.entries || []);
|
|
596
|
+
// Update status based on workflow lifecycle
|
|
597
|
+
setStatus(isWorkflowRunning(data.entries || []) ? "connected" : "disconnected");
|
|
572
598
|
break;
|
|
573
599
|
case "event":
|
|
574
600
|
setHistory((prev) => {
|
|
@@ -578,6 +604,14 @@
|
|
|
578
604
|
}
|
|
579
605
|
return [data, ...prev];
|
|
580
606
|
});
|
|
607
|
+
// Update status based on workflow lifecycle events
|
|
608
|
+
if (data.event === "WORKFLOW_STARTED") {
|
|
609
|
+
setStatus("connected");
|
|
610
|
+
} else if (data.event === "WORKFLOW_STOPPED" ||
|
|
611
|
+
data.event === "WORKFLOW_COMPLETED" ||
|
|
612
|
+
data.event === "WORKFLOW_FAILED") {
|
|
613
|
+
setStatus("disconnected");
|
|
614
|
+
}
|
|
581
615
|
break;
|
|
582
616
|
case "cli_connected":
|
|
583
617
|
case "cli_reconnected":
|