agent-state-machine 2.2.1 → 2.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.
Files changed (33) hide show
  1. package/bin/cli.js +33 -5
  2. package/lib/file-tree.js +1 -1
  3. package/lib/runtime/agent.js +6 -2
  4. package/lib/runtime/interaction.js +2 -1
  5. package/lib/runtime/prompt.js +37 -1
  6. package/lib/runtime/runtime.js +67 -5
  7. package/lib/setup.js +4 -4
  8. package/package.json +1 -1
  9. package/templates/project-builder/agents/code-fixer.md +50 -0
  10. package/templates/project-builder/agents/code-writer.md +3 -0
  11. package/templates/project-builder/agents/sanity-checker.md +6 -0
  12. package/templates/project-builder/agents/test-planner.md +3 -1
  13. package/templates/project-builder/config.js +4 -4
  14. package/templates/project-builder/scripts/workflow-helpers.js +104 -2
  15. package/templates/project-builder/workflow.js +151 -14
  16. package/templates/starter/config.js +1 -1
  17. package/vercel-server/api/submit/[token].js +0 -11
  18. package/vercel-server/local-server.js +0 -19
  19. package/vercel-server/public/remote/assets/index-BTLc1QSv.js +168 -0
  20. package/vercel-server/public/remote/assets/index-DLa4X08t.css +1 -0
  21. package/vercel-server/public/remote/index.html +2 -2
  22. package/vercel-server/ui/src/App.jsx +53 -18
  23. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
  24. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
  25. package/vercel-server/ui/src/components/ContentCard.jsx +607 -103
  26. package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
  27. package/vercel-server/ui/src/components/Footer.jsx +9 -4
  28. package/vercel-server/ui/src/components/Header.jsx +12 -3
  29. package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
  30. package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
  31. package/vercel-server/ui/src/index.css +82 -10
  32. package/vercel-server/public/remote/assets/index-CbgeVnKw.js +0 -148
  33. package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
@@ -8,11 +8,12 @@
8
8
  * 4. Task lifecycle with optimal agent sequencing
9
9
  */
10
10
 
11
- import { agent, memory, askHuman } from 'agent-state-machine';
11
+ import { agent, memory, askHuman, getCurrentRuntime } from 'agent-state-machine';
12
12
  import path from 'path';
13
13
  import { fileURLToPath } from 'url';
14
14
  import {
15
15
  writeMarkdownFile,
16
+ writeImplementationFiles,
16
17
  isApproval,
17
18
  renderRoadmapMarkdown,
18
19
  renderTasksMarkdown,
@@ -20,7 +21,12 @@ import {
20
21
  getTaskStage,
21
22
  setTaskStage,
22
23
  getTaskData,
23
- setTaskData
24
+ setTaskData,
25
+ clearPartialTaskData,
26
+ getQuickFixAttempts,
27
+ incrementQuickFixAttempts,
28
+ resetQuickFixAttempts,
29
+ detectTestFramework
24
30
  } from './scripts/workflow-helpers.js';
25
31
  import {
26
32
  createInteraction,
@@ -34,6 +40,57 @@ const __dirname = path.dirname(__filename);
34
40
  const WORKFLOW_DIR = __dirname;
35
41
  const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
36
42
 
43
+ // ANSI Colors for console output
44
+ const C = {
45
+ reset: '\x1b[0m',
46
+ bold: '\x1b[1m',
47
+ cyan: '\x1b[36m',
48
+ green: '\x1b[32m',
49
+ yellow: '\x1b[33m'
50
+ };
51
+
52
+ function applyFixesToImplementation(originalImplementation, fixes) {
53
+ if (!originalImplementation || !Array.isArray(fixes) || fixes.length === 0) {
54
+ return originalImplementation;
55
+ }
56
+
57
+ const updated = { ...originalImplementation };
58
+ const container = updated.implementation ? { ...updated.implementation } : updated;
59
+ const files = Array.isArray(container.files) ? [...container.files] : [];
60
+
61
+ for (const fix of fixes) {
62
+ if (!fix?.path || !fix?.code) {
63
+ console.warn(` [Fix] Skipping invalid fix entry: ${JSON.stringify(fix)}`);
64
+ continue;
65
+ }
66
+ if (fix.operation && fix.operation !== 'replace') {
67
+ console.warn(` [Fix] Unsupported operation "${fix.operation}" for ${fix.path}`);
68
+ continue;
69
+ }
70
+
71
+ const existingIndex = files.findIndex((file) => file.path === fix.path);
72
+ const nextFile = {
73
+ ...(existingIndex >= 0 ? files[existingIndex] : {}),
74
+ path: fix.path,
75
+ code: fix.code,
76
+ purpose: fix.purpose || (existingIndex >= 0 ? files[existingIndex].purpose : 'Updated by code-fixer')
77
+ };
78
+
79
+ if (existingIndex >= 0) {
80
+ files[existingIndex] = nextFile;
81
+ } else {
82
+ files.push(nextFile);
83
+ }
84
+ }
85
+
86
+ if (updated.implementation) {
87
+ updated.implementation = { ...container, files };
88
+ return updated;
89
+ }
90
+
91
+ return { ...updated, files };
92
+ }
93
+
37
94
  // ============================================
38
95
  // MAIN WORKFLOW
39
96
  // ============================================
@@ -334,6 +391,14 @@ export default async function () {
334
391
  });
335
392
  setTaskData(i, taskId, 'code', implementation);
336
393
  }
