agent-state-machine 2.1.8 → 2.2.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 (28) hide show
  1. package/README.md +55 -5
  2. package/bin/cli.js +1 -140
  3. package/lib/config-utils.js +259 -0
  4. package/lib/file-tree.js +366 -0
  5. package/lib/index.js +109 -2
  6. package/lib/llm.js +35 -5
  7. package/lib/runtime/agent.js +146 -118
  8. package/lib/runtime/model-resolution.js +128 -0
  9. package/lib/runtime/runtime.js +13 -2
  10. package/lib/runtime/track-changes.js +252 -0
  11. package/package.json +1 -1
  12. package/templates/project-builder/agents/assumptions-clarifier.md +0 -8
  13. package/templates/project-builder/agents/code-reviewer.md +0 -8
  14. package/templates/project-builder/agents/code-writer.md +0 -10
  15. package/templates/project-builder/agents/requirements-clarifier.md +0 -7
  16. package/templates/project-builder/agents/roadmap-generator.md +0 -10
  17. package/templates/project-builder/agents/scope-clarifier.md +0 -6
  18. package/templates/project-builder/agents/security-clarifier.md +0 -9
  19. package/templates/project-builder/agents/security-reviewer.md +0 -12
  20. package/templates/project-builder/agents/task-planner.md +0 -10
  21. package/templates/project-builder/agents/test-planner.md +0 -9
  22. package/templates/project-builder/config.js +13 -1
  23. package/templates/starter/config.js +12 -1
  24. package/vercel-server/public/remote/assets/{index-Cunx4VJE.js → index-BOKpYANC.js} +32 -27
  25. package/vercel-server/public/remote/assets/index-DHL_iHQW.css +1 -0
  26. package/vercel-server/public/remote/index.html +2 -2
  27. package/vercel-server/ui/src/components/ContentCard.jsx +6 -6
  28. package/vercel-server/public/remote/assets/index-D02x57pS.css +0 -1
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
12
12
  import { pathToFileURL } from 'url';
13
13
  import { getCurrentRuntime } from './runtime.js';
14
14
  import { formatInteractionPrompt } from './interaction.js';
15
+ import { withChangeTracking } from './track-changes.js';
15
16
 
16
17
  const require = createRequire(import.meta.url);
17
18
 
@@ -164,25 +165,37 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
164
165
  _config: {
165
166
  models: runtime.workflowConfig.models,
166
167
  apiKeys: runtime.workflowConfig.apiKeys,
167
- workflowDir: runtime.workflowDir
168
+ workflowDir: runtime.workflowDir,
169
+ projectRoot: runtime.workflowConfig.projectRoot
168
170
  }
169
171
  };
170
172
 
