agent-state-machine 1.1.0 → 1.3.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 CHANGED
@@ -44,11 +44,11 @@ Requirements: Node.js >= 16.
44
44
  ```bash
45
45
  state-machine --setup <workflow-name>
46
46
  state-machine run <workflow-name>
47
- state-machine resume <workflow-name>
48
- state-machine status <workflow-name>
47
+
48
+ state-machine follow <workflow-name> (view prompt trace history in browser with live updates)
49
49
  state-machine history <workflow-name> [limit]
50
- state-machine trace-logs <workflow-name>
51
- state-machine reset <workflow-name>
50
+ state-machine reset <workflow-name> (clears memory/state)
51
+ state-machine reset-hard <workflow-name> (clears everything: history/interactions/memory)
52
52
  ```
53
53
 
54
54
  Workflows live in:
@@ -120,24 +120,29 @@ export default async function() {
120
120
  // await agent('yoda-greeter', userInfo);
121
121
 
122
122
  // Example: Parallel execution
123
- // const [a, b] = await parallel([
124
- // agent('example', { which: 'a' }),
125
- // agent('example', { which: 'b' })
123
+ // const [a, b, c] = await parallel([
124
+ // agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
125
+ // agent('yoda-greeter', { name: 'uriah' }),
126
+ // agent('yoda-greeter', { name: 'lucas' })
126
127
  // ]);
127
128
 
129
+ // console.log('a: ' + JSON.stringify(a))
130
+ // console.log('b: ' + JSON.stringify(b))
131
+ // console.log('c: ' + JSON.stringify(c))
132
+
128
133
  notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
129
134
 
130
135
  console.log('Workflow completed!');
131
136
  }
132
137
  ```
133
138
 
134
- ### How “resume” works
139
+ ### Resuming workflows
135
140
 
136
- `resume` restarts your workflow from the top.
141
+ `state-machine run` restarts your workflow from the top, loading the persisted state.
137
142
 
138
143
  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.
139
144
 
140
- 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.
145
+ If the process is interrupted, running `state-machine run <workflow-name>` again will continue execution (assuming your workflow uses `memory` to skip completed steps).
141
146
 
142
147
  ---
143
148
 
@@ -293,10 +298,10 @@ export const config = {
293
298
  };
294
299
  ```
295
300
 
296
- The runtime captures the fully-built prompt in `state/history.jsonl`, viewable via:
301
+ The runtime captures the fully-built prompt in `state/history.jsonl`, viewable in the browser with live updates via:
297
302
 
298
303
  ```bash
299
- state-machine trace-logs <workflow-name>
304
+ state-machine follow <workflow-name>
300
305
  ```
301
306
 
302
307
  ---
package/bin/cli.js CHANGED
@@ -29,13 +29,12 @@ Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
29
29
 
30
30
  Usage:
31
31
  state-machine --setup <workflow-name> Create a new workflow project
32
- state-machine run <workflow-name> Run a workflow from the beginning
33
- state-machine resume <workflow-name> Resume a paused workflow
32
+ state-machine run <workflow-name> Run a workflow (loads existing state)
33
+ state-machine follow <workflow-name> View prompt trace history in browser with live updates
34
34
  state-machine status [workflow-name] Show current state (or list all)
35
- state-machine history <workflow-name> [limit] Show execution history
36
- state-machine trace-logs <workflow-name> View prompt trace history in browser
37
- state-machine reset <workflow-name> Reset workflow state
38
- state-machine reset-hard <workflow-name> Hard reset (clear history/interactions)
35
+ state-machine history <workflow-name> [limit] Show execution history logs
36
+ state-machine reset <workflow-name> Reset workflow state (clears memory/state)
37
+ state-machine reset-hard <workflow-name> Hard reset (clears everything: history/interactions/memory)
39
38
  state-machine list List all workflows
40
39
  state-machine help Show this help
41
40
 
