agent-state-machine 2.0.12 → 2.0.14

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 CHANGED
@@ -6,6 +6,7 @@ You write normal `async/await` code. The runtime handles:
6
6
  - **Auto-persisted** `memory` (saved to disk on mutation)
7
7
  - **Human-in-the-loop** blocking via `askHuman()` or agent-driven interactions
8
8
  - Local **JS agents** + **Markdown agents** (LLM-powered)
9
+ - **Agent retries** with history logging for failures
9
10
 
10
11
  ---
11
12
 
@@ -43,14 +44,19 @@ Requirements: Node.js >= 16.
43
44
 
44
45
  ```bash
45
46
  state-machine --setup <workflow-name>
47
+ state-machine --setup <workflow-name> --template <template-name>
46
48
  state-machine run <workflow-name>
47
49
  state-machine run <workflow-name> -reset
48
50
  state-machine run <workflow-name> -reset-hard
49
51
 
52
+ state-machine -reset <workflow-name>
53
+ state-machine -reset-hard <workflow-name>
50
54
 
51
55
  state-machine history <workflow-name> [limit]
52
56
  ```
53
57
 
58
+ Templates live in `templates/` and `starter` is used by default.
59
+
54
60
  Workflows live in:
55
61
 
56
62
  ```text
@@ -101,8 +107,8 @@ export default async function() {
101
107
 
102
108
  console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
103
109
 
104
- // Context is provided automatically
105
- const { greeting } = await agent('yoda-greeter', { userLocation });
110
+ // Context is explicit: pass what the agent needs
111
+ const { greeting } = await agent('yoda-greeter', { userLocation, memory });
106
112
  console.log('Example agent greeting:', greeting);
107
113
 
108
114
  // Or you can provide context manually
@@ -137,7 +143,7 @@ If the process is interrupted, running `state-machine run <workflow-name>` again
137
143
 
138
144
  ## Core API
139
145
 
140
- ### `agent(name, params?)`
146
+ ### `agent(name, params?, options?)`
141
147
 
142
148
  Runs `workflows/<name>/agents/<agent>.(js|mjs|cjs)` or `<agent>.md`.
143
149
 
@@ -146,6 +152,12 @@ const out = await agent('review', { file: 'src/app.js' });
146
152
  memory.lastReview = out;
147
153
  ```
148
154
 
155
+ Options:
156
+ - `retry` (number | false): default `2` (3 total attempts). Use `false` to disable retries.
157
+ - `steering` (string | string[]): extra steering files to load from `workflows/<name>/steering/`.
158
+
159
+ Context is explicit: only `params` are provided to agents unless you pass additional data.
160
+
149
161
  ### `memory`
150
162
 
151
163
  A persisted object for your workflow.
@@ -203,9 +215,8 @@ import { llm } from 'agent-state-machine';
203
215
 
204
216
  export default async function handler(context) {
205
217
  // context includes:
206
- // - persisted memory (spread into the object)
207
218
  // - params passed to agent(name, params)
208
- // - context._steering (global steering prompt/config)
219
+ // - context._steering (global + optional additional steering content)
209
220
  // - context._config (models/apiKeys/workflowDir)
210
221
  return { ok: true };
211
222
  }
@@ -240,11 +251,13 @@ The runtime will block execution and wait for your response in the terminal.
240
251
  ### Markdown agents (`.md`)
241
252
 
242
253
  Markdown agents are LLM-backed prompt templates with optional frontmatter.
254
+ Frontmatter can include `steering` to load additional files from `workflows/<name>/steering/`.
243
255
 
244
256
  ```md
245
257
  ---
246
258
  model: smart
247
259
  output: greeting
260
+ steering: tone, product
248
261
  ---
249
262
  Generate a friendly greeting for {{name}}.
250
263
  ```
@@ -296,7 +309,7 @@ The runtime captures the fully-built prompt in `state/history.jsonl`, viewable i
296
309
  Native JS workflows persist to:
297
310
 
298
311
  - `workflows/<name>/state/current.json` — status, memory, pending interaction