171
- let result = await handler(context);
172
- let interactionDepth = 0;
173
-
174
- // Handle interaction response from JS agent (support multiple rounds)
175
- while (result && result._interaction) {
176
- const interactionResponse = await handleInteraction(runtime, result._interaction, name);
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`);
173
+ // Execute handler with optional file change tracking
174
+ const executeHandler = async () => {
175
+ let result = await handler(context);
176
+ let interactionDepth = 0;
177
+
178
+ // Handle interaction response from JS agent (support multiple rounds)
179
+ while (result && result._interaction) {
180
+ const interactionResponse = await handleInteraction(runtime, result._interaction, name);
181
+ const resumedContext = { ...context, userResponse: interactionResponse };
182
+ await logAgentStart(runtime, name);
183
+ result = await handler(resumedContext);
184
+ interactionDepth += 1;
185
+ if (interactionDepth > 5) {
186
+ throw new Error(`Agent ${name} exceeded maximum interaction depth`);
187
+ }
183
188
  }
184
- }
185
189
 
190
+ return result;
191
+ };
192
+
193
+ let result;
194
+ if (runtime.workflowConfig.fileTracking !== false) {
195
+ result = await withChangeTracking(runtime, name, executeHandler);
196
+ } else {
197
+ result = await executeHandler();
198
+ }
186
199
 
187
200
  // Clean internal properties from result
188
201
  if (result && typeof result === 'object') {
@@ -191,6 +204,7 @@ async function executeJSAgent(runtime, agentPath, name, params, options = {}) {
191
204
  delete cleanResult._config;
192
205
  delete cleanResult._loop;
193
206
  delete cleanResult._interaction;
207
+ delete cleanResult._files;
194
208
 
195
209
  // If agent returned a context-like object, only return non-internal keys
196
210
  const meaningfulKeys = Object.keys(cleanResult).filter((k) => !k.startsWith('_'));
@@ -235,127 +249,141 @@ async function executeMDAgent(runtime, agentPath, name, params, options = {}) {
235
249
  ? runtime.loadSteeringFiles(steeringNames)
236
250
  : runtime.steering;
237
251
 
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
- };
254
-
255
- // Interpolate variables in prompt
256
- const interpolatedPrompt = interpolatePrompt(prompt, context);
252
+ // Build base config (used for all iterations)
253
+ const baseConfig = {
254
+ models: runtime.workflowConfig.models,
255
+ apiKeys: runtime.workflowConfig.apiKeys,
256
+ workflowDir: runtime.workflowDir,
257
+ projectRoot: runtime.workflowConfig.projectRoot
258
+ };
257
259
 
258
- const model = config.model || 'fast';
260
+ // Execute the MD agent core logic
261
+ const executeMDAgentCore = async () => {
262
+ let response = null;
263
+ let output = null;
264
+ let interactionDepth = 0;
265
+ let currentParams = params;
266
+
267
+ while (true) {
268
+ // Build context - only spread params, NOT memory (explicit context passing)
269
+ const context = {
270
+ ...currentParams,
271
+ _steering: steeringContext,
272
+ _config: baseConfig
273
+ };
274
+
275
+ // Interpolate variables in prompt
276
+ const interpolatedPrompt = interpolatePrompt(prompt, context);
277
+
278
+ const model = config.model || 'fast';
279
+
280
+ const fullPrompt = buildPrompt(context, {
281
+ model,
282
+ prompt: interpolatedPrompt,
283
+ includeContext: config.includeContext !== 'false',
284
+ responseType: config.response
285
+ });
259
286
 
260
- const fullPrompt = buildPrompt(context, {
261
- model,
262
- prompt: interpolatedPrompt,
263
- includeContext: config.includeContext !== 'false',
264
- responseType: config.response
265
- });
287
+ await logAgentStart(runtime, name, fullPrompt);
266
288
 
267
- await logAgentStart(runtime, name, fullPrompt);
289
+ console.log(` Using model: ${model}`);
268
290
 
269
- console.log(` Using model: ${model}`);
291
+ response = await llm(context, {
292
+ model: model,
293
+ prompt: interpolatedPrompt,
294
+ includeContext: config.includeContext !== 'false',
295
+ responseType: config.response
296
+ });
270
297
 
271
- response = await llm(context, {
272
- model: model,
273
- prompt: interpolatedPrompt,
274
- includeContext: config.includeContext !== 'false',
275
- responseType: config.response
276
- });
298
+ // Parse output based on format
299
+ output = response.text;
300
+ if (config.format === 'json') {
301
+ try {
302
+ output = parseJSON(response.text);
303
+ } catch {
304
+ console.warn(` Warning: Failed to parse JSON output`);
305
+ }
306
+ }
277
307
 
278
- // Parse output based on format
279
- 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`);
308
+ if (output && typeof output === 'object' && output._interaction) {
309
+ const interactionResponse = await handleInteraction(runtime, output._interaction, name);
310
+ currentParams = { ...params, userResponse: interactionResponse };
311
+ interactionDepth += 1;
312
+ if (interactionDepth > 5) {
313
+ throw new Error(`Agent ${name} exceeded maximum interaction depth`);
314
+ }
315
+ continue;
285
316
  }
317
+
318
+ break;
286
319
  }
287
320
 