@@ -184,7 +183,6 @@ async function main() {
184
183
 
185
184
  switch (command) {
186
185
  case 'run':
187
- case 'resume':
188
186
  if (!workflowName) {
189
187
  console.error('Error: Workflow name required');
190
188
  console.error(`Usage: state-machine ${command} <workflow-name>`);
@@ -224,10 +222,10 @@ async function main() {
224
222
  }
225
223
  break;
226
224
 
227
- case 'trace-logs':
225
+ case 'follow':
228
226
  if (!workflowName) {
229
227
  console.error('Error: Workflow name required');
230
- console.error('Usage: state-machine trace-logs <workflow-name>');
228
+ console.error('Usage: state-machine follow <workflow-name>');
231
229
  process.exit(1);
232
230
  }
233
231
  {
@@ -356,7 +356,8 @@ Enter your response below:
356
356
  runtime.prependHistory({
357
357
  event: 'INTERACTION_REQUESTED',
358
358
  slug,
359
- targetKey
359
+ targetKey,
360
+ question: content
360
361
  });
361
362
 
362
363
  // Block and wait for user input (instead of throwing)
@@ -71,7 +71,8 @@ Enter your response below:
71
71
  event: 'INTERACTION_REQUESTED',
72
72
  slug,
73
73
  targetKey: memoryKey,
74
- file: interactionFile
74
+ file: interactionFile,
75
+ question
75
76
  });
76
77
 
77
78
  // Block and wait for user input (instead of throwing)
package/lib/setup.js CHANGED
@@ -99,11 +99,16 @@ export default async function() {
99
99
  // await agent('yoda-greeter', userInfo);
100
100
 
101
101
  // Example: Parallel execution
102
- // const [a, b] = await parallel([
103
- // agent('example', { which: 'a' }),
104
- // agent('example', { which: 'b' })
102
+ // const [a, b, c] = await parallel([
103
+ // agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
104
+ // agent('yoda-greeter', { name: 'uriah' }),
105
+ // agent('yoda-greeter', { name: 'lucas' })
105
106
  // ]);
106
107
 
108
+ // console.log('a: ' + JSON.stringify(a))
109
+ // console.log('b: ' + JSON.stringify(b))
110
+ // console.log('c: ' + JSON.stringify(c))
111
+
107
112
  notify(['${workflowName}', userInfo.name || userInfo + ' has been greeted!']);
108
113
 
109
114
  console.log('Workflow completed!');
@@ -173,7 +178,7 @@ Once you have it create a yoda-greeting.md file in root dir with the greeting.
173
178
  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.
174
179
  `;
175
180
 
176
- const yodaNameCollectorAgent = `---
181
+ const yodaNameCollectorAgent = `---
177
182
  model: low
178
183
  output: name
179
184
  ---
@@ -294,16 +299,11 @@ ${workflowName}/
294
299
 
295
300
  ## Usage
296
301
 
297
- Run the workflow:
302
+ Run the workflow (or resume if interrupted):
298
303
  \\\`\\\`\\\`bash
299
304
  state-machine run ${workflowName}
300
305
  \\\`\\\`\\\`
301
306
 
302
- Resume a paused workflow:
303
- \\\`\\\`\\\`bash
304
- state-machine resume ${workflowName}
305
- \\\`\\\`\\\`
306
-
307
307
  Check status:
308
308
  \\\`\\\`\\\`bash
309
309
  state-machine status ${workflowName}
@@ -314,16 +314,21 @@ View history:
314
314
  state-machine history ${workflowName}
315
315
  \\\`\\\`\\\`
316
316
 
317
- View trace logs in browser:
317
+ View trace logs in browser with live updates:
318
318
  \\\`\\\`\\\`bash
319
- state-machine trace-logs ${workflowName}
319
+ state-machine follow ${workflowName}
320
320
  \\\`\\\`\\\`
321
321
 
322
- Reset state:
322
+ Reset state (clears memory/state):
323
323
  \\\`\\\`\\\`bash
324
324
  state-machine reset ${workflowName}
325
325
  \\\`\\\`\\\`
326
326
 
327
+ Hard reset (clears everything: history/interactions/memory):
328
+ \\\`\\\`\\\`bash
329
+ state-machine reset-hard ${workflowName}
330
+ \\\`\\\`\\\`
331
+
327
332
  ## Writing Workflows
328
333
 
329
334
  Edit \`workflow.js\` - write normal async JavaScript:
@@ -354,11 +359,16 @@ export default async function() {
354
359
  // await agent('yoda-greeter', userInfo);
355
360
 
356
361
  // Example: Parallel execution
357
- // const [a, b] = await parallel([
358
- // agent('example', { which: 'a' }),
359
- // agent('example', { which: 'b' })
362
+ // const [a, b, c] = await parallel([
363
+ // agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
364
+ // agent('yoda-greeter', { name: 'uriah' }),
365
+ // agent('yoda-greeter', { name: 'lucas' })
360
366
  // ]);
361
367
 
368
+ // console.log('a: ' + JSON.stringify(a))
369
+ // console.log('b: ' + JSON.stringify(b))
370
+ // console.log('c: ' + JSON.stringify(c))
371
+
362
372
  notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
363
373
 
364
374
  console.log('Workflow completed!');
package/lib/ui/index.html CHANGED
@@ -41,20 +41,40 @@
41
41
  </svg>
42
42
  );
43
43
 
44
- const SortDescIcon = () => (
45
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
44
+ const CopyIcon = () => (
45
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
47
47
  </svg>
48
48
  );
49
49
 
50
- const SortAscIcon = () => (
51
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
52
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h5m4 0l4-4m0 0l4 4m-4-4v12" transform="scale(1, -1) translate(0, -24)" />
53
- {/* Simple Down Arrow for Newest Top */}
54
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
50
+ const CheckIcon = () => (
51
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
52
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
55
53
  </svg>
56
54
  );
57
55
 
56
+ function CopyButton({ text, className }) {
57
+ const [copied, setCopied] = useState(false);
58
+
59
+ const handleCopy = () => {
60
+ const content = typeof text === 'object' ? JSON.stringify(text, null, 2) : text;
61
+ navigator.clipboard.writeText(content);
62
+ setCopied(true);
63
+ setTimeout(() => setCopied(false), 2000);
64
+ };
65
+
66
+ return (
67
+ <button
68
+ onClick={handleCopy}
69
+ className={`flex items-center space-x-1 text-[9px] uppercase tracking-wider transition-colors hover:text-blue-500 focus:outline-none ${className}`}
70
+ title="Copy to clipboard"
71
+ >
72
+ {copied ? <CheckIcon /> : <CopyIcon />}
73
+ <span>{copied ? 'Copied' : 'Copy'}</span>
74
+ </button>
75
+ );
76
+ }
77
+
58
78
  function App() {
59
79
  const [history, setHistory] = useState([]);
60
80
  const [loading, setLoading] = useState(true);
@@ -118,14 +138,9 @@
118
138
  </div>
119
139
  );
120
140
 
121
- // Filter for events we want to display
122
- let visibleEvents = history.filter(item =>
123
- [
124
- 'WORKFLOW_STARTED', 'WORKFLOW_COMPLETED', 'WORKFLOW_FAILED', 'WORKFLOW_RESET',
125
- 'AGENT_STARTED', 'AGENT_COMPLETED', 'AGENT_FAILED',
126
- 'INTERACTION_REQUESTED', 'INTERACTION_RESOLVED'
127
- ].includes(item.event)
128
- );
141
+ // Filter for events we want to display - NOW INCLUDES EVERYTHING
142
+ // We only filter out nulls or malformed entries if any
143
+ let visibleEvents = history;
129
144
 
130
145
  // Apply Sort
131
146
  // History from API is "Newest First" (index 0 is latest)
@@ -222,19 +237,40 @@
222
237
  );
223
238
  }
224
239
 
225
- // 4. Interaction Requested
226
- if (item.event === 'INTERACTION_REQUESTED') {
240
+ // 4. Interaction / Prompt Requested
241
+ if (item.event === 'INTERACTION_REQUESTED' || item.event === 'PROMPT_REQUESTED') {
242
+ const isPrompt = item.event === 'PROMPT_REQUESTED';
227
243
  return (
228
244
  <div key={idx} className="flex justify-center">
229
245
  <div className="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-dashed rounded-lg px-6 py-4 text-center max-w-md w-full">
230
- <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">Human Intervention Needed</div>
231
- <div className="text-xs text-zinc-600 dark:text-zinc-400 italic">Waiting for response to "{item.slug}"...</div>
246
+ <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
247
+ {isPrompt ? 'User Input Requested' : 'Human Intervention Needed'}
248
+ </div>
249
+ <div className="text-xs text-zinc-600 dark:text-zinc-400 italic">
250
+ {item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"...`}
251
+ </div>
232
252
  </div>
233
253
  </div>
234
254
  );
235
255
  }
