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 ADDED
@@ -0,0 +1,299 @@
1
+ # agent-state-machine
2
+
3
+ A workflow runner for building **linear, stateful agent workflows** in plain JavaScript.
4
+
5
+ You write normal `async/await` code. The runtime handles:
6
+ - **Auto-persisted** `memory` (saved to disk on mutation)
7
+ - **Human-in-the-loop** blocking via `initialPrompt()` or agent-driven interactions
8
+ - Local **JS agents** + **Markdown agents** (LLM-powered)
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm i agent-state-machine
16
+ ```
17
+
18
+ Global CLI:
19
+
20
+ ```bash
21
+ npm i -g agent-state-machine
22
+ ```
23
+
24
+ Requirements: Node.js >= 16.
25
+
26
+ ---
27
+
28
+ ## CLI
29
+
30
+ ```bash
31
+ state-machine --setup <workflow-name>
32
+ state-machine run <workflow-name>
33
+ state-machine resume <workflow-name>
34
+ state-machine status <workflow-name>
35
+ state-machine history <workflow-name> [limit]
36
+ state-machine reset <workflow-name>
37
+ ```
38
+
39
+ Workflows live in:
40
+
41
+ ```text
42
+ workflows/<name>/
43
+ ├── workflow.js # Native JS workflow (async/await)
44
+ ├── package.json # Sets "type": "module" for this workflow folder
45
+ ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
46
+ ├── interactions/ # Human-in-the-loop files (auto-created)
47
+ ├── state/ # current.json, history.jsonl, generated-prompt.md
48
+ └── steering/ # global.md + config.json
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Writing workflows (native JS)
54
+
55
+ ```js
56
+ /**
57
+ /**
58
+ * project-builder Workflow
59
+ *
60
+ * Native JavaScript workflow - write normal async/await code!
61
+ *
62
+ * Features:
63
+ * - memory object auto-persists to disk (use memory guards for idempotency)
64
+ * - Use standard JS control flow (if, for, etc.)
65
+ * - Interactive prompts pause and wait for user input
66
+ */
67
+
68
+ import { agent, memory, initialPrompt, parallel } from 'agent-state-machine';
69
+ import { notify } from './scripts/mac-notification.js';
70
+
71
+ // Model configuration (also supports models in a separate config export)
72
+ export const config = {
73
+ models: {
74
+ low: "gemini",
75
+ med: "codex --model gpt-5.2",
76
+ high: "claude -m claude-opus-4-20250514 -p",
77
+ },
78
+ apiKeys: {
79
+ gemini: process.env.GEMINI_API_KEY,
80
+ anthropic: process.env.ANTHROPIC_API_KEY,
81
+ openai: process.env.OPENAI_API_KEY,
82
+ }
83
+ };
84
+
85
+ export default async function() {
86
+ console.log('Starting project-builder workflow...');
87
+
88
+ // Example: Get user input (saved to memory)
89
+ const answer = await initialPrompt('Where do you live?');
90
+ console.log('Example prompt answer:', answer);
91
+
92
+ const userInfo = await agent('yoda-name-collector');
93
+ memory.userInfo = userInfo;
94
+
95
+ // Provide context
96
+ // const userInfo = await agent('yoda-name-collector', { name: 'Luke' });
97
+
98
+ console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
99
+
100
+ // Context is provided automatically
101
+ const { greeting } = await agent('yoda-greeter');
102
+ console.log('Example agent greeting:', greeting);
103
+
104
+ // Or you can provide context manually
105
+ // await agent('yoda-greeter', userInfo);
106
+
107
+ // Example: Parallel execution
108
+ // const [a, b] = await parallel([
109
+ // agent('example', { which: 'a' }),
110
+ // agent('example', { which: 'b' })
111
+ // ]);
112
+
113
+ notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
114
+
115
+ console.log('Workflow completed!');
116
+ }
117
+ ```
118
+
119
+ ### How “resume” works
120
+
121
+ `resume` restarts your workflow from the top.
122
+
123
+ If the workflow needs human input, it will **block inline** in the terminal. You’ll be told which `interactions/<slug>.md` file to edit; after you fill it in, press `y` in the same terminal session to continue.
124
+
125
+ If the process is interrupted, running `state-machine resume <workflow-name>` will restart the execution. Use the `memory` object to store and skip work manually if needed.
126
+
127
+ ---
128
+
129
+ ## Core API
130
+
131
+ ### `agent(name, params?)`
132
+
133
+ Runs `workflows/<name>/agents/<agent>.(js|mjs|cjs)` or `<agent>.md`.
134
+
135
+ ```js
136
+ const out = await agent('review', { file: 'src/app.js' });
137
+ memory.lastReview = out;
138
+ ```
139
+
140
+ ### `memory`
141
+
142
+ A persisted object for your workflow.
143
+
144
+ - Mutations auto-save to `workflows/<name>/state/current.json`.
145
+ - Use it as your “long-lived state” between runs.
146
+
147
+ ```js
148
+ memory.count = (memory.count || 0) + 1;
149
+ ```
150
+
151
+ ### `initialPrompt(question, options?)`
152
+
153
+ Gets user input.
154
+
155
+ - In a TTY, it prompts in the terminal.
156
+ - Otherwise it creates `interactions/<slug>.md` and blocks until you confirm in the terminal.
157
+
158
+ ```js
159
+ const repo = await initialPrompt('What repo should I work on?', { slug: 'repo' });
160
+ memory.repo = repo;
161
+ ```
162
+
163
+ ### `parallel([...])` / `parallelLimit([...], limit)`
164
+
165
+ Run multiple `agent()` calls concurrently:
166
+
167
+ ```js
168
+ import { agent, parallel, parallelLimit } from 'agent-state-machine';
169
+
170
+ const [a, b] = await parallel([
171
+ agent('review', { file: 'src/a.js' }),
172
+ agent('review', { file: 'src/b.js' }),
173
+ ]);
174
+
175
+ const results = await parallelLimit(
176
+ ['a.js', 'b.js', 'c.js'].map(f => agent('review', { file: f })),
177
+ 2
178
+ );
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Agents
184
+
185
+ Agents live in `workflows/<workflow>/agents/`.
186
+
187
+ ### JavaScript agents
188
+
189
+ **ESM (`.js` / `.mjs`)**:
190
+
191
+ ```js
192
+ // workflows/<name>/agents/example.js
193
+ import { llm } from 'agent-state-machine';
194
+
195
+ export default async function handler(context) {
196
+ // context includes:
197
+ // - persisted memory (spread into the object)
198
+ // - params passed to agent(name, params)
199
+ // - context._steering (global steering prompt/config)
200
+ // - context._config (models/apiKeys/workflowDir)
201
+ return { ok: true };
202
+ }
203
+ ```
204
+
205
+ **CommonJS (`.cjs`)** (only if you prefer CJS):
206
+
207
+ ```js
208
+ // workflows/<name>/agents/example.cjs
209
+ async function handler(context) {
210
+ return { ok: true };
211
+ }
212
+
213
+ module.exports = handler;
214
+ module.exports.handler = handler;
215
+ ```
216
+
217
+ If you need to request human input from a JS agent, return an `_interaction` payload:
218
+
219
+ ```js
220
+ return {
221
+ _interaction: {
222
+ slug: 'approval',
223
+ targetKey: 'approval',
224
+ content: 'Please approve this change (yes/no).'
225
+ }
226
+ };
227
+ ```
228
+
229
+ The runtime will block execution and wait for your response in the terminal.
230
+
231
+ ### Markdown agents (`.md`)
232
+
233
+ Markdown agents are LLM-backed prompt templates with optional frontmatter.
234
+
235
+ ```md
236
+ ---
237
+ model: smart
238
+ output: greeting
239
+ ---
240
+ Generate a friendly greeting for {{name}}.
241
+ ```
242
+
243
+ Calling it:
244
+
245
+ ```js
246
+ const { greeting } = await agent('greeter', { name: 'Sam' });
247
+ memory.greeting = greeting;
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Models & LLM execution
253
+
254
+ In your workflow’s `export const config = { models: { ... } }`, each model value can be:
255
+
256
+ ### CLI command
257
+
258
+ ```js
259
+ export const config = {
260
+ models: {
261
+ smart: "claude -m claude-sonnet-4-20250514 -p"
262
+ }
263
+ };
264
+ ```
265
+
266
+ ### API target
267
+
268
+ Format: `api:<provider>:<model>`
269
+
270
+ ```js
271
+ export const config = {
272
+ models: {
273
+ smart: "api:openai:gpt-4.1-mini"
274
+ },
275
+ apiKeys: {
276
+ openai: process.env.OPENAI_API_KEY
277
+ }
278
+ };
279
+ ```
280
+
281
+ The runtime writes the fully-built prompt to:
282
+
283
+ ```text
284
+ workflows/<name>/state/generated-prompt.md
285
+ ```
286
+
287
+ ---
288
+
289
+ ## State & persistence
290
+
291
+ Native JS workflows persist to:
292
+
293
+ - `workflows/<name>/state/current.json` — status, memory, pending interaction
294
+ - `workflows/<name>/state/history.jsonl` — event log (newest entries first)
295
+ - `workflows/<name>/interactions/*.md` — human input files (when paused)
296
+
297
+ ## License
298
+
299
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { pathToFileURL } from 'url';
6
+ import { WorkflowRuntime } from '../lib/index.js';
7
+ import { setup } from '../lib/setup.js';
8
+
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+
12
+ function printHelp() {
13
+ console.log(`
14
+ Agent State Machine CLI (Native JS Workflows Only)
15
+
16
+ Usage:
17
+ state-machine --setup <workflow-name> Create a new workflow project
18
+ state-machine run <workflow-name> Run a workflow from the beginning
19
+ state-machine resume <workflow-name> Resume a paused workflow
20
+ state-machine status [workflow-name] Show current state (or list all)
21
+ state-machine history <workflow-name> [limit] Show execution history
22
+ state-machine reset <workflow-name> Reset workflow state
23
+ state-machine reset-hard <workflow-name> Hard reset (clear history/interactions)
24
+ state-machine list List all workflows
25
+ state-machine help Show this help
26
+
27
+ Options:
28
+ --setup, -s Initialize a new workflow with directory structure
29
+ --help, -h Show help
30
+
31
+ Workflow Structure:
32
+ workflows/<name>/
33
+ ├── workflow.js # Native JS workflow (async/await)
34
+ ├── package.json # Sets "type": "module" for this workflow folder
35
+ ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
36
+ ├── interactions/ # Human-in-the-loop files (auto-created)
37
+ ├── state/ # current.json, history.jsonl, generated-prompt.md
38
+ └── steering/ # global.md + config.json
39
+ `);
40
+ }
41
+
42
+ function workflowsRoot() {
43
+ return path.join(process.cwd(), 'workflows');
44
+ }
45
+
46
+ function resolveWorkflowDir(workflowName) {
47
+ return path.join(workflowsRoot(), workflowName);
48
+ }
49
+
50
+ function resolveWorkflowEntry(workflowDir) {
51
+ const candidates = ['workflow.js', 'workflow.mjs'];
52
+ for (const f of candidates) {
53
+ const p = path.join(workflowDir, f);
54
+ if (fs.existsSync(p)) return p;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function readState(workflowDir) {
60
+ const stateFile = path.join(workflowDir, 'state', 'current.json');
61
+ if (!fs.existsSync(stateFile)) return null;
62
+ try {
63
+ return JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function summarizeStatus(state) {
70
+ if (!state) return ' [no state]';
71
+
72
+ const s = String(state.status || '').toUpperCase();
73
+ if (s === 'COMPLETED') return ' [completed]';
74
+ if (s === 'FAILED') return ' [failed - can resume]';
75
+ if (s === 'PAUSED') return ' [paused - can resume]';
76
+ if (s === 'RUNNING') return ' [running]';
77
+ if (s === 'IDLE') return ' [idle]';
78
+ return state.status ? ` [${state.status}]` : '';
79
+ }
80
+
81
+ function listWorkflows() {
82
+ const root = workflowsRoot();
83
+
84
+ if (!fs.existsSync(root)) {
85
+ console.log('No workflows directory found.');
86
+ console.log('Run `state-machine --setup <name>` to create your first workflow.');
87
+ return;
88
+ }
89
+
90
+ const workflows = fs
91
+ .readdirSync(root, { withFileTypes: true })
92
+ .filter((d) => d.isDirectory())
93
+ .map((d) => d.name)
94
+ .sort((a, b) => a.localeCompare(b));
95
+
96
+ if (workflows.length === 0) {
97
+ console.log('No workflows found.');
98
+ console.log('Run `state-machine --setup <name>` to create your first workflow.');
99
+ return;
100
+ }
101
+
102
+ console.log('\nAvailable Workflows:');
103
+ console.log('─'.repeat(40));
104
+
105
+ for (const name of workflows) {
106
+ const dir = resolveWorkflowDir(name);
107
+ const entry = resolveWorkflowEntry(dir);
108
+ const state = readState(dir);
109
+
110
+ const entryNote = entry ? '' : ' [missing workflow.js]';
111
+ const statusNote = summarizeStatus(state);
112
+
113
+ const pausedNote =
114
+ state && state._pendingInteraction && state._pendingInteraction.file
115
+ ? ` [needs input: ${state._pendingInteraction.file}]`
116
+ : '';
117
+
118
+ console.log(` ${name}${entryNote}${statusNote}${pausedNote}`);
119
+ }
120
+
121
+ console.log('');
122
+ }
123
+
124
+ async function runOrResume(workflowName) {
125
+ const workflowDir = resolveWorkflowDir(workflowName);
126
+
127
+ if (!fs.existsSync(workflowDir)) {
128
+ console.error(`Error: Workflow '${workflowName}' not found at ${workflowDir}`);
129
+ console.error(`Run: state-machine --setup ${workflowName}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ const entry = resolveWorkflowEntry(workflowDir);
134
+ if (!entry) {
135
+ console.error(`Error: No workflow entry found (expected workflow.js or workflow.mjs) in ${workflowDir}`);
136
+ process.exit(1);
137
+ }
138
+
139
+ const runtime = new WorkflowRuntime(workflowDir);
140
+ const workflowUrl = pathToFileURL(entry).href;
141
+
142
+ await runtime.runWorkflow(workflowUrl);
143
+ }
144
+
145
+ async function main() {
146
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
147
+ printHelp();
148
+ process.exit(0);
149
+ }
150
+
151
+ if (command === '--setup' || command === '-s') {
152
+ const workflowName = args[1];
153
+ if (!workflowName) {
154
+ console.error('Error: Workflow name required');
155
+ console.error('Usage: state-machine --setup <workflow-name>');
156
+ process.exit(1);
157
+ }
158
+ await setup(workflowName);
159
+ process.exit(0);
160
+ }
161
+
162
+ const workflowName = args[1];
163
+
164
+ switch (command) {
165
+ case 'run':
166
+ case 'resume':
167
+ if (!workflowName) {
168
+ console.error('Error: Workflow name required');
169
+ console.error(`Usage: state-machine ${command} <workflow-name>`);
170
+ process.exit(1);
171
+ }
172
+ try {
173
+ await runOrResume(workflowName);
174
+ } catch (err) {
175
+ console.error('Error:', err.message || String(err));
176
+ process.exit(1);
177
+ }
178
+ break;
179
+
180
+ case 'status':
181
+ if (!workflowName) {
182
+ listWorkflows();
183
+ break;
184
+ }
185
+ {
186
+ const workflowDir = resolveWorkflowDir(workflowName);
187
+ const runtime = new WorkflowRuntime(workflowDir);
188
+ runtime.showStatus();
189
+ }
190
+ break;
191
+
192
+ case 'history':
193
+ if (!workflowName) {
194
+ console.error('Error: Workflow name required');
195
+ console.error('Usage: state-machine history <workflow-name> [limit]');
196
+ process.exit(1);
197
+ }
198
+ {
199
+ const limit = parseInt(args[2], 10) || 20;
200
+ const workflowDir = resolveWorkflowDir(workflowName);
201
+ const runtime = new WorkflowRuntime(workflowDir);
202
+ runtime.showHistory(limit);
203
+ }
204
+ break;
205
+
206
+ case 'reset':
207
+ if (!workflowName) {
208
+ console.error('Error: Workflow name required');
209
+ console.error('Usage: state-machine reset <workflow-name>');
210
+ process.exit(1);
211
+ }
212
+ {
213
+ const workflowDir = resolveWorkflowDir(workflowName);
214
+ const runtime = new WorkflowRuntime(workflowDir);
215
+ runtime.reset();
216
+ }
217
+ break;
218
+
219
+ case 'reset-hard':
220
+ if (!workflowName) {
221
+ console.error('Error: Workflow name required');
222
+ console.error('Usage: state-machine reset-hard <workflow-name>');
223
+ process.exit(1);
224
+ }
225
+ {
226
+ const workflowDir = resolveWorkflowDir(workflowName);
227
+ const runtime = new WorkflowRuntime(workflowDir);
228
+ runtime.resetHard();
229
+ }
230
+ break;
231
+
232
+ case 'list':
233
+ listWorkflows();
234
+ break;
235
+
236
+ default:
237
+ console.error(`Unknown command: ${command}`);
238
+ printHelp();
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ main().catch((err) => {
244
+ console.error('Fatal error:', err);
245
+ process.exit(1);
246
+ });
package/lib/index.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * File: /lib/index.js
3
+ *
4
+ * Public API (native JS workflows only)
5
+ */
6
+
7
+ import { setup } from './setup.js';
8
+ import { llm, llmText, llmJSON, parseJSON, detectAvailableCLIs } from './llm.js';
9
+
10
+ import {
11
+ WorkflowRuntime,
12
+ agent,
13
+ executeAgent,
14
+ initialPrompt,
15
+ parallel,
16
+ parallelLimit,
17
+ getMemory,
18
+ getCurrentRuntime
19
+ } from './runtime/index.js';
20
+
21
+ /**
22
+ * Live memory proxy:
23
+ * - Reads/writes always target the *current* workflow runtime's memory proxy
24
+ * - Throws on writes if used outside a workflow run (prevents silent no-op)
25
+ */
26
+ export const memory = new Proxy(
27
+ {},
28
+ {
29
+ get(_target, prop) {
30
+ const runtime = getCurrentRuntime();
31
+ if (!runtime) return undefined;
32
+ return runtime.memory[prop];
33
+ },
34
+ set(_target, prop, value) {
35
+ const runtime = getCurrentRuntime();
36
+ if (!runtime) {
37
+ throw new Error('memory can only be mutated within a running workflow');
38
+ }
39
+ runtime.memory[prop] = value;
40
+ return true;
41
+ },
42
+ deleteProperty(_target, prop) {
43
+ const runtime = getCurrentRuntime();
44
+ if (!runtime) {
45
+ throw new Error('memory can only be mutated within a running workflow');
46
+ }
47
+ delete runtime.memory[prop];
48
+ return true;
49
+ },
50
+ has(_target, prop) {
51
+ const runtime = getCurrentRuntime();
52
+ if (!runtime) return false;
53
+ const raw = runtime.memory?._raw || runtime._rawMemory || {};
54
+ return prop in raw;
55
+ },
56
+ ownKeys() {
57
+ const runtime = getCurrentRuntime();
58
+ if (!runtime) return [];
59
+ const raw = runtime.memory?._raw || runtime._rawMemory || {};
60
+ return Reflect.ownKeys(raw);
61
+ },
62
+ getOwnPropertyDescriptor(_target, prop) {
63
+ const runtime = getCurrentRuntime();
64
+ if (!runtime) return undefined;
65
+ const raw = runtime.memory?._raw || runtime._rawMemory || {};
66
+ if (!(prop in raw)) return undefined;
67
+ return {
68
+ enumerable: true,
69
+ configurable: true
70
+ };
71
+ }
72
+ }
73
+ );
74
+
75
+ export {
76
+ setup,
77
+ llm,
78
+ llmText,
79
+ llmJSON,
80
+ parseJSON,
81
+ detectAvailableCLIs,
82
+ WorkflowRuntime,
83
+ agent,
84
+ executeAgent,
85
+ initialPrompt,
86
+ parallel,
87
+ parallelLimit,
88
+ getCurrentRuntime,
89
+ getMemory
90
+ };
91
+
92
+ const api = {
93
+ setup,
94
+ llm,
95
+ llmText,
96
+ llmJSON,
97
+ parseJSON,
98
+ detectAvailableCLIs,
99
+ WorkflowRuntime,
100
+ agent,
101
+ executeAgent,
102
+ initialPrompt,
103
+ parallel,
104
+ parallelLimit,
105
+ getCurrentRuntime,
106
+ getMemory,
107
+ memory
108
+ };
109
+
110
+ export default api;
package/lib/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * File: /lib/index.mjs
3
+ *
4
+ * ESM re-export shim
5
+ */
6
+
7
+ export * from './index.js';
8
+ import * as api from './index.js';
9
+ export default api;