299
- - `workflows/<name>/state/history.jsonl` — event log (newest entries first)
312
+ - `workflows/<name>/state/history.jsonl` — event log (newest entries first, includes agent retry/failure entries)
300
313
  - `workflows/<name>/interactions/*.md` — human input files (when paused)
301
314
 
302
315
  ## License
package/bin/cli.js CHANGED
@@ -13,8 +13,13 @@ import { startLocalServer } from '../vercel-server/local-server.js';
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = path.dirname(__filename);
15
15
 
16
- const args = process.argv.slice(2);
17
- const command = args[0];
16
+ let args = process.argv.slice(2);
17
+ let command = args[0];
18
+ const legacyResetCommand = command === '-reset' || command === '-reset-hard';
19
+ if (legacyResetCommand) {
20
+ command = command.slice(1);
21
+ args = [command, ...args.slice(1)];
22
+ }
18
23
 
19
24
  function getVersion() {
20
25
  try {
@@ -34,13 +39,15 @@ function printHelp() {
34
39
  Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
35
40
 
36
41
  Usage:
37
- state-machine --setup <workflow-name> Create a new workflow project
42
+ state-machine --setup <workflow-name> [--template <template-name>] Create a new workflow project
38
43
  state-machine run <workflow-name> Run a workflow (remote follow enabled by default)
39
44
  state-machine run <workflow-name> -l Run with local server (localhost:3000)
40
45
  state-machine run <workflow-name> -n Generate a new remote follow path
41
46
  state-machine run <workflow-name> -reset Reset workflow state before running
42
47
  state-machine run <workflow-name> -reset-hard Hard reset workflow before running
43
48
 
49
+ state-machine -reset <workflow-name> Reset workflow state (legacy alias)
50
+ state-machine -reset-hard <workflow-name> Hard reset workflow (legacy alias)
44
51
  state-machine status [workflow-name] Show current state (or list all)
45
52
  state-machine history <workflow-name> [limit] Show execution history logs
46
53
  state-machine reset <workflow-name> Reset workflow state (clears memory/state)
@@ -50,6 +57,7 @@ Usage:
50
57
 
51
58
  Options:
52
59
  --setup, -s Initialize a new workflow with directory structure
60
+ --template, -t Template name for --setup (default: starter)
53
61
  --local, -l Use local server instead of remote (starts on localhost:3000)
54
62
  --new, -n Generate a new remote follow path
55
63
  -reset Reset workflow state before running
@@ -421,10 +429,20 @@ async function main() {
421
429
  const workflowName = args[1];
422
430
  if (!workflowName) {
423
431
  console.error('Error: Workflow name required');
424
- console.error('Usage: state-machine --setup <workflow-name>');
432
+ console.error('Usage: state-machine --setup <workflow-name> [--template <template-name>]');
425
433
  process.exit(1);
426
434
  }
427
- await setup(workflowName);
435
+ const templateFlagIndex = args.findIndex((arg) => arg === '--template' || arg === '-t');
436
+ let templateName = null;
437
+ if (templateFlagIndex !== -1) {
438
+ templateName = args[templateFlagIndex + 1];
439
+ if (!templateName || templateName.startsWith('-')) {
440
+ console.error('Error: Template name required');
441
+ console.error('Usage: state-machine --setup <workflow-name> [--template <template-name>]');
442
+ process.exit(1);
443
+ }
444
+ }
445
+ await setup(workflowName, { template: templateName || undefined });
428
446
  process.exit(0);
429
447
  }
430
448
 
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
 
@@ -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
- console.log(` [Agent: ${name}] Starting...`);
31
+ // Parse retry option: default is 2 retries (3 total attempts)
32
+ const retryCount = options.retry === false ? 0 : (options.retry ?? 2);
29
33
 
30
- try {
31
- const result = await executeAgent(runtime, name, params);
34
+ let lastError;
32
35
 
33
- if (result && typeof result === 'object' && result._debug_prompt) {
34
- delete result._debug_prompt;
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
- runtime.prependHistory({
44
- event: 'AGENT_COMPLETED',
45
- agent: name,
46
- output: result
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
- return result;
50
- } catch (error) {
51
- runtime.prependHistory({
52
- event: 'AGENT_FAILED',
53
- agent: name,
54
- error: error.message
55
- });
56
- throw error;
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: runtime.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
- // Build context
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: runtime.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
  */
@@ -89,6 +89,9 @@ export class WorkflowRuntime {
89
89
  // Agent interaction tracking for history logging
90
90
  this._agentResumeFlags = new Set();
91
91
  this._agentSuppressCompletion = new Set();
92
+
93
+ // Agent error tracking (not persisted to memory, but accessible during run)
94
+ this._agentErrors = [];
92
95
  }
93
96
 
94
97
  ensureDirectories() {
@@ -142,6 +145,39 @@ export class WorkflowRuntime {
142
145
  return steering;
143
146
  }
144
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
+
145
181
  /**
146
182
  * Persist state to disk
147
183
  */
package/lib/setup.js CHANGED
@@ -4,13 +4,75 @@
4
4
 
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const DEFAULT_TEMPLATE = 'starter';
13
+
14
+ function getTemplatesDir() {
15
+ return path.join(__dirname, '..', 'templates');
16
+ }
17
+
18
+ function listTemplates(templatesDir) {
19
+ if (!fs.existsSync(templatesDir)) return [];
20
+ return fs.readdirSync(templatesDir, { withFileTypes: true })
21
+ .filter((entry) => entry.isDirectory())
22
+ .map((entry) => entry.name)
23
+ .sort();
24
+ }
25
+
26
+ function applyReplacements(content, replacements) {
27
+ let output = content;
28
+ for (const [token, value] of Object.entries(replacements)) {
29
+ output = output.split(token).join(value);
30
+ }
31
+ return output;
32
+ }
33
+
34
+ function copyTemplateDir(srcDir, destDir, replacements, createdPaths) {
35
+ fs.mkdirSync(destDir, { recursive: true });
36
+
37
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ const srcPath = path.join(srcDir, entry.name);
40
+ const destPath = path.join(destDir, entry.name);
41
+
42
+ if (entry.isDirectory()) {
43
+ copyTemplateDir(srcPath, destPath, replacements, createdPaths);
44
+ continue;
45
+ }
46
+
47
+ if (!entry.isFile()) {
48
+ continue;
49
+ }
50
+
51
+ let written = false;
52
+ try {
53
+ const content = fs.readFileSync(srcPath, 'utf-8');
54
+ const replaced = applyReplacements(content, replacements);
55
+ fs.writeFileSync(destPath, replaced);
56
+ written = true;
57
+ } catch {
58
+ // Fallback for non-text files
59
+ }
60
+
61
+ if (!written) {
62
+ fs.copyFileSync(srcPath, destPath);
63
+ }
64
+
65
+ createdPaths.push(destPath);
66
+ }
67
+ }
7
68
 
8
69
  /**
9
70
  * Setup a new workflow with directory structure
10
71
  */
11
- async function setup(workflowName) {
72
+ async function setup(workflowName, options = {}) {
12
73
  const workflowsDir = path.join(process.cwd(), 'workflows');
13
74
  const workflowDir = path.join(workflowsDir, workflowName);
75
+ const templateName = options.template || DEFAULT_TEMPLATE;
14
76
 
15
77
  // Check if workflow already exists
16
78
  if (fs.existsSync(workflowDir)) {
@@ -18,401 +80,33 @@ async function setup(workflowName) {
18
80
  process.exit(1);
19
81
  }
20
82
 
21
- console.log(`\nCreating workflow: ${workflowName}`);
22
- console.log('─'.repeat(40));
23
-
24
- // Create directory structure (native JS workflow only)
25
- const dirs = [
26
- workflowDir,
27
- path.join(workflowDir, 'agents'),
28
- path.join(workflowDir, 'interactions'),
29
- path.join(workflowDir, 'state'),
30
- path.join(workflowDir, 'steering'),
31
- path.join(workflowDir, 'scripts')
32
- ];
33
-
34
- dirs.forEach((dir) => {
35
- fs.mkdirSync(dir, { recursive: true });
36
- console.log(` Created: ${path.relative(process.cwd(), dir)}/`);
37
- });
38
-
39
- // Ensure this workflow folder is ESM (so workflow.js + agents/*.js can use import/export)
40
- // const workflowPkg = {
41
- // name: `workflow-${workflowName}`,
42
- // private: true,
43
- // type: 'module'
44
- // };
45
- // const workflowPkgFile = path.join(workflowDir, 'package.json');
46
- // fs.writeFileSync(workflowPkgFile, JSON.stringify(workflowPkg, null, 2));
47
- // console.log(` Created: ${path.relative(process.cwd(), workflowPkgFile)}`);
48
-
49
- // Create workflow.js (native JS format)
50
- const workflowJs = `/**
51
- /**
52
- * ${workflowName} Workflow
53
- *
54
- * Native JavaScript workflow - write normal async/await code!
55
- *
56
- * Features:
57
- * - memory object auto-persists to disk (use memory guards for idempotency)
58
- * - Use standard JS control flow (if, for, etc.)
59
- * - Interactive prompts pause and wait for user input
60
- */
61
-
62
- import { agent, memory, askHuman, parallel } from 'agent-state-machine';
63
- import { notify } from './scripts/mac-notification.js';
64
-
65
- export default async function() {
66
- console.log('Starting ${workflowName} workflow...');
67
-
68
- // Example: Get user input (saved to memory)
69
- const userLocation = await askHuman('Where do you live?');
70
- console.log('Example prompt answer:', userLocation);
71
-
72
- const userInfo = await agent('yoda-name-collector');
73
- memory.userInfo = userInfo;
74
-
75
- // Provide context
76
- // const userInfo = await agent('yoda-name-collector', { name: 'Luke' });
77
-
78
- console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
79
-
80
- // Context is provided automatically
81
- const { greeting } = await agent('yoda-greeter', { userLocation });
82
- console.log('Example agent greeting:', greeting);
83
-
84
- // Or you can provide context manually
85
- // await agent('yoda-greeter', userInfo);
86
-
87
- // Example: Parallel execution
88
- // const [a, b, c] = await parallel([
89
- // agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
90
- // agent('yoda-greeter', { name: 'uriah' }),
91
- // agent('yoda-greeter', { name: 'lucas' })
92
- // ]);
83
+ const templatesDir = getTemplatesDir();
84
+ const templateDir = path.join(templatesDir, templateName);
93
85
 
94
- // console.log('a: ' + JSON.stringify(a))
95
- // console.log('b: ' + JSON.stringify(b))
96
- // console.log('c: ' + JSON.stringify(c))
97
-
98
- notify(['${workflowName}', userInfo.name || userInfo + ' has been greeted!']);
99
-
100
- console.log('Workflow completed!');
101
- }
102
- `;
103
-
104
- const workflowFile = path.join(workflowDir, 'workflow.js');
105
- fs.writeFileSync(workflowFile, workflowJs);
106
- console.log(` Created: ${path.relative(process.cwd(), workflowFile)}`);
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
-
126
- // Create example JS agent (ESM)
127
- // Create example JS agent (ESM)
128
- const exampleAgent = `/**
129
- * Example Agent for ${workflowName}
130
- *
131
- * Agents are async functions that receive a context object and return a result.
132
- * - Context includes: persisted memory (spread), params, _steering, _config
133
- */
134
-
135
- import { llm } from 'agent-state-machine';
136
-
137
- export default async function handler(context) {
138
- console.log('[Agent: example] Processing...');
139
-
140
- // Access global steering prompt if available
141
- if (context._steering?.global) {
142
- console.log('[Agent: example] Steering loaded (' + context._steering.global.length + ' chars)');
86
+ if (!fs.existsSync(templateDir)) {
87
+ const available = listTemplates(templatesDir);
88
+ console.error(`Error: Template '${templateName}' not found.`);
89
+ if (available.length > 0) {
90
+ console.error(`Available templates: ${available.join(', ')}`);
91
+ }
92
+ process.exit(1);
143
93
  }
144
94
 
145
- // Example: Call an LLM (configure models in config.js)
146
- // const response = await llm(context, {
147
- // model: 'smart',
148
- // prompt: 'Say hello and describe what you can help with.'
149
- // });
150
- // console.log('[Agent: example] LLM response:', response.text);
151
-
152
- return {
153
- ok: true,
154
- received: Object.keys(context).filter((k) => !String(k).startsWith('_')),
155
- processedAt: new Date().toISOString()
156
- };
157
- }
158
-
159
- export const meta = {
160
- name: 'example',
161
- description: 'An example agent to get you started',
162
- version: '1.0.0'
163
- };
164
- `;
165
-
166
- const agentFile = path.join(workflowDir, 'agents', 'example.js');
167
- fs.writeFileSync(agentFile, exampleAgent);
168
- console.log(` Created: ${path.relative(process.cwd(), agentFile)}`);
169
-
170
- // Create example markdown agent
171
- const yodaGreeterAgent = `---
172
- model: low
173
- output: greeting
174
- ---
175
-
176
- # Greeting Task
177
-
178
- Generate a friendly greeting for {{name}} from {{location}} in a yoda style. Prompt user for their actual {{name}} if you dont have it.
179
-
180
- Once you have it create a yoda-greeting.md file in root dir with the greeting.
181
-
182
- You are a fast, direct worker. Do NOT investigate the codebase or read files unless strictly necessary. Perform the requested action immediately using the provided context. Avoid "thinking" steps or creating plans if the task is simple.
183
- `;
184
-
185
- const yodaNameCollectorAgent = `---
186
- model: low
187
- output: name
188
- ---
189
-
190
- # Name Collection Task
191
-
192
- Ask for users name in a yoda style. Unless you have it already.
193
-
194
- Keep it brief and warm.
195
-
196
- You are a fast, direct worker. Do NOT investigate the codebase or read files unless strictly necessary. Perform the requested action immediately using the provided context. Avoid "thinking" steps or creating plans if the task is simple.
197
- `;
198
-
199
- const yodaNameCollectorAgentFile = path.join(workflowDir, 'agents', 'yoda-name-collector.md');
200
- fs.writeFileSync(yodaNameCollectorAgentFile, yodaNameCollectorAgent);
201
-
202
- const yodaGreeterFile = path.join(workflowDir, 'agents', 'yoda-greeter.md');
203
- fs.writeFileSync(yodaGreeterFile, yodaGreeterAgent);
204
-
205
- console.log(` Created: ${path.relative(process.cwd(), yodaGreeterFile)}`);
206
- console.log(` Created: ${path.relative(process.cwd(), yodaNameCollectorAgentFile)}`);
207
-
208
- // Create initial state (native format)
209
- const initialState = {
210
- format: 'native',
211
- status: 'IDLE',
212
- memory: {},
213
- _pendingInteraction: null,
214
- _error: null,
215
- startedAt: null,
216
- lastUpdatedAt: new Date().toISOString()
217
- };
218
- const stateFile = path.join(workflowDir, 'state', 'current.json');
219
- fs.writeFileSync(stateFile, JSON.stringify(initialState, null, 2));
220
- console.log(` Created: ${path.relative(process.cwd(), stateFile)}`);
221
-
222
- // Create empty history file
223
- const historyFile = path.join(workflowDir, 'state', 'history.jsonl');
224
- fs.writeFileSync(historyFile, '');
225
- console.log(` Created: ${path.relative(process.cwd(), historyFile)}`);
95
+ console.log(`\nCreating workflow: ${workflowName}`);
96
+ console.log(`Using template: ${templateName}`);
97
+ console.log(''.repeat(40));
226
98
 
227
- // Create steering config
228
- const steeringConfig = {
229
- _comment: 'Steering configuration',
230
- enabled: true,
231
- globalPrompt: 'global.md'
99
+ const replacements = {
100
+ '__WORKFLOW_NAME__': workflowName,
101
+ '__NOW_ISO__': new Date().toISOString()
232
102
  };
233
- const steeringFile = path.join(workflowDir, 'steering', 'config.json');
234
- fs.writeFileSync(steeringFile, JSON.stringify(steeringConfig, null, 2));
235
- console.log(` Created: ${path.relative(process.cwd(), steeringFile)}`);
236
-
237
- // Create global.md steering prompt
238
- const globalMd = `# Global Steering Prompt
239
-
240
- This content is included with every agent execution in the ${workflowName} workflow.
241
-
242
- ## Guidelines
243
-
244
- - Process data carefully and validate inputs
245
- - Return well-structured JSON when applicable
246
- - Log meaningful progress messages
247
- - Handle errors gracefully
248
-
249
- ## Helpful Tips
250
- - Scripts live in /workflows/project-builder/scripts/
251
- - .gitignore includes /workflows so include ignored files when searching for a script and use \`search_file_content\` with the \`no_ignore\` flag to read the contents of script files.
252
- - Shell execution via \`run_shell_command\` is unavailable, so reproduce the script's intended side effect to fulfill the task.
253
-
254
- ## Notes
255
103
 
256
- This file is automatically loaded and passed to every agent in the workflow via \`context._steering.global\`.
257
- `;
258
- const globalMdFile = path.join(workflowDir, 'steering', 'global.md');
259
- fs.writeFileSync(globalMdFile, globalMd);
260
- console.log(` Created: ${path.relative(process.cwd(), globalMdFile)}`);
104
+ const createdPaths = [];
105
+ copyTemplateDir(templateDir, workflowDir, replacements, createdPaths);
261
106
 
262
- // Create mac-notification.js script
263
- const macNotificationScript = `"use strict";
264
-
265
- import { spawnSync } from "node:child_process";
266
- import { existsSync } from "node:fs";
267
-
268
- function escAppleScript(s) {
269
- return String(s).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
270
- }
271
-
272
- function notify(title = "Notification", message = "Everything finished!") {
273
- const script = \`display notification "\${escAppleScript(message)}" with title "\${escAppleScript(title)}"\`;
274
- spawnSync("osascript", ["-e", script], { stdio: "ignore" });
275
-
276
- const soundPath = "/System/Library/Sounds/Glass.aiff";
277
- const fallbackPath = "/System/Library/Sounds/Ping.aiff";
278
-
279
- if (existsSync(soundPath)) {
280
- spawnSync("afplay", [soundPath], { stdio: "ignore" });
281
- } else if (existsSync(fallbackPath)) {
282
- spawnSync("afplay", [fallbackPath], { stdio: "ignore" });
107
+ for (const createdPath of createdPaths) {
108
+ console.log(` Created: ${path.relative(process.cwd(), createdPath)}`);
283
109
  }
284
- }
285
-
286
- export { notify };
287
- `;
288
- const notificationFile = path.join(workflowDir, 'scripts', 'mac-notification.js');
289
- fs.writeFileSync(notificationFile, macNotificationScript);
290
- console.log(` Created: ${path.relative(process.cwd(), notificationFile)}`);
291
-
292
- // Create README
293
- const readme = `# ${workflowName}
294
-
295
- A workflow created with agent-state-machine (native JS format).
296
-
297
- ## Structure
298
-
299
- \\\`\\\`\\\`
300
- ${workflowName}/
301
- ├── workflow.js # Native JS workflow (async/await)
302
- ├── config.js # Model/API key configuration
303
- ├── package.json # Sets "type": "module" for this workflow folder
304
- ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
305
- ├── interactions/ # Human-in-the-loop inputs (created at runtime)
306
- ├── state/ # Runtime state (current.json, history.jsonl)
307
- └── steering/ # Steering configuration
308
- \\\`\\\`\\\`
309
-
310
- ## Usage
311
-
312
- Edit \`config.js\` to set models and API keys for this workflow.
313
-
314
- Run the workflow (or resume if interrupted):
315
- \\\`\\\`\\\`bash
316
- state-machine run ${workflowName}
317
- \\\`\\\`\\\`
318
-
319
- Check status:
320
- \\\`\\\`\\\`bash
321
- state-machine status ${workflowName}
322
- \\\`\\\`\\\`
323
-
324
- View history:
325
- \\\`\\\`\\\`bash
326
- state-machine history ${workflowName}
327
- \\\`\\\`\\\`
328
-
329
- View trace logs in browser with live updates:
330
- \\\`\\\`\\\`bash
331
- state-machine follow ${workflowName}
332
- \\\`\\\`\\\`
333
-
334
- Reset state (clears memory/state):
335
- \\\`\\\`\\\`bash
336
- state-machine reset ${workflowName}
337
- \\\`\\\`\\\`
338
-
339
- Hard reset (clears everything: history/interactions/memory):
340
- \\\`\\\`\\\`bash
341
- state-machine reset-hard ${workflowName}
342
- \\\`\\\`\\\`
343
-
344
- ## Writing Workflows
345
-
346
- Edit \`workflow.js\` - write normal async JavaScript:
347
-
348
- \\\`\\\`\\\`js
349
- import { agent, memory, askHuman, parallel } from 'agent-state-machine';
350
-
351
- export default async function() {
352
- console.log('Starting project-builder workflow...');
353
-
354
- // Example: Get user input (saved to memory)
355
- const userLocation = await askHuman('Where do you live?');
356
- console.log('Example prompt answer:', userLocation);
357
-
358
- const userInfo = await agent('yoda-name-collector');
359
- memory.userInfo = userInfo;
360
-
361
- // Provide context
362
- // const userInfo = await agent('yoda-name-collector', { name: 'Luke' });
363
-
364
- console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
365
-
366
- // Context is provided automatically
367
- const { greeting } = await agent('yoda-greeter', { userLocation });
368
- console.log('Example agent greeting:', greeting);
369
-
370
- // Or you can provide context manually
371
- // await agent('yoda-greeter', userInfo);
372
-
373
- // Example: Parallel execution
374
- // const [a, b, c] = await parallel([
375
- // agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
376
- // agent('yoda-greeter', { name: 'uriah' }),
377
- // agent('yoda-greeter', { name: 'lucas' })
378
- // ]);
379
-
380
- // console.log('a: ' + JSON.stringify(a))
381
- // console.log('b: ' + JSON.stringify(b))
382
- // console.log('c: ' + JSON.stringify(c))
383
-
384
- notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
385
-
386
- console.log('Workflow completed!');
387
- }
388
- \\\`\\\`\\\`
389
-
390
- ## Creating Agents
391
-
392
- **JavaScript agent** (\`agents/my-agent.js\`):
393
-
394
- \\\`\\\`\\\`js
395
- import { llm } from 'agent-state-machine';
396
-
397
- export default async function handler(context) {
398
- const response = await llm(context, { model: 'smart', prompt: 'Hello!' });
399
- return { greeting: response.text };
400
- }
401
- \\\`\\\`\\\`
402
-
403
- **Markdown agent** (\`agents/greeter.md\`):
404
-
405
- \\\`\\\`\\\`md
406
- ---
407
- model: fast
408
- output: greeting
409
- ---
410
- Generate a greeting for {{name}}.
411
- \\\`\\\`\\\`
412
- `;
413
- const readmeFile = path.join(workflowDir, 'README.md');
414
- fs.writeFileSync(readmeFile, readme);
415
- console.log(` Created: ${path.relative(process.cwd(), readmeFile)}`);
416
110
 
417
111
  console.log('─'.repeat(40));
418
112
  console.log(`\n✓ Workflow '${workflowName}' created successfully!\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.0.12",
3
+ "version": "2.0.14",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",