236
256
 
237
- // 5. Agent Completed / Interaction Resolved (The Bubbles)
257
+ // 5. Prompt Answered
258
+ if (item.event === 'PROMPT_ANSWERED') {
259
+ return (
260
+ <div key={idx} className="flex justify-center">
261
+ <div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900/50 rounded-lg px-4 py-2 text-center max-w-md w-full">
262
+ <div className="text-[10px] text-green-600 dark:text-green-400 uppercase font-bold tracking-widest mb-1">
263
+ User Answered
264
+ </div>
265
+ <div className="text-xs text-green-700 dark:text-green-300 italic font-medium">
266
+ "{item.answer}"
267
+ </div>
268
+ </div>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ // 6. Agent Completed / Interaction Resolved (The Bubbles)
238
274
  if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
239
275
  return (
240
276
  <div key={idx} className="flex flex-col space-y-4">
@@ -250,8 +286,13 @@
250
286
  {/* Output (Response) - NOW ON TOP */}
251
287
  {(item.output || item.result) && (
252
288
  <div className="flex justify-end w-full group">
253
- <div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800">
254
- <div className="text-[9px] font-black text-blue-300 dark:text-blue-800/60 mb-3 uppercase tracking-[0.2em] text-right">Output / Response</div>
289
+ <div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800 relative">
290
+ <div className="flex justify-between items-center mb-3">
291
+ <div className="text-[9px] font-black text-blue-300 dark:text-blue-800/60 uppercase tracking-[0.2em] text-right w-full">Output / Response</div>
292
+ <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
293
+ <CopyButton text={item.output || item.result} className="text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400" />
294
+ </div>
295
+ </div>
255
296
  <div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
256
297
  {typeof item.output === 'object' ? JSON.stringify(item.output, null, 2) : (item.output || item.result)}
257
298
  </div>
@@ -262,8 +303,13 @@
262
303
  {/* Prompt (Input) - NOW ON BOTTOM */}
263
304
  {item.prompt && (
264
305
  <div className="flex justify-start w-full group">
265
- <div className="max-w-[85%] bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl rounded-tl-none shadow-sm p-6 transition-all hover:border-zinc-300 dark:hover:border-zinc-700">
266
- <div className="text-[9px] font-black text-zinc-300 dark:text-zinc-700 mb-3 uppercase tracking-[0.2em]">Prompt / Input</div>
306
+ <div className="max-w-[85%] bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl rounded-tl-none shadow-sm p-6 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 relative">
307
+ <div className="flex justify-between items-center mb-3">
308
+ <div className="text-[9px] font-black text-zinc-300 dark:text-zinc-700 uppercase tracking-[0.2em]">Prompt / Input</div>
309
+ <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
310
+ <CopyButton text={item.prompt} className="text-gray-400 hover:text-gray-600 dark:text-zinc-600 dark:hover:text-zinc-400" />
311
+ </div>
312
+ </div>
267
313
  <div className="markdown-body text-gray-800 dark:text-zinc-300 text-sm overflow-x-auto leading-relaxed">
268
314
  {item.prompt}
269
315
  </div>
@@ -274,7 +320,26 @@
274
320
  );
275
321
  }
276
322
 
277
- return null;
323
+ // 7. CATCH-ALL for Unknown Events
324
+ // If we made it here, it's an event we didn't explicitly handle.
325
+ // Render it generically.
326
+ return (
327
+ <div key={idx} className="flex justify-center px-4">
328
+ <div className="bg-gray-100 dark:bg-zinc-900/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-4 py-3 text-xs w-full max-w-2xl font-mono overflow-x-auto">
329
+ <div className="text-[9px] text-gray-400 dark:text-zinc-600 uppercase tracking-widest mb-1 flex justify-between">
330
+ <span>{item.event}</span>
331
+ <span>{formatTime(item.timestamp)}</span>
332
+ </div>
333
+ <div className="text-gray-600 dark:text-zinc-400 whitespace-pre-wrap">
334
+ {JSON.stringify(item, (key, value) => {
335
+ // Exclude redundant keys to keep it clean
336
+ if (key === 'event' || key === 'timestamp') return undefined;
337
+ return value;
338
+ }, 2).replace(/^{|}$/g, '').trim()}
339
+ </div>
340
+ </div>
341
+ </div>
342
+ );
278
343
  })}
279
344
  </div>
280
345
 
@@ -291,4 +356,4 @@
291
356
  root.render(<App />);
292
357
  </script>
293
358
  </body>
294
- </html>
359
+ </html>
package/lib/ui/server.js CHANGED
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = path.dirname(__filename);
12
12
 
13
- export function startServer(workflowDir, port = 3000) {
13
+ export function startServer(workflowDir, initialPort = 3000) {
14
14
  const clients = new Set();
15
15
  const stateDir = path.join(workflowDir, 'state');
16
16
 
@@ -45,7 +45,8 @@ export function startServer(workflowDir, port = 3000) {
45
45
  console.warn('Warning: Failed to setup file watcher:', err.message);
46
46
  }
47
47
 
48
- const server = http.createServer((req, res) => {
48
+ // Request Handler
49
+ const requestHandler = (req, res) => {
49
50
  // Serve the main HTML page
50
51
  if (req.url === '/' || req.url === '/index.html') {
51
52
  const htmlPath = path.join(__dirname, 'index.html');
@@ -115,11 +116,35 @@ export function startServer(workflowDir, port = 3000) {
115
116
  // 404
116
117
  res.writeHead(404);
117
118
  res.end('Not found');
118
- });
119
-
120
- server.listen(port, () => {
121
- console.log(`\n> Trace Logs running at http://localhost:${port}`);
122
- console.log(`> Viewing history for: ${workflowDir}`);
123
- console.log(`> Press Ctrl+C to stop`);
124
- });
125
- }
119
+ };
120
+
121
+ // Port hunting logic
122
+ let port = initialPort;
123
+ const maxPort = initialPort + 100; // Try up to 100 ports
124
+
125
+ const attemptServer = () => {
126
+ const server = http.createServer(requestHandler);
127
+
128
+ server.on('error', (e) => {
129
+ if (e.code === 'EADDRINUSE') {
130
+ if (port < maxPort) {
131
+ console.log(`Port ${port} is in use, trying ${port + 1}...`);
132
+ port++;
133
+ attemptServer();
134
+ } else {
135
+ console.error(`Error: Could not find an open port between ${initialPort} and ${maxPort}.`);
136
+ }
137
+ } else {
138
+ console.error('Server error:', e);
139
+ }
140
+ });
141
+
142
+ server.listen(port, () => {
143
+ console.log(`\n> Follow UI running at http://localhost:${port}`);
144
+ console.log(`> Viewing history for: ${workflowDir}`);
145
+ console.log(`> Press Ctrl+C to stop`);
146
+ });
147
+ };
148
+
149
+ attemptServer();
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
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",