agent-state-machine 1.0.2 → 1.1.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
@@ -11,14 +11,28 @@ You write normal `async/await` code. The runtime handles:
11
11
 
12
12
  ## Install
13
13
 
14
+ You need to install the package **globally** to get the CLI, and **locally** in your project so your workflow can import the library.
15
+
16
+ ### Global CLI
17
+ Provides the `state-machine` command.
18
+
14
19
  ```bash
15
- npm i agent-state-machine
20
+ # npm
21
+ npm i -g agent-state-machine
22
+
23
+ # pnpm
24
+ pnpm add -g agent-state-machine
16
25
  ```
17
26
 
18
- Global CLI:
27
+ ### Local Library
28
+ Required so your `workflow.js` can `import { agent, memory } from 'agent-state-machine'`.
19
29
 
20
30
  ```bash
21
- npm i -g agent-state-machine
31
+ # npm
32
+ npm i agent-state-machine
33
+
34
+ # pnpm (for monorepos/turbo, install in root)
35
+ pnpm add agent-state-machine -w
22
36
  ```
23
37
 
24
38
  Requirements: Node.js >= 16.
@@ -33,6 +47,7 @@ state-machine run <workflow-name>
33
47
  state-machine resume <workflow-name>
34
48
  state-machine status <workflow-name>
35
49
  state-machine history <workflow-name> [limit]
50
+ state-machine trace-logs <workflow-name>
36
51
  state-machine reset <workflow-name>
37
52
  ```
38
53
 
@@ -44,7 +59,7 @@ workflows/<name>/
44
59
  ├── package.json # Sets "type": "module" for this workflow folder
45
60
  ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
46
61
  ├── interactions/ # Human-in-the-loop files (auto-created)
47
- ├── state/ # current.json, history.jsonl, generated-prompt.md
62
+ ├── state/ # current.json, history.jsonl
48
63
  └── steering/ # global.md + config.json
49
64
  ```
50
65
 
@@ -278,10 +293,10 @@ export const config = {
278
293
  };
279
294
  ```
280
295
 
281
- The runtime writes the fully-built prompt to:
296
+ The runtime captures the fully-built prompt in `state/history.jsonl`, viewable via:
282
297
 
283
- ```text
284
- workflows/<name>/state/generated-prompt.md
298
+ ```bash
299
+ state-machine trace-logs <workflow-name>
285
300
  ```
286
301
 
287
302
  ---
package/bin/cli.js CHANGED
@@ -2,16 +2,30 @@
2
2
 
3
3
  import path from 'path';
4
4
  import fs from 'fs';
5
- import { pathToFileURL } from 'url';
5
+ import { pathToFileURL, fileURLToPath } from 'url';
6
6
  import { WorkflowRuntime } from '../lib/index.js';
7
7
  import { setup } from '../lib/setup.js';
8
+ import { startServer } from '../lib/ui/server.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
8
12
 
9
13
  const args = process.argv.slice(2);
10
14
  const command = args[0];
11
15
 
16
+ function getVersion() {
17
+ try {
18
+ const pkgPath = path.join(__dirname, '../package.json');
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
20
+ return pkg.version;
21
+ } catch {
22
+ return 'unknown';
23
+ }
24
+ }
25
+
12
26
  function printHelp() {
13
27
  console.log(`
14
- Agent State Machine CLI (Native JS Workflows Only)
28
+ Agent State Machine CLI (Native JS Workflows Only) v${getVersion()}
15
29
 
16
30
  Usage:
17
31
  state-machine --setup <workflow-name> Create a new workflow project
@@ -19,6 +33,7 @@ Usage:
19
33
  state-machine resume <workflow-name> Resume a paused workflow
20
34
  state-machine status [workflow-name] Show current state (or list all)
21
35
  state-machine history <workflow-name> [limit] Show execution history
36
+ state-machine trace-logs <workflow-name> View prompt trace history in browser
22
37
  state-machine reset <workflow-name> Reset workflow state
23
38
  state-machine reset-hard <workflow-name> Hard reset (clear history/interactions)
24
39
  state-machine list List all workflows
@@ -27,6 +42,7 @@ Usage:
27
42
  Options:
28
43
  --setup, -s Initialize a new workflow with directory structure
29
44
  --help, -h Show help
45
+ --version, -v Show version
30
46
 
31
47
  Workflow Structure:
32
48
  workflows/<name>/
@@ -34,7 +50,7 @@ Workflow Structure:
34
50
  ├── package.json # Sets "type": "module" for this workflow folder
35
51
  ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
36
52
  ├── interactions/ # Human-in-the-loop files (auto-created)
37
- ├── state/ # current.json, history.jsonl, generated-prompt.md
53
+ ├── state/ # current.json, history.jsonl
38
54
  └── steering/ # global.md + config.json
39
55
  `);
40
56
  }