288
- if (output && typeof output === 'object' && output._interaction) {
289
- const interactionResponse = await handleInteraction(runtime, output._interaction, name);
290
- currentParams = { ...params, userResponse: interactionResponse };
291
- interactionDepth += 1;
292
- if (interactionDepth > 5) {
293
- throw new Error(`Agent ${name} exceeded maximum interaction depth`);
321
+ // Check for interaction request
322
+ const parsedInteraction = parseInteractionRequest(response.text);
323
+ const structuredInteraction =
324
+ config.autoInteract !== 'false' && parsedInteraction.isInteraction;
325
+
326
+ // Check if agent returned an 'interact' object in its JSON response
327
+ const hasInteractKey = output && typeof output === 'object' && output.interact;
328
+
329
+ // Explicit interaction mode (format: interaction OR interaction: true)
330
+ // But only trigger if agent actually wants to interact (has interact key or parsed interaction)
331
+ const explicitInteraction =
332
+ config.format === 'interaction' ||
333
+ ((config.interaction === 'true' || (typeof config.interaction === 'string' && config.interaction.length > 0)) &&
334
+ (hasInteractKey || structuredInteraction));
335
+
336
+ if (explicitInteraction || structuredInteraction) {
337
+ // Use interact object if present, otherwise fall back to parsed/raw
338
+ const interactionData = hasInteractKey ? output.interact : (structuredInteraction ? parsedInteraction : null);
339
+
340
+ const slugRaw =
341
+ interactionData?.slug ||
342
+ (typeof config.interaction === 'string' && config.interaction !== 'true'
343
+ ? config.interaction
344
+ : null) ||
345
+ config.interactionSlug ||
346
+ config.interactionKey ||
347
+ name;
348
+
349
+ const slug = sanitizeSlug(slugRaw);
350
+ const targetKey = config.interactionKey || outputKey || slug;
351
+
352
+ // Build interaction object with full metadata
353
+ const interactionObj = hasInteractKey ? {
354
+ ...output.interact,
355
+ slug,
356
+ targetKey
357
+ } : {
358
+ slug,
359
+ targetKey,
360
+ content: structuredInteraction ? parsedInteraction.question : response.text
361
+ };
362
+
363
+ const userResponse = await handleInteraction(runtime, interactionObj, name);
364
+
365
+ // Return the user's response as the agent result
366
+ if (outputKey) {
367
+ return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
294
368
  }
295
- continue;
296
- }
297
369
 
298
- break;
299
- }
370
+ return userResponse;
371
+ }
300
372
 
301
- // Check for interaction request
302
- const parsedInteraction = parseInteractionRequest(response.text);
303
- const structuredInteraction =
304
- config.autoInteract !== 'false' && parsedInteraction.isInteraction;
305
-
306
- // Check if agent returned an 'interact' object in its JSON response
307
- const hasInteractKey = output && typeof output === 'object' && output.interact;
308
-
309
- // Explicit interaction mode (format: interaction OR interaction: true)
310
- // But only trigger if agent actually wants to interact (has interact key or parsed interaction)
311
- const explicitInteraction =
312
- config.format === 'interaction' ||
313
- ((config.interaction === 'true' || (typeof config.interaction === 'string' && config.interaction.length > 0)) &&
314
- (hasInteractKey || structuredInteraction));
315
-
316
- if (explicitInteraction || structuredInteraction) {
317
- // Use interact object if present, otherwise fall back to parsed/raw
318
- const interactionData = hasInteractKey ? output.interact : (structuredInteraction ? parsedInteraction : null);
319
-
320
- const slugRaw =
321
- interactionData?.slug ||
322
- (typeof config.interaction === 'string' && config.interaction !== 'true'
323
- ? config.interaction
324
- : null) ||
325
- config.interactionSlug ||
326
- config.interactionKey ||
327
- name;
328
-
329
- const slug = sanitizeSlug(slugRaw);
330
- const targetKey = config.interactionKey || outputKey || slug;
331
-
332
- // Build interaction object with full metadata
333
- const interactionObj = hasInteractKey ? {
334
- ...output.interact,
335
- slug,
336
- targetKey
337
- } : {
338
- slug,
339
- targetKey,
340
- content: structuredInteraction ? parsedInteraction.question : response.text
341
- };
342
-
343
- const userResponse = await handleInteraction(runtime, interactionObj, name);
344
-
345
- // Return the user's response as the agent result
373
+ // Return result object
346
374
  if (outputKey) {
347
- return { [outputKey]: userResponse, _debug_prompt: response.fullPrompt };
375
+ return { [outputKey]: output, _debug_prompt: response.fullPrompt };
348
376
  }
349
377
 
350
- return userResponse;
351
- }
378
+ return output;
379
+ };
352
380
 
