agent-state-machine 2.0.14 → 2.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.
Files changed (67) hide show
  1. package/bin/cli.js +1 -1
  2. package/lib/index.js +33 -0
  3. package/lib/remote/client.js +7 -2
  4. package/lib/runtime/agent.js +102 -67
  5. package/lib/runtime/index.js +13 -0
  6. package/lib/runtime/interaction.js +304 -0
  7. package/lib/runtime/prompt.js +39 -12
  8. package/lib/runtime/runtime.js +11 -10
  9. package/package.json +2 -1
  10. package/templates/project-builder/README.md +119 -0
  11. package/templates/project-builder/agents/assumptions-clarifier.md +65 -0
  12. package/templates/project-builder/agents/code-reviewer.md +81 -0
  13. package/templates/project-builder/agents/code-writer.md +74 -0
  14. package/templates/project-builder/agents/requirements-clarifier.md +55 -0
  15. package/templates/project-builder/agents/response-interpreter.md +25 -0
  16. package/templates/project-builder/agents/roadmap-generator.md +73 -0
  17. package/templates/project-builder/agents/sanity-checker.md +45 -0
  18. package/templates/project-builder/agents/sanity-runner.js +161 -0
  19. package/templates/project-builder/agents/scope-clarifier.md +44 -0
  20. package/templates/project-builder/agents/security-clarifier.md +71 -0
  21. package/templates/project-builder/agents/security-reviewer.md +71 -0
  22. package/templates/project-builder/agents/task-planner.md +62 -0
  23. package/templates/project-builder/agents/test-planner.md +76 -0
  24. package/templates/project-builder/config.js +13 -0
  25. package/templates/project-builder/scripts/interaction-helpers.js +33 -0
  26. package/templates/project-builder/scripts/mac-notification.js +24 -0
  27. package/templates/project-builder/scripts/text-human.js +92 -0
  28. package/templates/project-builder/scripts/workflow-helpers.js +122 -0
  29. package/templates/project-builder/state/current.json +9 -0
  30. package/templates/project-builder/state/history.jsonl +0 -0
  31. package/templates/project-builder/steering/config.json +5 -0
  32. package/templates/project-builder/steering/global.md +19 -0
  33. package/templates/project-builder/workflow.js +554 -0
  34. package/templates/starter/README.md +118 -0
  35. package/templates/starter/agents/example.js +36 -0
  36. package/templates/starter/agents/yoda-greeter.md +12 -0
  37. package/templates/starter/agents/yoda-name-collector.md +12 -0
  38. package/templates/starter/config.js +12 -0
  39. package/templates/starter/interactions/.gitkeep +0 -0
  40. package/templates/starter/scripts/mac-notification.js +24 -0
  41. package/templates/starter/state/current.json +9 -0
  42. package/templates/starter/state/history.jsonl +0 -0
  43. package/templates/starter/steering/config.json +5 -0
  44. package/templates/starter/steering/global.md +19 -0
  45. package/templates/starter/workflow.js +52 -0
  46. package/vercel-server/api/session/[token].js +3 -3
  47. package/vercel-server/api/submit/[token].js +5 -3
  48. package/vercel-server/local-server.js +33 -6
  49. package/vercel-server/public/remote/index.html +17 -0
  50. package/vercel-server/ui/index.html +9 -1012
  51. package/vercel-server/ui/package-lock.json +2650 -0
  52. package/vercel-server/ui/package.json +25 -0
  53. package/vercel-server/ui/postcss.config.js +6 -0
  54. package/vercel-server/ui/src/App.jsx +236 -0
  55. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
  56. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
  57. package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
  58. package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
  59. package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
  60. package/vercel-server/ui/src/components/Footer.jsx +66 -0
  61. package/vercel-server/ui/src/components/Header.jsx +38 -0
  62. package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
  63. package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
  64. package/vercel-server/ui/src/index.css +145 -0
  65. package/vercel-server/ui/src/main.jsx +8 -0
  66. package/vercel-server/ui/tailwind.config.js +19 -0
  67. package/vercel-server/ui/vite.config.js +11 -0