@@ -143,6 +159,11 @@ async function runOrResume(workflowName) {
143
159
  }
144
160
 
145
161
  async function main() {
162
+ if (command === '--version' || command === '-v') {
163
+ console.log(getVersion());
164
+ process.exit(0);
165
+ }
166
+
146
167
  if (!command || command === 'help' || command === '--help' || command === '-h') {
147
168
  printHelp();
148
169
  process.exit(0);
@@ -203,6 +224,23 @@ async function main() {
203
224
  }
204
225
  break;
205
226
 
227
+ case 'trace-logs':
228
+ if (!workflowName) {
229
+ console.error('Error: Workflow name required');
230
+ console.error('Usage: state-machine trace-logs <workflow-name>');
231
+ process.exit(1);
232
+ }
233
+ {
234
+ const workflowDir = resolveWorkflowDir(workflowName);
235
+ if (!fs.existsSync(workflowDir)) {
236
+ console.error(`Error: Workflow '${workflowName}' not found`);
237
+ process.exit(1);
238
+ }
239
+ startServer(workflowDir);
240
+ // Do not exit, server needs to stay alive
241
+ }
242
+ break;
243
+
206
244
  case 'reset':
207
245
  if (!workflowName) {
208
246
  console.error('Error: Workflow name required');
package/lib/llm.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
+ import os from 'os';
7
8
  import { spawn, execSync } from 'child_process';
8
9
  import { createRequire } from 'module';
9
10
 
@@ -38,21 +39,6 @@ export function detectAvailableCLIs() {
38
39
  return available;
39
40
  }
40
41
 
41
- /**
42
- * Write the generated prompt file
43
- */
44
- export function writeGeneratedPrompt(workflowDir, content) {
45
- const promptDir = path.join(workflowDir, 'state');
46
- const promptFile = path.join(promptDir, 'generated-prompt.md');
47
-
48
- if (!fs.existsSync(promptDir)) {
49
- fs.mkdirSync(promptDir, { recursive: true });
50
- }
51
-
52
- fs.writeFileSync(promptFile, content);
53
- return promptFile;
54
- }
55
-
56
42
  /**
57
43
  * Build the full prompt with steering and context
58
44
  */
@@ -96,17 +82,18 @@ export function buildPrompt(context, options) {
96
82
 
97
83
  /**
98
84
  * Execute CLI command and return response
85
+ * Uses Stdin for supported tools, and temporary files for generic tools.
99
86
  */
100
- async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
87
+ async function executeCLI(command, promptText, options = {}, apiKeys = {}) {
101
88
  return new Promise((resolve, reject) => {
102
89
  // Parse command to extract base command and args
103
- // Note: naive split; if you need quoted args, consider a shell-args parser.
104
90
  const parts = command.split(' ');
105
91
  const baseCmd = parts[0];
106
92
  const baseArgs = parts.slice(1);
107
93
 
108
94
  // Build full args
109
95
  const args = [...baseArgs];
96
+ let tempPromptFile = null;
110
97
 
111
98
  const ensureCodexExec = () => {
112
99
  const CODEX_SUBCOMMANDS = new Set([
@@ -140,7 +127,7 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
140
127
  '-o', '--output-last-message'
141
128
  ]);
142
129
 
143
- // Insert `exec` after any leading global options so codex doesn't start interactive mode.
130
+ // Insert `exec` after any leading global options
144
131
  let i = 0;
145
132
  while (i < args.length) {
146
133
  const token = args[i];
@@ -150,7 +137,6 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
150
137
  i += 2;
151
138
  continue;
152
139
  }
153
-
154
140
  i += 1;
155
141
  }
156
142
 
@@ -158,41 +144,38 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
158
144
  if (firstNonOption && CODEX_SUBCOMMANDS.has(firstNonOption)) {
159
145
  return;
160
146
  }
161
-
162
147
  args.splice(i, 0, 'exec');
163
148
  };
164
149
 
165
- // Different CLIs handle file input differently
150
+ // Configure args based on the tool
151
+ const isStandardCLI = (baseCmd === 'claude' || baseCmd === 'gemini' || baseCmd === 'codex');
152
+
166
153
  if (baseCmd === 'claude') {
167
- // Claude CLI: use stdin for prompt input
168
- args.push('--print'); // Print response only
154
+ args.push('--print');
169
155
  args.push('--permission-mode', 'acceptEdits');
170
- // File content will be piped via stdin (no additional args needed)
156
+ // Input via stdin
171
157
  } else if (baseCmd === 'gemini') {
172
- // Gemini CLI
173
158
  args.push('--approval-mode', 'auto_edit');
174
- // No specific args needed for stdin input + one-shot mode
159
+ // Input via stdin
175
160
  } else if (baseCmd === 'codex') {
176
- // Codex CLI defaults to an interactive TUI, which requires a TTY.
177
- // Force non-interactive mode via `codex exec`, and feed PROMPT via stdin ("-").
178
161
  ensureCodexExec();
179
-
180
- // Write only the final message to a file to avoid parsing extra output.
181
162
  const lastMessageFile = path.join(
182
- path.dirname(promptFile),
163
+ os.tmpdir(),
183
164
  `codex-last-message-${process.pid}-${Date.now()}.txt`
184
165
  );
185
166
  args.push('--output-last-message', lastMessageFile);
186
-
187
- args.push('-');
167
+ args.push('-'); // Explicitly read from stdin
188
168
  } else {
189
- // Generic: try passing file as argument
190
- args.push(promptFile);
169
+ // Generic CLI: Fallback to temp file if not a known stdin consumer
170
+ // We assume generic tools might expect a filename as an argument.
171
+ const uniqueId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
172
+ tempPromptFile = path.join(os.tmpdir(), `asm-prompt-${uniqueId}.md`);
173
+ fs.writeFileSync(tempPromptFile, promptText);
174
+ args.push(tempPromptFile);
191
175
  }
192
176
 
193
177
  console.log(` [LLM] Running: ${baseCmd} ${args.join(' ')}`);
194
178
 
195
- // Prepare environment variables with API keys if provided
196
179
  const env = { ...process.env };
197
180
  if (apiKeys.gemini) env.GEMINI_API_KEY = apiKeys.gemini;
198
181
  if (apiKeys.anthropic) env.ANTHROPIC_API_KEY = apiKeys.anthropic;
@@ -203,10 +186,12 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
203
186
  env: env
204
187
  });
205
188
 
206
- // Feed stdin for Codex, Claude, and Gemini from the prompt file; otherwise close stdin.
207
- if (baseCmd === 'codex' || baseCmd === 'claude' || baseCmd === 'gemini') {
208
- fs.createReadStream(promptFile).pipe(child.stdin);
189
+ // Write prompt to stdin if it's a standard tool or we decided to use stdin
190
+ if (isStandardCLI) {
191
+ child.stdin.write(promptText);
192
+ child.stdin.end();
209
193
  } else {
194
+ // For generic tools using temp files, just close stdin
210
195
  child.stdin.end();
211
196
  }
212
197
 
@@ -222,6 +207,11 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
222
207
  });
223
208
 
224
209
  child.on('close', (code) => {
210
+ // Cleanup temp file if used
211
+ if (tempPromptFile && fs.existsSync(tempPromptFile)) {
212
+ try { fs.unlinkSync(tempPromptFile); } catch {}
213
+ }
214
+
225
215
  if (code === 0) {
226
216
  if (baseCmd === 'codex') {
227
217
  const outputFlagIndex = args.findIndex(a => a === '--output-last-message' || a === '-o');
@@ -247,6 +237,10 @@ async function executeCLI(command, promptFile, options = {}, apiKeys = {}) {
247
237
  });
248
238
 
249
239
  child.on('error', (err) => {
240
+ // Cleanup temp file if used
241
+ if (tempPromptFile && fs.existsSync(tempPromptFile)) {
242
+ try { fs.unlinkSync(tempPromptFile); } catch {}
243
+ }
250
244
  reject(new Error(`Failed to execute CLI: ${err.message}`));
251
245
  });
252
246
  });
@@ -259,7 +253,6 @@ async function executeAPI(provider, model, prompt, apiKey, options = {}) {
259
253
  console.log(` [LLM] Calling API: ${provider}/${model}`);
260
254
 
261
255
  if (provider === 'anthropic') {
262
- // Dynamic import to avoid requiring the package if not used
263
256
  let Anthropic;
264
257
  try {
265
258
  Anthropic = require('@anthropic-ai/sdk');
@@ -341,8 +334,9 @@ export async function llm(context, options) {
341
334
  const config = context._config || {};
342
335
  const models = config.models || {};
343
336
  const apiKeys = config.apiKeys || {};
344
- const workflowDir = config.workflowDir || process.cwd();
345
-
337
+
338
+ // No longer needed to write to prompts/ directory here
339
+
346
340
  // Look up the model command/config
347
341
  const modelConfig = models[options.model];
348
342
 
@@ -356,13 +350,9 @@ export async function llm(context, options) {
356
350
  // Build the full prompt
357
351
  const fullPrompt = buildPrompt(context, options);
358
352
 
359
- // Write to generated-prompt.md
360
- const promptFile = writeGeneratedPrompt(workflowDir, fullPrompt);
361
- console.log(` [LLM] Prompt written to: ${promptFile}`);
362
-
363
353
  // Check if it's an API call or CLI
354
+ let result;
364
355
  if (modelConfig.startsWith('api:')) {
365
- // Format: api:provider:model
366
356
  const parts = modelConfig.split(':');
367
357
  const provider = parts[1];
368
358
  const model = parts.slice(2).join(':');
@@ -375,11 +365,13 @@ export async function llm(context, options) {
375
365
  );
376
366
  }
377
367
 
378
- return executeAPI(provider, model, fullPrompt, apiKey, options);
368
+ result = await executeAPI(provider, model, fullPrompt, apiKey, options);
369
+ } else {
370
+ // CLI execution - pass fullPrompt string directly
371
+ result = await executeCLI(modelConfig, fullPrompt, options, apiKeys);
379
372
  }
380
373
 
381
- // CLI execution
382
- return executeCLI(modelConfig, promptFile, options, apiKeys);
374
+ return { ...result, fullPrompt };
383
375
  }
384
376
 
385
377
  /**
@@ -446,7 +438,7 @@ export function parseJSON(text) {
446
438
  } catch {}
447
439
  }
448
440
 
449
- const arrayMatch = text.match(/\[[\s\S]*\]/);
441
+ const arrayMatch = text.match(/\{[\[\s\S]*\]\}/);
450
442
  if (arrayMatch) {
451
443
  try {
452
444
  return JSON.parse(arrayMatch[0]);
@@ -469,4 +461,4 @@ export async function llmJSON(context, options) {
469
461
  ...response,
470
462
  data: parseJSON(response.text)
471
463
  };
472
- }
464
+ }
@@ -34,11 +34,18 @@ export async function agent(name, params = {}) {
34
34
  try {
35
35
  const result = await executeAgent(runtime, name, params);
36
36
 
37
+ let prompt = undefined;
38
+ if (result && typeof result === 'object' && result._debug_prompt) {
39
+ prompt = result._debug_prompt;
40
+ delete result._debug_prompt;
41
+ }
42
+
37
43
  console.log(` [Agent: ${name}] Completed`);
38
44
  runtime.prependHistory({
39
45
  event: 'AGENT_COMPLETED',
40
46
  agent: name,
41
- output: result
47
+ output: result,
48
+ prompt: prompt
42
49
  });
43
50
 
44
51
  return result;
@@ -243,11 +250,11 @@ async function executeMDAgent(runtime, agentPath, name, params) {
243
250
  });
244
251
 
245
252
  // Return the user's response as the agent result
246
- return { [outputKey]: userResponse };
253
+ return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
247
254
  }
248
255
 
249
256
  // Return result object
250
- return { [outputKey]: output };
257
+ return { [outputKey]: output, _debug_prompt: response.fullPrompt };
251
258
  }
252
259
 
253
260
  /**
@@ -38,7 +38,6 @@ export class WorkflowRuntime {
38
38
  this.agentsDir = path.join(workflowDir, 'agents');
39
39
  this.interactionsDir = path.join(workflowDir, 'interactions');
40
40
  this.steeringDir = path.join(workflowDir, 'steering');
41
- this.generatedPromptFile = path.join(this.stateDir, 'generated-prompt.md');
42
41
  this.historyFile = path.join(this.stateDir, 'history.jsonl');
43
42
  this.stateFile = path.join(this.stateDir, 'current.json');
44
43
 
@@ -361,11 +360,6 @@ export class WorkflowRuntime {
361
360
  fs.unlinkSync(this.historyFile);
362
361
  }
363
362
 
364
- // 2. Delete generated-prompt.md file
365
- if (fs.existsSync(this.generatedPromptFile)) {
366
- fs.unlinkSync(this.generatedPromptFile);
367
- }
368
-
369
363
  // 3. Clear interactions directory
370
364
  if (fs.existsSync(this.interactionsDir)) {
371
365
  fs.rmSync(this.interactionsDir, { recursive: true, force: true });
package/lib/setup.js CHANGED
@@ -288,7 +288,7 @@ ${workflowName}/
288
288
  ├── package.json # Sets "type": "module" for this workflow folder
289
289
  ├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
290
290
  ├── interactions/ # Human-in-the-loop inputs (created at runtime)
291
- ├── state/ # Runtime state (current.json, history.jsonl)
291
+ ├── state/ # Runtime state (current.json, history.jsonl, prompts/)
292
292
  └── steering/ # Steering configuration
293
293
  \\\`\\\`\\\`
294
294
 
@@ -314,6 +314,11 @@ View history:
314
314
  state-machine history ${workflowName}
315
315
  \\\`\\\`\\\`
316
316
 
317
+ View trace logs in browser:
318
+ \\\`\\\`\\\`bash
319
+ state-machine trace-logs ${workflowName}
320
+ \\\`\\\`\\\`
321
+
317
322
  Reset state:
318
323
  \\\`\\\`\\\`bash
319
324
  state-machine reset ${workflowName}
@@ -0,0 +1,294 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Workflow Prompts Viewer</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'class',
11
+ }
12
+ </script>
13
+ <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
14
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
15
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
16
+ <style>
17
+ .markdown-body { white-space: pre-wrap; font-family: monospace; }
18
+ /* Scrollbar styles for dark mode */
19
+ .dark ::-webkit-scrollbar { width: 10px; height: 10px; }
20
+ .dark ::-webkit-scrollbar-track { background: #000000; }
21
+ .dark ::-webkit-scrollbar-thumb { background: #27272a; border-radius: 5px; }
22
+ .dark ::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="root"></div>
27
+
28
+ <script type="text/babel">
29
+ const { useState, useEffect } = React;
30
+
31
+ // Icons
32
+ const SunIcon = () => (
33
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
34
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
35
+ </svg>
36
+ );
37
+
38
+ const MoonIcon = () => (
39
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
40
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
41
+ </svg>
42
+ );
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" />
47
+ </svg>
48
+ );
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" />
55
+ </svg>
56
+ );
57
+
58
+ function App() {
59
+ const [history, setHistory] = useState([]);
60
+ const [loading, setLoading] = useState(true);
61
+ const [error, setError] = useState(null);
62
+ const [workflowName, setWorkflowName] = useState('');
63
+ const [theme, setTheme] = useState('dark');
64
+ const [sortOrder, setSortOrder] = useState('newest'); // 'newest' | 'oldest'
65
+
66
+ useEffect(() => {
67
+ const fetchData = () => {
68
+ fetch('/api/history')
69
+ .then(res => res.json())
70
+ .then(data => {
71
+ setHistory(data.entries);
72
+ setWorkflowName(data.workflowName);
73
+ setLoading(false);
74
+ })
75
+ .catch(err => {
76
+ setError(err.message);
77
+ setLoading(false);
78
+ });
79
+ };
80
+
81
+ // Initial fetch
82
+ fetchData();
83
+
84
+ // Setup SSE
85
+ const eventSource = new EventSource('/api/events');
86
+ eventSource.onmessage = (event) => {
87
+ if (event.data === 'update') {
88
+ fetchData();
89
+ }
90
+ };
91
+
92
+ return () => {
93
+ eventSource.close();
94
+ };
95
+ }, []);
96
+
97
+ const toggleTheme = () => {
98
+ setTheme(prev => prev === 'dark' ? 'light' : 'dark');
99
+ };
100
+
101
+ const toggleSort = () => {
102
+ setSortOrder(prev => prev === 'newest' ? 'oldest' : 'newest');
103
+ };
104
+
105
+ if (loading) return (
106
+ <div className={theme}>
107
+ <div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-gray-500 dark:text-zinc-500">
108
+ Loading history...
109
+ </div>
110
+ </div>
111
+ );
112
+
113
+ if (error) return (
114
+ <div className={theme}>
115
+ <div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-red-500">
116
+ Error: {error}
117
+ </div>
118
+ </div>
119
+ );
120
+
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
+ );
129
+
130
+ // Apply Sort
131
+ // History from API is "Newest First" (index 0 is latest)
132
+ if (sortOrder === 'oldest') {
133
+ visibleEvents = [...visibleEvents].reverse();
134
+ }
135
+
136
+ const formatTime = (ts) => new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
137
+
138
+ return (
139
+ <div className={theme}>
140
+ <div className="min-h-screen bg-gray-50 dark:bg-black transition-colors duration-200">
141
+ <div className="max-w-5xl mx-auto min-h-screen flex flex-col">
142
+
143
+ {/* Sticky Header */}
144
+ <header className="sticky top-0 z-50 py-4 px-6 bg-gray-50/90 dark:bg-black/90 backdrop-blur-md border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between mb-8 transition-colors">
145
+ <div className="flex-1">
146
+ <h1 className="text-xl font-bold text-gray-800 dark:text-zinc-100 transition-colors uppercase tracking-tight">{workflowName}</h1>
147
+ <p className="text-gray-500 dark:text-zinc-500 text-xs mt-0.5">Runtime History & Prompt Logs</p>
148
+ </div>
149
+ <div className="flex items-center space-x-2">
150
+ <button
151
+ onClick={toggleSort}
152
+ className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
153
+ title={sortOrder === 'newest' ? "Sort: Newest First" : "Sort: Oldest First"}
154
+ >
155
+ {sortOrder === 'newest' ?
156
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /></svg>
157
+ :
158
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" transform="scale(1, -1) translate(0, -24)" /></svg>
159
+ }
160
+ </button>
161
+ <button
162
+ onClick={toggleTheme}
163
+ className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
164
+ title="Toggle Theme"
165
+ >
166
+ {theme === 'dark' ? <SunIcon /> : <MoonIcon />}
167
+ </button>
168
+ </div>
169
+ </header>
170
+
171
+ {/* Content */}
172
+ <div className="flex-1 space-y-8 px-6">
173
+ {visibleEvents.length === 0 && (
174
+ <div className="text-center text-gray-400 py-10">No execution history found.</div>
175
+ )}
176
+
177
+ {visibleEvents.map((item, idx) => {
178
+ // 1. Lifecycle Events
179
+ if (item.event.startsWith('WORKFLOW_')) {
180
+ const colorMap = {
181
+ 'WORKFLOW_STARTED': 'text-green-500 dark:text-green-400',
182
+ 'WORKFLOW_COMPLETED': 'text-blue-500 dark:text-blue-400',
183
+ 'WORKFLOW_FAILED': 'text-red-500 dark:text-red-400',
184
+ 'WORKFLOW_RESET': 'text-yellow-500 dark:text-yellow-400'
185
+ };
186
+ return (
187
+ <div key={idx} className="flex flex-col items-center py-4">
188
+ <div className="flex items-center space-x-3 text-[10px] uppercase tracking-[0.2em] font-bold text-zinc-400 dark:text-zinc-600">
189
+ <div className="h-px w-8 bg-zinc-200 dark:bg-zinc-800"></div>
190
+ <span className={colorMap[item.event]}>{item.event.replace('_', ' ')}</span>
191
+ <span>{formatTime(item.timestamp)}</span>
192
+ <div className="h-px w-8 bg-zinc-200 dark:bg-zinc-800"></div>
193
+ </div>
194
+ {item.error && <div className="mt-2 text-red-500 text-xs font-mono">{item.error}</div>}
195
+ </div>
196
+ );
197
+ }
198
+
199
+ // 2. Agent Started
200
+ if (item.event === 'AGENT_STARTED') {
201
+ return (
202
+ <div key={idx} className="flex justify-start">
203
+ <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest flex items-center space-x-2">
204
+ <span className="w-1.5 h-1.5 rounded-full bg-blue-500/50 animate-pulse"></span>
205
+ <span>Agent <span className="text-zinc-600 dark:text-zinc-400">{item.agent}</span> started</span>
206
+ <span>&bull;</span>
207
+ <span>{formatTime(item.timestamp)}</span>
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ // 3. Agent Failed
214
+ if (item.event === 'AGENT_FAILED') {
215
+ return (
216
+ <div key={idx} className="flex justify-center">
217
+ <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900/50 rounded-lg px-4 py-2 text-red-600 dark:text-red-400 text-xs font-mono w-full max-w-2xl">
218
+ <div className="font-bold mb-1 underline">AGENT FAILED: {item.agent}</div>
219
+ <div>{item.error}</div>
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ // 4. Interaction Requested
226
+ if (item.event === 'INTERACTION_REQUESTED') {
227
+ return (
228
+ <div key={idx} className="flex justify-center">
229
+ <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>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ // 5. Agent Completed / Interaction Resolved (The Bubbles)
238
+ if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
239
+ return (
240
+ <div key={idx} className="flex flex-col space-y-4">
241
+ {/* Header Line */}
242
+ <div className="flex items-center justify-center space-x-2 text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest">
243
+ <span className="font-black text-zinc-500 dark:text-zinc-400">{item.agent || item.slug}</span>
244
+ <span>&bull;</span>
245
+ <span>COMPLETED</span>
246
+ <span>&bull;</span>
247
+ <span>{formatTime(item.timestamp)}</span>
248
+ </div>
249
+
250
+ {/* Output (Response) - NOW ON TOP */}
251
+ {(item.output || item.result) && (
252
+ <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>
255
+ <div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
256
+ {typeof item.output === 'object' ? JSON.stringify(item.output, null, 2) : (item.output || item.result)}
257
+ </div>
258
+ </div>
259
+ </div>
260
+ )}
261
+
262
+ {/* Prompt (Input) - NOW ON BOTTOM */}
263
+ {item.prompt && (
264
+ <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>
267
+ <div className="markdown-body text-gray-800 dark:text-zinc-300 text-sm overflow-x-auto leading-relaxed">
268
+ {item.prompt}
269
+ </div>
270
+ </div>
271
+ </div>
272
+ )}
273
+ </div>
274
+ );
275
+ }
276
+
277
+ return null;
278
+ })}
279
+ </div>
280
+
281
+ <footer className="mt-20 mb-8 text-center text-zinc-400 dark:text-zinc-800 text-[10px] uppercase tracking-[0.3em] transition-colors">
282
+ Agent State Machine &bull; Debug Terminal
283
+ </footer>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ }
289
+
290
+ const root = ReactDOM.createRoot(document.getElementById('root'));
291
+ root.render(<App />);
292
+ </script>
293
+ </body>
294
+ </html>
@@ -0,0 +1,125 @@
1
+ /**
2
+ * File: /lib/ui/server.js
3
+ */
4
+
5
+ import http from 'http';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ export function startServer(workflowDir, port = 3000) {
14
+ const clients = new Set();
15
+ const stateDir = path.join(workflowDir, 'state');
16
+
17
+ // Watch for changes in the state directory
18
+ // We debounce slightly to avoid sending multiple events for a single write burst
19
+ let debounceTimer;
20
+ const broadcastUpdate = () => {
21
+ if (debounceTimer) clearTimeout(debounceTimer);
22
+ debounceTimer = setTimeout(() => {
23
+ const msg = 'data: update\n\n';
24
+ for (const client of clients) {
25
+ try {
26
+ client.write(msg);
27
+ } catch (e) {
28
+ clients.delete(client);
29
+ }
30
+ }
31
+ }, 100);
32
+ };
33
+
34
+ try {
35
+ if (fs.existsSync(stateDir)) {
36
+ fs.watch(stateDir, (eventType, filename) => {
37
+ if (filename && (filename === 'history.jsonl' || filename.startsWith('history'))) {
38
+ broadcastUpdate();
39
+ }
40
+ });
41
+ } else {
42
+ console.warn('Warning: State directory does not exist yet. Live updates might not work until it is created.');
43
+ }
44
+ } catch (err) {
45
+ console.warn('Warning: Failed to setup file watcher:', err.message);
46
+ }
47
+
48
+ const server = http.createServer((req, res) => {
49
+ // Serve the main HTML page
50
+ if (req.url === '/' || req.url === '/index.html') {
51
+ const htmlPath = path.join(__dirname, 'index.html');
52
+ fs.readFile(htmlPath, (err, content) => {
53
+ if (err) {
54
+ res.writeHead(500);
55
+ res.end('Error loading UI');
56
+ return;
57
+ }
58
+ res.writeHead(200, { 'Content-Type': 'text/html' });
59
+ res.end(content);
60
+ });
61
+ return;
62
+ }
63
+
64
+ // Server-Sent Events endpoint
65
+ if (req.url === '/api/events') {
66
+ res.writeHead(200, {
67
+ 'Content-Type': 'text/event-stream',
68
+ 'Cache-Control': 'no-cache',
69
+ 'Connection': 'keep-alive',
70
+ });
71
+ res.write('retry: 10000\n\n');
72
+
73
+ clients.add(res);
74
+
75
+ req.on('close', () => {
76
+ clients.delete(res);
77
+ });
78
+ return;
79
+ }
80
+
81
+ // Serve API
82
+ if (req.url === '/api/history') {
83
+ const historyFile = path.join(stateDir, 'history.jsonl');
84
+
85
+ if (!fs.existsSync(historyFile)) {
86
+ res.writeHead(200, { 'Content-Type': 'application/json' });
87
+ res.end(JSON.stringify({
88
+ workflowName: path.basename(workflowDir),
89
+ entries: []
90
+ }));
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const fileContent = fs.readFileSync(historyFile, 'utf-8');
96
+ const lines = fileContent.trim().split('\n');
97
+ const entries = lines
98
+ .map(line => {
99
+ try { return JSON.parse(line); } catch { return null; }
100
+ })
101
+ .filter(Boolean);
102
+
103
+ res.writeHead(200, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({
105
+ workflowName: path.basename(workflowDir),
106
+ entries
107
+ }));
108
+ } catch (err) {
109
+ res.writeHead(500, { 'Content-Type': 'application/json' });
110
+ res.end(JSON.stringify({ error: err.message }));
111
+ }
112
+ return;
113
+ }
114
+
115
+ // 404
116
+ res.writeHead(404);
117
+ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "1.0.2",
3
+ "version": "1.1.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",