353
- // Return result object
354
- if (outputKey) {
355
- return { [outputKey]: output, _debug_prompt: response.fullPrompt };
381
+ // Execute with optional file change tracking
382
+ if (runtime.workflowConfig.fileTracking !== false) {
383
+ return withChangeTracking(runtime, name, executeMDAgentCore);
384
+ } else {
385
+ return executeMDAgentCore();
356
386
  }
357
-
358
- return output;
359
387
  }
360
388
 
361
389
  /**
@@ -0,0 +1,128 @@
1
+ /**
2
+ * File: /lib/runtime/model-resolution.js
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { askHuman } from './prompt.js';
8
+ import { createInteraction, parseInteractionResponse } from './interaction.js';
9
+ import { readModelFromConfig, writeModelToConfig } from '../config-utils.js';
10
+
11
+ function sanitizeSlug(value) {
12
+ return String(value)
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]+/g, '-')
15
+ .replace(/^-+|-+$/g, '')
16
+ .substring(0, 40) || 'model';
17
+ }
18
+
19
+ export function buildModelSuggestions(availableCLIs = {}, existingModels = {}) {
20
+ const options = [];
21
+ const lookup = {};
22
+
23
+ const addOption = (key, label, description, value) => {
24
+ options.push({ key, label, description });
25
+ lookup[key] = value;
26
+ };
27
+
28
+ Object.entries(existingModels).forEach(([key, value]) => {
29
+ const optionKey = `existing:${key}`;
30
+ const description = value ? `Maps to "${value}"` : 'Existing model mapping';
31
+ addOption(optionKey, `Use existing model: ${key}`, description, value);
32
+ });
33
+
34
+ if (availableCLIs.claude) {
35
+ addOption('cli:claude', 'claude -p', 'Claude CLI (print mode)', 'claude -p');
36
+ }
37
+ if (availableCLIs.gemini) {
38
+ addOption('cli:gemini', 'gemini', 'Gemini CLI', 'gemini');
39
+ }
40
+ if (availableCLIs.codex) {
41
+ addOption('cli:codex', 'codex', 'Codex CLI (exec)', 'codex');
42
+ }
43
+ if (availableCLIs.ollama) {
44
+ addOption('cli:ollama', 'ollama run llama3.1', 'Ollama CLI (example model)', 'ollama run llama3.1');
45
+ }
46
+
47
+ addOption(
48
+ 'api:openai',
49
+ 'api:openai:gpt-4.1-mini',
50
+ 'OpenAI API (example model)',
51
+ 'api:openai:gpt-4.1-mini'
52
+ );
53
+ addOption(
54
+ 'api:anthropic',
55
+ 'api:anthropic:claude-3-5-sonnet-20241022',
56
+ 'Anthropic API (example model)',
57
+ 'api:anthropic:claude-3-5-sonnet-20241022'
58
+ );
59
+
60
+ return { options, lookup };
61
+ }
62
+
63
+ export async function promptForModelConfig(modelKey, existingModels = {}, availableCLIs = {}) {
64
+ const existingKeys = Object.keys(existingModels);
65
+ const existingSummary = existingKeys.length > 0
66
+ ? `Existing models: ${existingKeys.join(', ')}`
67
+ : 'No models configured yet.';
68
+
69
+ const { options, lookup } = buildModelSuggestions(availableCLIs, existingModels);
70
+ const prompt = `Unknown model key: "${modelKey}"\n\nHow would you like to configure this model?\n${existingSummary}`;
71
+ const slug = `model-${sanitizeSlug(modelKey)}`;
72
+ const interaction = createInteraction('choice', slug, {
73
+ prompt,
74
+ options,
75
+ allowCustom: true,
76
+ multiSelect: false
77
+ });
78
+
79
+ const answer = await askHuman(prompt, { slug, interaction });
80
+ const parsed = await parseInteractionResponse(interaction, answer);
81
+
82
+ if (parsed.isCustom && parsed.customText) {
83
+ return parsed.customText.trim();
84
+ }
85
+
86
+ if (parsed.selectedKey) {
87
+ const mapped = lookup[parsed.selectedKey];
88
+ if (mapped) return mapped;
89
+ return String(parsed.selectedKey).trim();
90
+ }
91
+
92
+ if (typeof parsed.text === 'string' && parsed.text.trim()) {
93
+ return parsed.text.trim();
94
+ }
95
+
96
+ if (typeof parsed.raw === 'string' && parsed.raw.trim()) {
97
+ return parsed.raw.trim();
98
+ }
99
+
100
+ throw new Error('No model configuration provided.');
101
+ }
102
+
103
+ export async function resolveUnknownModel(modelKey, config, workflowDir, options = {}) {
104
+ if (!workflowDir) {
105
+ throw new Error('Cannot resolve model without a workflow directory.');
106
+ }
107
+
108
+ const configFile = path.join(workflowDir, 'config.js');
109
+ if (!fs.existsSync(configFile)) {
110
+ throw new Error(`config.js not found in ${workflowDir}`);
111
+ }
112
+
113
+ const existing = readModelFromConfig(configFile, modelKey);
114
+ if (existing) {
115
+ return existing;
116
+ }
117
+
118
+ const existingModels = config?.models || {};
119
+ const availableCLIs = options.availableCLIs || {};
120
+ const modelValue = await promptForModelConfig(modelKey, existingModels, availableCLIs);
121
+
122
+ if (!modelValue) {
123
+ throw new Error('Model configuration cannot be empty.');
124
+ }
125
+
126
+ writeModelToConfig(configFile, modelKey, modelValue);
127
+ return modelValue;
128
+ }
@@ -12,6 +12,7 @@ import readline from 'readline';
12
12
  import { pathToFileURL } from 'url';
13
13
  import { createMemoryProxy } from './memory.js';
14
14
  import { RemoteClient } from '../remote/client.js';
15
+ import { DEFAULT_IGNORE } from '../file-tree.js';
15
16
 
16
17
  // Global runtime reference for agent() and memory access
17
18
  // stored on globalThis to ensure singleton access across different module instances (CLI vs local)
@@ -74,7 +75,12 @@ export class WorkflowRuntime {
74
75
  this.workflowConfig = {
75
76
  models: {},
76
77
  apiKeys: {},
77
- description: ''
78
+ description: '',
79
+ // File tracking defaults
80
+ projectRoot: path.resolve(workflowDir, '../..'),
81
+ fileTracking: true,
82
+ fileTrackingIgnore: DEFAULT_IGNORE,
83
+ fileTrackingKeepDeleted: false
78
84
  };
79
85
 
80
86
  // Load steering
@@ -317,7 +323,12 @@ export class WorkflowRuntime {
317
323
  this.workflowConfig = {
318
324
  models: cfg.models || {},
319
325
  apiKeys: cfg.apiKeys || {},
320
- description: cfg.description || ''
326
+ description: cfg.description || '',
327
+ // File tracking configuration
328
+ projectRoot: cfg.projectRoot || path.resolve(this.workflowDir, '../..'),
329
+ fileTracking: cfg.fileTracking ?? true,
330
+ fileTrackingIgnore: cfg.fileTrackingIgnore || DEFAULT_IGNORE,
331
+ fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false
321
332
  };
322
333
 
323
334
  // Import workflow module