394
+
395
+ // Write implementation files to disk
396
+ const implementation = getTaskData(i, taskId, 'code');
397
+ if (implementation) {
398
+ console.log(' > Writing files to disk...');
399
+ writeImplementationFiles(implementation);
400
+ }
401
+
337
402
  setTaskStage(i, taskId, TASK_STAGES.CODE_REVIEW);
338
403
  stage = TASK_STAGES.CODE_REVIEW;
339
404
  }
@@ -373,10 +438,12 @@ export default async function () {
373
438
 
374
439
  // 6. Sanity check generation & execution
375
440
  if (stage === TASK_STAGES.SANITY_CHECK) {
441
+ const testFramework = detectTestFramework();
376
442
  const executableChecks = await agent('sanity-checker', {
377
443
  task: task,
378
444
  implementation: getTaskData(i, taskId, 'code'),
379
- testPlan: getTaskData(i, taskId, 'tests')
445
+ testPlan: getTaskData(i, taskId, 'tests'),
446
+ testFramework
380
447
  });
381
448
  setTaskData(i, taskId, 'sanity_checks', executableChecks);
382
449
 
@@ -387,8 +454,8 @@ export default async function () {
387
454
  const sanityChoice = createInteraction('choice', `phase-${i + 1}-task-${taskId}-sanity-choice`, {
388
455
  prompt: `Sanity checks for "${task.title}":\n\n${checksDisplay}\n\nHow would you like to proceed?`,
389
456
  options: [
390
- { key: 'manual', label: 'Run checks manually', description: 'You run the commands and confirm results' },
391
457
  { key: 'auto', label: 'Run automatically', description: 'Agent executes checks and reports results' },
458
+ { key: 'manual', label: 'Run checks manually', description: 'You run the commands and confirm results' },
392
459
  { key: 'skip', label: 'Skip verification', description: 'Approve without running checks' }
393
460
  ],
394
461
  allowCustom: true
@@ -402,6 +469,7 @@ export default async function () {
402
469
 
403
470
  if (sanityResponse.isCustom) {
404
471
  setTaskData(i, taskId, 'feedback', sanityResponse.customText || sanityResponse.raw || sanityRaw);
472
+ resetQuickFixAttempts(i, taskId);
405
473
  setTaskStage(i, taskId, TASK_STAGES.PENDING);
406
474
  t--;
407
475
  continue;
@@ -423,12 +491,26 @@ export default async function () {
423
491
  .map((r) => ` - Check ${r.id}: ${r.error}`)
424
492
  .join('\n');
425
493
 
494
+ const quickFixAttempts = getQuickFixAttempts(i, taskId);
495
+ const runtime = getCurrentRuntime();
496
+ const maxAttempts = runtime?.workflowConfig?.maxQuickFixAttempts ?? 10;
497
+ const failOptions = [];
498
+ if (quickFixAttempts < maxAttempts) {
499
+ failOptions.push({
500
+ key: 'quickfix',
501
+ label: 'Quick fix',
502
+ description: `Run targeted fixes (attempt ${quickFixAttempts + 1} of ${maxAttempts})`
503
+ });
504
+ }
505
+ failOptions.push(
506
+ { key: 'partial', label: 'Partial reimplement', description: 'Keep security review and test plan, redo implementation' },
507
+ { key: 'reimplement', label: 'Full reimplement', description: 'Restart task from scratch' },
508
+ { key: 'ignore', label: 'Ignore failures and approve anyway' }
509
+ );
510
+
426
511
  const failChoice = createInteraction('choice', `phase-${i + 1}-task-${taskId}-sanity-fail`, {
427
512
  prompt: `${results.summary.failed} sanity check(s) failed:\n\n${failedChecks}\n\nHow would you like to proceed?`,
428
- options: [
429
- { key: 'reimplement', label: 'Re-implement task with this feedback' },
430
- { key: 'ignore', label: 'Ignore failures and approve anyway' }
431
- ],
513
+ options: failOptions,
432
514
  allowCustom: true
433
515
  });
434
516
 
@@ -438,19 +520,71 @@ export default async function () {
438
520
  });
439
521
  const failResponse = await parseResponse(failChoice, failRaw);
440
522
 
441
- if (failResponse.selectedKey === 'reimplement' || failResponse.isCustom) {
523
+ if (failResponse.isCustom) {
524
+ const customFeedback = failResponse.customText || failResponse.text || failResponse.raw || failRaw;
525
+ const combinedFeedback = `${customFeedback}\n\nSanity check failures:\n${failedChecks}`;
526
+ setTaskData(i, taskId, 'feedback', combinedFeedback);
527
+ clearPartialTaskData(i, taskId);
528
+ resetQuickFixAttempts(i, taskId);
529
+ setTaskStage(i, taskId, TASK_STAGES.PENDING);
530
+ t--;
531
+ continue;
532
+ }
533
+
534
+ if (failResponse.selectedKey === 'quickfix') {
535
+ console.log(' > Running quick fix...');
536
+ const fixerResult = await agent('code-fixer', {
537
+ task: task,
538
+ originalImplementation: getTaskData(i, taskId, 'code'),
539
+ sanityCheckResults: {
540
+ summary: results.summary,
541
+ results: results.results,
542
+ checks: executableChecks.checks
543
+ },
544
+ testPlan: getTaskData(i, taskId, 'tests'),
545
+ previousAttempts: quickFixAttempts
546
+ });
547
+
548
+ const fixes = fixerResult?.fixes || [];
549
+ const fixFiles = fixes
550
+ .filter((fix) => fix?.path && fix?.code && (!fix.operation || fix.operation === 'replace'))
551
+ .map((fix) => ({ path: fix.path, code: fix.code }));
552
+
553
+ if (fixFiles.length > 0) {
554
+ console.log(' > Applying fixes to disk...');
555
+ writeImplementationFiles({ files: fixFiles });
556
+ }
557
+
558
+ const updatedImplementation = applyFixesToImplementation(getTaskData(i, taskId, 'code'), fixes);
559
+ setTaskData(i, taskId, 'code', updatedImplementation);
560
+ incrementQuickFixAttempts(i, taskId);
561
+ setTaskData(i, taskId, 'sanity_checks', null);
562
+ setTaskData(i, taskId, 'sanity_results', null);
563
+ setTaskStage(i, taskId, TASK_STAGES.SANITY_CHECK);
564
+ t--;
565
+ continue;
566
+ }
567
+
568
+ if (failResponse.selectedKey === 'partial') {
569
+ setTaskData(i, taskId, 'feedback', `Sanity check failures:\n${failedChecks}`);
570
+ clearPartialTaskData(i, taskId, ['security_pre', 'tests']);
571
+ resetQuickFixAttempts(i, taskId);
572
+ setTaskStage(i, taskId, TASK_STAGES.IMPLEMENTING);
573
+ t--;
574
+ continue;
575
+ }
576
+
577
+ if (failResponse.selectedKey === 'reimplement') {
442
578
  setTaskData(i, taskId, 'feedback', `Sanity check failures:\n${failedChecks}`);
443
- setTaskData(i, taskId, 'security_pre', null);
444
- setTaskData(i, taskId, 'tests', null);
445
- setTaskData(i, taskId, 'code', null);
446
- setTaskData(i, taskId, 'review', null);
447
- setTaskData(i, taskId, 'security_post', null);
579
+ clearPartialTaskData(i, taskId);
580
+ resetQuickFixAttempts(i, taskId);
448
581
  setTaskStage(i, taskId, TASK_STAGES.PENDING);
449
582
  t--;
450
583
  continue;
451
584
  }
452
585
  }
453
586
 
587
+ resetQuickFixAttempts(i, taskId);
454
588
  setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
455
589
  stage = TASK_STAGES.COMPLETED;
456
590
  task.stage = 'completed';
@@ -458,6 +592,7 @@ export default async function () {
458
592
  writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
459
593
  console.log(` Task ${t + 1} confirmed complete!\n`);
460
594
  } else if (action === 'skip') {
595
+ resetQuickFixAttempts(i, taskId);
461
596
  setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
462
597
  stage = TASK_STAGES.COMPLETED;
463
598
  task.stage = 'completed';
@@ -489,6 +624,7 @@ export default async function () {
489
624
 
490
625
  if (approvalResponse.selectedKey === 'approve' || isApproval(approvalResponse.raw || approvalRaw)) {
491
626
  setTaskStage(i, taskId, TASK_STAGES.COMPLETED);
627
+ resetQuickFixAttempts(i, taskId);
492
628
  task.stage = 'completed';
493
629
  memory[tasksKey] = tasks;
494
630
  writeMarkdownFile(STATE_DIR, `phase-${i + 1}-tasks.md`, renderTasksMarkdown(i + 1, phase.title, tasks));
@@ -505,6 +641,7 @@ export default async function () {
505
641
  setTaskData(i, taskId, 'security_post', null);
506
642
  setTaskData(i, taskId, 'sanity_checks', null);
507
643
  setTaskData(i, taskId, 'sanity_results', null);
644
+ resetQuickFixAttempts(i, taskId);
508
645
 
509
646
  setTaskStage(i, taskId, TASK_STAGES.PENDING);
510
647
  t--;
@@ -1,6 +1,6 @@
1
1
  export const config = {
2
2
  models: {
3
- low: "gemini",
3
+ low: "gemini -m gemini-2.5-flash-lite",
4
4
  med: "codex --model gpt-5.2",
5
5
  high: "claude -m claude-opus-4-20250514 -p",
6
6
  },
@@ -6,7 +6,6 @@
6
6
 
7
7
  import {
8
8
  getSession,
9
- addEvent,
10
9
  redis,
11
10
  KEYS,
12
11
  } from '../../lib/redis.js';
@@ -69,16 +68,6 @@ export default async function handler(req, res) {
69
68
  // Set TTL on pending list (24 hours - same as session, allows laptop sleep)
70
69
  await redis.expire(pendingKey, 24 * 60 * 60);
71
70
 
72
- // Log event to events list (single source of truth for UI)
73
- await addEvent(token, {
74
- timestamp: new Date().toISOString(),
75
- event: 'INTERACTION_SUBMITTED',
76
- slug,
77
- targetKey: targetKey || `_interaction_${slug}`,
78
- answer: responseString.substring(0, 200) + (responseString.length > 200 ? '...' : ''),
79
- source: 'remote',
80
- });
81
-
82
71
  return res.status(200).json({ success: true });
83
72
  } catch (err) {
84
73
  console.error('Error submitting interaction:', err);
@@ -342,8 +342,6 @@ async function handleSubmitPost(req, res, token) {
342
342
  return sendJson(res, 400, { error: 'Missing slug or response' });
343
343
  }
344
344
 
345
- const responseString = typeof response === 'string' ? response : JSON.stringify(response);
346
-
347
345
  // Add to pending interactions for CLI to pick up
348
346
  session.pendingInteractions.push({
349
347
  slug,
@@ -351,23 +349,6 @@ async function handleSubmitPost(req, res, token) {
351
349
  response,
352
350
  });
353
351
 
354
- // Log to history (include answer preview)
355
- const event = {
356
- timestamp: new Date().toISOString(),
357
- event: 'INTERACTION_SUBMITTED',
358
- slug,
359
- targetKey: targetKey || `_interaction_${slug}`,
360
- answer: responseString.substring(0, 200) + (responseString.length > 200 ? '...' : ''),
361
- source: 'remote',
362
- };
363
- session.history.unshift(event);
364
-
365
- // Broadcast to browsers
366
- broadcastToSession(token, {
367
- type: 'event',
368
- ...event,
369
- });
370
-
371
352
  return sendJson(res, 200, { success: true });
372
353
  }
373
354