package/bin/cli.js CHANGED
@@ -394,7 +394,7 @@ async function runOrResume(
394
394
  // Enable remote follow mode if we have a URL
395
395
  if (remoteUrl) {
396
396
  const sessionToken = ensureRemotePath(configFile, { forceNew: forceNewRemotePath });
397
- await runtime.enableRemote(remoteUrl, { sessionToken });
397
+ await runtime.enableRemote(remoteUrl, { sessionToken, uiBaseUrl: useLocalServer });
398
398
  }
399
399
 
400
400
  try {
package/lib/index.js CHANGED
@@ -12,6 +12,17 @@ import {
12
12
  agent,
13
13
  executeAgent,
14
14
  askHuman,
15
+ InteractionSchema,
16
+ InteractionResponseSchema,
17
+ normalizeInteraction,
18
+ validateInteraction,
19
+ normalizeInteractionResponse,
20
+ validateInteractionResponse,
21
+ createInteraction,
22
+ formatInteractionPrompt,
23
+ matchSingleSelect,
24
+ matchMultiSelect,
25
+ parseInteractionResponse,
15
26
  parallel,
16
27
  parallelLimit,
17
28
  getMemory,
@@ -83,6 +94,17 @@ export {
83
94
  agent,
84
95
  executeAgent,
85
96
  askHuman,
97
+ InteractionSchema,
98
+ InteractionResponseSchema,
99
+ normalizeInteraction,
100
+ validateInteraction,
101
+ normalizeInteractionResponse,
102
+ validateInteractionResponse,
103
+ createInteraction,
104
+ formatInteractionPrompt,
105
+ matchSingleSelect,
106
+ matchMultiSelect,
107
+ parseInteractionResponse,
86
108
  parallel,
87
109
  parallelLimit,
88
110
  getCurrentRuntime,
@@ -100,6 +122,17 @@ const api = {
100
122
  agent,
101
123
  executeAgent,
102
124
  askHuman,
125
+ InteractionSchema,
126
+ InteractionResponseSchema,
127
+ normalizeInteraction,
128
+ validateInteraction,
129
+ normalizeInteractionResponse,
130
+ validateInteractionResponse,
131
+ createInteraction,
132
+ formatInteractionPrompt,
133
+ matchSingleSelect,
134
+ matchMultiSelect,
135
+ parseInteractionResponse,
103
136
  parallel,
104
137
  parallelLimit,
105
138
  getCurrentRuntime,
@@ -81,12 +81,14 @@ export class RemoteClient {
81
81
  * @param {function} options.onInteractionResponse - Callback when interaction response received
82
82
  * @param {function} [options.onStatusChange] - Callback when connection status changes
83
83
  * @param {string} [options.sessionToken] - Optional session token to reuse
84
+ * @param {boolean} [options.uiBaseUrl] - If true, return base URL for UI instead of /s/{token}
84
85
  */
85
86
  constructor(options) {
86
87
  this.serverUrl = options.serverUrl.replace(/\/$/, ''); // Remove trailing slash
87
88
  this.workflowName = options.workflowName;
88
89
  this.onInteractionResponse = options.onInteractionResponse;
89
90
  this.onStatusChange = options.onStatusChange || (() => {});
91
+ this.uiBaseUrl = Boolean(options.uiBaseUrl);
90
92
 
91
93
  this.sessionToken = options.sessionToken || generateSessionToken();
92
94
  this.connected = false;
@@ -99,6 +101,9 @@ export class RemoteClient {
99
101
  * Get the full remote URL for browser access
100
102
  */
101
103
  getRemoteUrl() {
104
+ if (this.uiBaseUrl) {
105
+ return this.serverUrl;
106
+ }
102
107
  return `${this.serverUrl}/s/${this.sessionToken}`;
103
108
  }
104
109
 
@@ -187,9 +192,9 @@ export class RemoteClient {
187
192
  }
188
193
 
189
194
  await this.send({
190
- type: 'event',
191
- sessionToken: this.sessionToken,
192
195
  ...event,
196
+ type: 'event', // Must come after spread to not be overwritten by event.type
197
+ sessionToken: this.sessionToken,
193
198
  });
194
199
  }
195
200
 
@@ -11,6 +11,7 @@ import path from 'path';
11
11
  import { createRequire } from 'module';
12
12
  import { pathToFileURL } from 'url';
13
13
  import { getCurrentRuntime } from './runtime.js';
14
+ import { formatInteractionPrompt } from './interaction.js';
14
15
 
15
16
  const require = createRequire(import.meta.url);
16
17
 
@@ -48,12 +49,7 @@ export async function agent(name, params = {}, options = {}) {
48
49
  }
49
50
 
50
51
  console.log(` [Agent: ${name}] Completed`);
51
- if (runtime._agentSuppressCompletion?.has(name)) {
52
- runtime._agentSuppressCompletion.delete(name);
53
- return result;
54
- }
55
-
56
- runtime.prependHistory({
52
+ await runtime.prependHistory({
57
53
  event: 'AGENT_COMPLETED',
58
54
  agent: name,
59
55
  output: result,
@@ -66,7 +62,7 @@ export async function agent(name, params = {}, options = {}) {
66
62
 
67
63
  if (attempt < retryCount) {
68
64
  console.error(` [Agent: ${name}] Error (attempt ${attempt + 1}/${retryCount + 1}): ${error.message}`);
69
- runtime.prependHistory({
65
+ await runtime.prependHistory({
70
66
  event: 'AGENT_RETRY',
71
67
  agent: name,
72
68
  attempt: attempt + 1,
@@ -77,7 +73,7 @@ export async function agent(name, params = {}, options = {}) {
77
73
  }
78
74
 
79
75
  // All retries exhausted - record failure
80
- runtime.prependHistory({
76
+ await runtime.prependHistory({
81
77
  event: 'AGENT_FAILED',
82
78
  agent: name,
83
79
  error: lastError.message,
@@ -154,7 +150,7 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
154
150
  throw new Error(`Agent ${name} does not export a function`);
155
151
  }
156
152
 
157
- logAgentStart(runtime, name);
153
+ await logAgentStart(runtime, name);
158
154
 
159
155
  // Build steering context (global + any additional files from options)
160
156
  const steeringContext = options.steering
@@ -172,25 +168,22 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
172
168
  }
173
169
  };
174
170
 
175
- const result = await handler(context);
171
+ let result = await handler(context);
172
+ let interactionDepth = 0;
176
173
 
177
- // Handle interaction response from JS agent
178
- if (result && result._interaction) {
174
+ // Handle interaction response from JS agent (support multiple rounds)
175
+ while (result && result._interaction) {
179
176
  const interactionResponse = await handleInteraction(runtime, result._interaction, name);
180
-
181
- // Use the interaction response as the primary output if it exists
182
- // This allows the workflow to receive the user's input directly
183
- if (typeof result === 'object') {
184
- // Merge response into result or replace?
185
- // For consistency with MDAgent, we might want to return { result: response }
186
- // but JS agents are more flexible.
187
- // Let's assume the agent wants the response mixed in or as the return.
188
- // A safe bet is to return the response if the agent explicitly asked for interaction.
189
- return { ...result, result: interactionResponse };
177
+ const resumedContext = { ...context, userResponse: interactionResponse };
178
+ await logAgentStart(runtime, name);
179
+ result = await handler(resumedContext);
180
+ interactionDepth += 1;
181
+ if (interactionDepth > 5) {
182
+ throw new Error(`Agent ${name} exceeded maximum interaction depth`);
190
183
  }
191
- return interactionResponse;
192
184
  }
193
185
 
186
+
194
187
  // Clean internal properties from result
195
188
  if (result && typeof result === 'object') {
196
189
  const cleanResult = { ...result };
@@ -222,8 +215,7 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
222
215
  const content = fs.readFileSync(agentPath, 'utf-8');
223
216
  const { config, prompt } = parseMarkdownAgent(content);
224
217
 
225
- const outputKey = config.output || 'result';
226
- const targetKey = config.interactionKey || outputKey;
218
+ const outputKey = config.output;
227
219
 
228
220
  // Combine steering from options (runtime call) and frontmatter (static)
229
221
  let steeringNames = [];
@@ -243,46 +235,65 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
243
235
  ? runtime.loadSteeringFiles(steeringNames)
244
236
  : runtime.steering;
245
237
 
246
- // Build context - only spread params, NOT memory (explicit context passing)
247
- const context = {
248
- ...params,
249
- _steering: steeringContext,
250
- _config: {
251
- models: runtime.workflowConfig.models,
252
- apiKeys: runtime.workflowConfig.apiKeys,
253
- workflowDir: runtime.workflowDir
254
- }
255
- };
238
+ let response = null;
239
+ let output = null;
240
+ let interactionDepth = 0;
241
+ let currentParams = params;
242
+
243
+ while (true) {
244
+ // Build context - only spread params, NOT memory (explicit context passing)
245
+ const context = {
246
+ ...currentParams,
247
+ _steering: steeringContext,
248
+ _config: {
249
+ models: runtime.workflowConfig.models,
250
+ apiKeys: runtime.workflowConfig.apiKeys,
251
+ workflowDir: runtime.workflowDir
252
+ }
253
+ };
256
254
 
257
- // Interpolate variables in prompt
258
- const interpolatedPrompt = interpolatePrompt(prompt, context);
255
+ // Interpolate variables in prompt
256
+ const interpolatedPrompt = interpolatePrompt(prompt, context);
259
257
 
260
- const model = config.model || 'fast';
258
+ const model = config.model || 'fast';
261
259
 
262
- const fullPrompt = buildPrompt(context, {
263
- model,
264
- prompt: interpolatedPrompt,
265
- includeContext: config.includeContext !== 'false'
266
- });
260
+ const fullPrompt = buildPrompt(context, {
261
+ model,
262
+ prompt: interpolatedPrompt,
263
+ includeContext: config.includeContext !== 'false'
264
+ });
267
265
 
268
- logAgentStart(runtime, name, fullPrompt);
266
+ await logAgentStart(runtime, name, fullPrompt);
269
267
 
270
- console.log(` Using model: ${model}`);
268
+ console.log(` Using model: ${model}`);
271
269
 
272
- const response = await llm(context, {
273
- model: model,
274
- prompt: interpolatedPrompt,
275
- includeContext: config.includeContext !== 'false'
276
- });
270
+ response = await llm(context, {
271
+ model: model,
272
+ prompt: interpolatedPrompt,
273
+ includeContext: config.includeContext !== 'false'
274
+ });
277
275
 
278
- // Parse output based on format
279
- let output = response.text;
280
- if (config.format === 'json') {
281
- try {
282
- output = parseJSON(response.text);
283
- } catch {
284
- console.warn(` Warning: Failed to parse JSON output`);
276
+ // Parse output based on format
277
+ output = response.text;
278
+ if (config.format === 'json') {
279
+ try {
280
+ output = parseJSON(response.text);
281
+ } catch {
282
+ console.warn(` Warning: Failed to parse JSON output`);
283
+ }
284
+ }
285
+
286
+ if (output && typeof output === 'object' && output._interaction) {
287
+ const interactionResponse = await handleInteraction(runtime, output._interaction, name);
288
+ currentParams = { ...params, userResponse: interactionResponse };
289
+ interactionDepth += 1;
290
+ if (interactionDepth > 5) {
291
+ throw new Error(`Agent ${name} exceeded maximum interaction depth`);
292
+ }
293
+ continue;
285
294
  }
295
+
296
+ break;
286
297
  }
287
298
 
288
299
  // Check for interaction request
@@ -302,7 +313,6 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
302
313
  : null) ||
303
314
  config.interactionSlug ||
304
315
  config.interactionKey ||
305
- outputKey ||
306
316
  name;
307
317
 
308
318
  const slug = sanitizeSlug(slugRaw);
@@ -316,11 +326,19 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
316
326
  }, name);
317
327
 
318
328
  // Return the user's response as the agent result
319
- return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
329
+ if (outputKey) {
330
+ return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
331
+ }
332
+
333
+ return userResponse;
320
334
  }
321
335
 
322
336
  // Return result object
323
- return { [outputKey]: output, _debug_prompt: response.fullPrompt };
337
+ if (outputKey) {
338
+ return { [outputKey]: output, _debug_prompt: response.fullPrompt };
339
+ }
340
+
341
+ return output;
324
342
  }
325
343
 
326
344
  /**
@@ -435,7 +453,16 @@ async function handleInteraction(runtime, interaction, agentName) {
435
453
 
436
454
  const slug = sanitizeSlug(interaction.slug);
437
455
  const targetKey = String(interaction.targetKey || slug);
438
- const content = String(interaction.content || '').trim();
456
+ const prompt = String(
457
+ interaction.prompt ??
458
+ interaction.content ??
459
+ interaction.question ??
460
+ ''
461
+ ).trim();
462
+ const content = formatInteractionPrompt({
463
+ ...interaction,
464
+ prompt
465
+ });
439
466
 
440
467
  const filePath = path.join(runtime.interactionsDir, `${slug}.md`);
441
468
 
@@ -448,15 +475,23 @@ ${content}
448
475
 
449
476
  fs.writeFileSync(filePath, fileContent);
450
477
 
451
- runtime.prependHistory({
478
+ await runtime.prependHistory({
452
479
  event: 'INTERACTION_REQUESTED',
453
480
  slug,
454
481
  targetKey,
455
- question: content
482
+ question: prompt || content,
483
+ type: interaction.type || 'text',
484
+ prompt,
485
+ options: interaction.options,
486
+ allowCustom: interaction.allowCustom,
487
+ multiSelect: interaction.multiSelect,
488
+ validation: interaction.validation,
489
+ confirmLabel: interaction.confirmLabel,
490
+ cancelLabel: interaction.cancelLabel,
491
+ context: interaction.context
456
492
  });
457
493
 
458
494
  if (effectiveAgentName) {
459
- runtime._agentSuppressCompletion?.add(effectiveAgentName);
460
495
  runtime._agentResumeFlags?.add(effectiveAgentName);
461
496
  }
462
497
 
@@ -466,10 +501,10 @@ ${content}
466
501
  return response;
467
502
  }
468
503
 
469
- function logAgentStart(runtime, name, prompt) {
504
+ async function logAgentStart(runtime, name, prompt) {
470
505
  if (runtime._agentResumeFlags?.has(name)) {
471
506
  runtime._agentResumeFlags.delete(name);
472
- runtime.prependHistory({
507
+ await runtime.prependHistory({
473
508
  event: 'AGENT_RESUMED',
474
509
  agent: name
475
510
  });
@@ -485,5 +520,5 @@ function logAgentStart(runtime, name, prompt) {
485
520
  entry.prompt = prompt;
486
521
  }
487
522
 
488
- runtime.prependHistory(entry);
523
+ await runtime.prependHistory(entry);
489
524
  }
@@ -15,6 +15,19 @@ export {
15
15
 
16
16
  export { agent, executeAgent } from './agent.js';
17
17
  export { askHuman } from './prompt.js';
18
+ export {
19
+ InteractionSchema,
20
+ InteractionResponseSchema,
21
+ normalizeInteraction,
22
+ validateInteraction,
23
+ normalizeInteractionResponse,
24
+ validateInteractionResponse,
25
+ createInteraction,
26
+ formatInteractionPrompt,
27
+ matchSingleSelect,
28
+ matchMultiSelect,
29
+ parseInteractionResponse
30
+ } from './interaction.js';
18
31
  export { parallel, parallelLimit } from './parallel.js';
19
32
  export { createMemoryProxy } from './memory.js';
20
33