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.
- package/bin/cli.js +1 -1
- package/lib/index.js +33 -0
- package/lib/remote/client.js +7 -2
- package/lib/runtime/agent.js +102 -67
- package/lib/runtime/index.js +13 -0
- package/lib/runtime/interaction.js +304 -0
- package/lib/runtime/prompt.js +39 -12
- package/lib/runtime/runtime.js +11 -10
- package/package.json +2 -1
- package/templates/project-builder/README.md +119 -0
- package/templates/project-builder/agents/assumptions-clarifier.md +65 -0
- package/templates/project-builder/agents/code-reviewer.md +81 -0
- package/templates/project-builder/agents/code-writer.md +74 -0
- package/templates/project-builder/agents/requirements-clarifier.md +55 -0
- package/templates/project-builder/agents/response-interpreter.md +25 -0
- package/templates/project-builder/agents/roadmap-generator.md +73 -0
- package/templates/project-builder/agents/sanity-checker.md +45 -0
- package/templates/project-builder/agents/sanity-runner.js +161 -0
- package/templates/project-builder/agents/scope-clarifier.md +44 -0
- package/templates/project-builder/agents/security-clarifier.md +71 -0
- package/templates/project-builder/agents/security-reviewer.md +71 -0
- package/templates/project-builder/agents/task-planner.md +62 -0
- package/templates/project-builder/agents/test-planner.md +76 -0
- package/templates/project-builder/config.js +13 -0
- package/templates/project-builder/scripts/interaction-helpers.js +33 -0
- package/templates/project-builder/scripts/mac-notification.js +24 -0
- package/templates/project-builder/scripts/text-human.js +92 -0
- package/templates/project-builder/scripts/workflow-helpers.js +122 -0
- package/templates/project-builder/state/current.json +9 -0
- package/templates/project-builder/state/history.jsonl +0 -0
- package/templates/project-builder/steering/config.json +5 -0
- package/templates/project-builder/steering/global.md +19 -0
- package/templates/project-builder/workflow.js +554 -0
- package/templates/starter/README.md +118 -0
- package/templates/starter/agents/example.js +36 -0
- package/templates/starter/agents/yoda-greeter.md +12 -0
- package/templates/starter/agents/yoda-name-collector.md +12 -0
- package/templates/starter/config.js +12 -0
- package/templates/starter/interactions/.gitkeep +0 -0
- package/templates/starter/scripts/mac-notification.js +24 -0
- package/templates/starter/state/current.json +9 -0
- package/templates/starter/state/history.jsonl +0 -0
- package/templates/starter/steering/config.json +5 -0
- package/templates/starter/steering/global.md +19 -0
- package/templates/starter/workflow.js +52 -0
- package/vercel-server/api/session/[token].js +3 -3
- package/vercel-server/api/submit/[token].js +5 -3
- package/vercel-server/local-server.js +33 -6
- package/vercel-server/public/remote/index.html +17 -0
- package/vercel-server/ui/index.html +9 -1012
- package/vercel-server/ui/package-lock.json +2650 -0
- package/vercel-server/ui/package.json +25 -0
- package/vercel-server/ui/postcss.config.js +6 -0
- package/vercel-server/ui/src/App.jsx +236 -0
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
- package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
- package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
- package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
- package/vercel-server/ui/src/components/Footer.jsx +66 -0
- package/vercel-server/ui/src/components/Header.jsx +38 -0
- package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
- package/vercel-server/ui/src/index.css +145 -0
- package/vercel-server/ui/src/main.jsx +8 -0
- package/vercel-server/ui/tailwind.config.js +19 -0
- package/vercel-server/ui/vite.config.js +11 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Schema and Helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides structured interaction types (choice, text, confirm) for human-in-the-loop workflows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Default schema for interactions
|
|
8
|
+
export const InteractionSchema = {
|
|
9
|
+
type: 'text',
|
|
10
|
+
slug: '',
|
|
11
|
+
prompt: '',
|
|
12
|
+
options: [],
|
|
13
|
+
allowCustom: true,
|
|
14
|
+
multiSelect: false,
|
|
15
|
+
placeholder: '',
|
|
16
|
+
validation: {
|
|
17
|
+
minLength: 0,
|
|
18
|
+
maxLength: 0,
|
|
19
|
+
pattern: ''
|
|
20
|
+
},
|
|
21
|
+
confirmLabel: 'Confirm',
|
|
22
|
+
cancelLabel: 'Cancel',
|
|
23
|
+
default: '',
|
|
24
|
+
context: {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Default schema for interaction responses
|
|
28
|
+
export const InteractionResponseSchema = {
|
|
29
|
+
slug: '',
|
|
30
|
+
selectedKey: '',
|
|
31
|
+
selectedKeys: [],
|
|
32
|
+
text: '',
|
|
33
|
+
confirmed: false,
|
|
34
|
+
raw: '',
|
|
35
|
+
interpreted: false,
|
|
36
|
+
isCustom: false,
|
|
37
|
+
customText: ''
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize an interaction object by merging with defaults
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeInteraction(interaction) {
|
|
44
|
+
if (!interaction || typeof interaction !== 'object') return null;
|
|
45
|
+
return {
|
|
46
|
+
...InteractionSchema,
|
|
47
|
+
...interaction,
|
|
48
|
+
validation: {
|
|
49
|
+
...InteractionSchema.validation,
|
|
50
|
+
...(interaction.validation || {})
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate an interaction object
|
|
57
|
+
*/
|
|
58
|
+
export function validateInteraction(interaction) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (!interaction || typeof interaction !== 'object') {
|
|
61
|
+
return { valid: false, errors: ['Interaction must be an object'] };
|
|
62
|
+
}
|
|
63
|
+
if (!interaction.type) errors.push('Missing type');
|
|
64
|
+
if (!interaction.slug) errors.push('Missing slug');
|
|
65
|
+
if (!interaction.prompt) errors.push('Missing prompt');
|
|
66
|
+
if (interaction.type === 'choice' && (!Array.isArray(interaction.options) || interaction.options.length === 0)) {
|
|
67
|
+
errors.push('Choice interaction must include options');
|
|
68
|
+
}
|
|
69
|
+
return { valid: errors.length === 0, errors };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalize an interaction response object
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeInteractionResponse(response) {
|
|
76
|
+
if (!response || typeof response !== 'object') return null;
|
|
77
|
+
return {
|
|
78
|
+
...InteractionResponseSchema,
|
|
79
|
+
...response
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate an interaction response
|
|
85
|
+
*/
|
|
86
|
+
export function validateInteractionResponse(response) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
if (!response || typeof response !== 'object') {
|
|
89
|
+
return { valid: false, errors: ['Response must be an object'] };
|
|
90
|
+
}
|
|
91
|
+
if (!('raw' in response)) errors.push('Missing raw response');
|
|
92
|
+
return { valid: errors.length === 0, errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create an interaction object with defaults
|
|
97
|
+
*/
|
|
98
|
+
export function createInteraction(type, slug, options = {}) {
|
|
99
|
+
return normalizeInteraction({
|
|
100
|
+
type,
|
|
101
|
+
slug,
|
|
102
|
+
...options
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format an interaction as a human-readable prompt string
|
|
108
|
+
*/
|
|
109
|
+
export function formatInteractionPrompt(interaction) {
|
|
110
|
+
const prompt = String(interaction?.prompt || interaction?.question || interaction?.content || '').trim();
|
|
111
|
+
const lines = [];
|
|
112
|
+
|
|
113
|
+
if (prompt) lines.push(prompt);
|
|
114
|
+
|
|
115
|
+
if (interaction?.type === 'choice' && Array.isArray(interaction.options)) {
|
|
116
|
+
lines.push('', 'Options:');
|
|
117
|
+
interaction.options.forEach((opt, index) => {
|
|
118
|
+
const letter = String.fromCharCode(65 + index);
|
|
119
|
+
const label = opt.label || opt.key || `Option ${index + 1}`;
|
|
120
|
+
const desc = opt.description ? ` - ${opt.description}` : '';
|
|
121
|
+
lines.push(`- ${letter}: ${label}${desc}`);
|
|
122
|
+
});
|
|
123
|
+
if (interaction.allowCustom) {
|
|
124
|
+
lines.push('- Other: Provide a custom response');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (interaction?.type === 'confirm') {
|
|
129
|
+
const confirmLabel = interaction.confirmLabel || 'Confirm';
|
|
130
|
+
const cancelLabel = interaction.cancelLabel || 'Cancel';
|
|
131
|
+
lines.push('', 'Options:');
|
|
132
|
+
lines.push(`- A: ${confirmLabel}`);
|
|
133
|
+
lines.push(`- B: ${cancelLabel}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join('\n').trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Match a single-select response against options
|
|
141
|
+
* Returns the matched option key/label, or null if no match
|
|
142
|
+
*/
|
|
143
|
+
export function matchSingleSelect(options, input) {
|
|
144
|
+
const lower = String(input || '').toLowerCase().trim();
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
147
|
+
const opt = options[i];
|
|
148
|
+
const letter = String.fromCharCode(65 + i).toLowerCase();
|
|
149
|
+
const key = String(opt.key || '').toLowerCase();
|
|
150
|
+
const label = String(opt.label || '').toLowerCase();
|
|
151
|
+
const letterRegex = new RegExp(`^${letter}(\\s|[-:.)\\]])`, 'i');
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
lower === letter ||
|
|
155
|
+
letterRegex.test(lower) ||
|
|
156
|
+
lower === key ||
|
|
157
|
+
lower === label ||
|
|
158
|
+
lower.startsWith(`${label}:`) ||
|
|
159
|
+
lower.startsWith(`${label} -`)
|
|
160
|
+
) {
|
|
161
|
+
return opt.key || opt.label || letter.toUpperCase();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Match a multi-select response against options
|
|
169
|
+
* Returns array of matched option keys/labels
|
|
170
|
+
*/
|
|
171
|
+
export function matchMultiSelect(options, input) {
|
|
172
|
+
const lower = String(input || '').toLowerCase().trim();
|
|
173
|
+
const tokens = lower.split(/[,\s]+/).filter(Boolean);
|
|
174
|
+
const selections = new Set();
|
|
175
|
+
|
|
176
|
+
// Match letter patterns like "a:", "b)", "c -"
|
|
177
|
+
const letterMatches = lower.matchAll(/(^|[\s,])([a-z])\s*[-:.)\]]/g);
|
|
178
|
+
for (const match of letterMatches) {
|
|
179
|
+
const letter = match[2];
|
|
180
|
+
const index = letter.charCodeAt(0) - 97;
|
|
181
|
+
if (index >= 0 && index < options.length) {
|
|
182
|
+
const opt = options[index];
|
|
183
|
+
selections.add(opt.key || opt.label || letter.toUpperCase());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Match tokens against keys/labels
|
|
188
|
+
tokens.forEach((token) => {
|
|
189
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
190
|
+
const opt = options[i];
|
|
191
|
+
const letter = String.fromCharCode(65 + i).toLowerCase();
|
|
192
|
+
const key = String(opt.key || '').toLowerCase();
|
|
193
|
+
const label = String(opt.label || '').toLowerCase();
|
|
194
|
+
if (token === letter || token === key || token === label) {
|
|
195
|
+
selections.add(opt.key || opt.label || letter.toUpperCase());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return Array.from(selections);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse a raw response string into a structured response object
|
|
205
|
+
*
|
|
206
|
+
* @param {object} interaction - The interaction schema
|
|
207
|
+
* @param {string|object} rawResponse - The raw user response
|
|
208
|
+
* @param {function} [interpreter] - Optional async function to interpret ambiguous responses
|
|
209
|
+
* @returns {object} Structured response object
|
|
210
|
+
*/
|
|
211
|
+
export async function parseInteractionResponse(interaction, rawResponse, interpreter = null) {
|
|
212
|
+
const normalized = normalizeInteraction(interaction);
|
|
213
|
+
if (!normalized) return { raw: String(rawResponse ?? '') };
|
|
214
|
+
|
|
215
|
+
// If already an object, normalize it
|
|
216
|
+
if (rawResponse && typeof rawResponse === 'object') {
|
|
217
|
+
return normalizeInteractionResponse({
|
|
218
|
+
...rawResponse,
|
|
219
|
+
raw: rawResponse.raw ?? rawResponse.text ?? ''
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const raw = String(rawResponse ?? '').trim();
|
|
224
|
+
|
|
225
|
+
// Text type - just return the text
|
|
226
|
+
if (normalized.type === 'text') {
|
|
227
|
+
return { text: raw, raw };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Confirm type - check for yes/no patterns
|
|
231
|
+
if (normalized.type === 'confirm') {
|
|
232
|
+
const lower = raw.toLowerCase();
|
|
233
|
+
const confirmLabel = (normalized.confirmLabel || 'confirm').toLowerCase();
|
|
234
|
+
const cancelLabel = (normalized.cancelLabel || 'cancel').toLowerCase();
|
|
235
|
+
|
|
236
|
+
const confirmed = lower.startsWith('y') || lower.startsWith('a') ||
|
|
237
|
+
lower.startsWith('confirm') || lower === confirmLabel;
|
|
238
|
+
const cancelled = lower.startsWith('n') || lower.startsWith('b') ||
|
|
239
|
+
lower.startsWith('cancel') || lower === cancelLabel;
|
|
240
|
+
|
|
241
|
+
if (confirmed || cancelled) {
|
|
242
|
+
return { confirmed, raw };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Try interpreter if provided
|
|
246
|
+
if (interpreter) {
|
|
247
|
+
const interpreted = await tryInterpreter(interpreter, normalized, raw);
|
|
248
|
+
if (interpreted) return interpreted;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Default to not confirmed for ambiguous responses
|
|
252
|
+
return { confirmed: false, raw };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Choice type - match against options
|
|
256
|
+
if (normalized.type === 'choice') {
|
|
257
|
+
const options = normalized.options || [];
|
|
258
|
+
|
|
259
|
+
if (normalized.multiSelect) {
|
|
260
|
+
const selectedKeys = matchMultiSelect(options, raw);
|
|
261
|
+
if (selectedKeys.length > 0) {
|
|
262
|
+
return { selectedKeys, raw };
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const selectedKey = matchSingleSelect(options, raw);
|
|
266
|
+
if (selectedKey) {
|
|
267
|
+
return { selectedKey, raw };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Try interpreter if provided
|
|
272
|
+
if (interpreter) {
|
|
273
|
+
const interpreted = await tryInterpreter(interpreter, normalized, raw);
|
|
274
|
+
if (interpreted) return interpreted;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Treat as custom response if allowed
|
|
278
|
+
if (normalized.allowCustom !== false) {
|
|
279
|
+
return { isCustom: true, customText: raw, raw };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { raw };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Helper to safely call the interpreter function
|
|
288
|
+
*/
|
|
289
|
+
async function tryInterpreter(interpreter, interaction, raw) {
|
|
290
|
+
try {
|
|
291
|
+
const result = await interpreter(interaction, raw);
|
|
292
|
+
if (result && typeof result === 'object') {
|
|
293
|
+
if (result.selectedKey || result.selectedKeys?.length || result.confirmed !== undefined) {
|
|
294
|
+
return { ...result, raw, interpreted: true };
|
|
295
|
+
}
|
|
296
|
+
if (result.isCustom && interaction.allowCustom !== false) {
|
|
297
|
+
return { ...result, raw, interpreted: true };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
// Silently fail - caller can handle uninterpreted responses
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
package/lib/runtime/prompt.js
CHANGED
|
@@ -35,12 +35,23 @@ export async function askHuman(question, options = {}) {
|
|
|
35
35
|
|
|
36
36
|
const slug = options.slug || generateSlug(question);
|
|
37
37
|
const memoryKey = `_interaction_${slug}`;
|
|
38
|
+
const interaction = options.interaction || null;
|
|
39
|
+
const prompt = interaction?.prompt || question;
|
|
38
40
|
|
|
39
|
-
runtime.prependHistory({
|
|
41
|
+
await runtime.prependHistory({
|
|
40
42
|
event: 'PROMPT_REQUESTED',
|
|
41
43
|
slug,
|
|
42
44
|
targetKey: memoryKey,
|
|
43
|
-
question
|
|
45
|
+
question: prompt,
|
|
46
|
+
type: interaction?.type || 'text',
|
|
47
|
+
prompt,
|
|
48
|
+
options: interaction?.options,
|
|
49
|
+
allowCustom: interaction?.allowCustom,
|
|
50
|
+
multiSelect: interaction?.multiSelect,
|
|
51
|
+
validation: interaction?.validation,
|
|
52
|
+
confirmLabel: interaction?.confirmLabel,
|
|
53
|
+
cancelLabel: interaction?.cancelLabel,
|
|
54
|
+
context: interaction?.context
|
|
44
55
|
});
|
|
45
56
|
|
|
46
57
|
// Check if we're in TTY mode (interactive terminal)
|
|
@@ -49,17 +60,19 @@ export async function askHuman(question, options = {}) {
|
|
|
49
60
|
const answer = await askQuestionWithRemote(runtime, question, slug, memoryKey);
|
|
50
61
|
console.log('');
|
|
51
62
|
|
|
63
|
+
const normalizedAnswer = normalizePromptAnswer(answer);
|
|
64
|
+
|
|
52
65
|
// Save the response to memory
|
|
53
|
-
runtime._rawMemory[memoryKey] =
|
|
66
|
+
runtime._rawMemory[memoryKey] = normalizedAnswer;
|
|
54
67
|
runtime.persist();
|
|
55
68
|
|
|
56
|
-
runtime.prependHistory({
|
|
69
|
+
await runtime.prependHistory({
|
|
57
70
|
event: 'PROMPT_ANSWERED',
|
|
58
71
|
slug,
|
|
59
|
-
answer:
|
|
72
|
+
answer: normalizedAnswer.substring(0, 100) + (normalizedAnswer.length > 100 ? '...' : '')
|
|
60
73
|
});
|
|
61
74
|
|
|
62
|
-
return
|
|
75
|
+
return normalizedAnswer;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
// Non-TTY mode - create interaction file and wait inline
|
|
@@ -73,7 +86,7 @@ ${question}
|
|
|
73
86
|
|
|
74
87
|
fs.writeFileSync(interactionFile, fileContent);
|
|
75
88
|
|
|
76
|
-
runtime.prependHistory({
|
|
89
|
+
await runtime.prependHistory({
|
|
77
90
|
event: 'INTERACTION_REQUESTED',
|
|
78
91
|
slug,
|
|
79
92
|
targetKey: memoryKey,
|
|
@@ -84,16 +97,18 @@ ${question}
|
|
|
84
97
|
// Block and wait for user input (instead of throwing)
|
|
85
98
|
const answer = await runtime.waitForInteraction(interactionFile, slug, memoryKey);
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
const normalizedAnswer = normalizePromptAnswer(answer);
|
|
101
|
+
|
|
102
|
+
runtime._rawMemory[memoryKey] = normalizedAnswer;
|
|
88
103
|
runtime.persist();
|
|
89
104
|
|
|
90
|
-
runtime.prependHistory({
|
|
105
|
+
await runtime.prependHistory({
|
|
91
106
|
event: 'PROMPT_ANSWERED',
|
|
92
107
|
slug,
|
|
93
|
-
answer:
|
|
108
|
+
answer: normalizedAnswer.substring(0, 100) + (normalizedAnswer.length > 100 ? '...' : '')
|
|
94
109
|
});
|
|
95
110
|
|
|
96
|
-
return
|
|
111
|
+
return normalizedAnswer;
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
/**
|
|
@@ -173,4 +188,16 @@ function generateSlug(question) {
|
|
|
173
188
|
.replace(/[^a-z0-9]+/g, '-')
|
|
174
189
|
.replace(/^-+|-+$/g, '')
|
|
175
190
|
.substring(0, 30);
|
|
176
|
-
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizePromptAnswer(answer) {
|
|
194
|
+
if (typeof answer === 'string') return answer;
|
|
195
|
+
if (answer && typeof answer === 'object') {
|
|
196
|
+
if (typeof answer.raw === 'string') return answer.raw;
|
|
197
|
+
if (typeof answer.text === 'string') return answer.text;
|
|
198
|
+
if (typeof answer.selectedKey === 'string') return answer.selectedKey;
|
|
199
|
+
if (Array.isArray(answer.selectedKeys)) return answer.selectedKeys.join(', ');
|
|
200
|
+
if (typeof answer.confirmed === 'boolean') return answer.confirmed ? 'confirm' : 'cancel';
|
|
201
|
+
}
|
|
202
|
+
return String(answer ?? '');
|
|
203
|
+
}
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -88,7 +88,6 @@ export class WorkflowRuntime {
|
|
|
88
88
|
|
|
89
89
|
// Agent interaction tracking for history logging
|
|
90
90
|
this._agentResumeFlags = new Set();
|
|
91
|
-
this._agentSuppressCompletion = new Set();
|
|
92
91
|
|
|
93
92
|
// Agent error tracking (not persisted to memory, but accessible during run)
|
|
94
93
|
this._agentErrors = [];
|
|
@@ -197,7 +196,7 @@ export class WorkflowRuntime {
|
|
|
197
196
|
/**
|
|
198
197
|
* Prepend an event to history.jsonl (newest first)
|
|
199
198
|
*/
|
|
200
|
-
prependHistory(event) {
|
|
199
|
+
async prependHistory(event) {
|
|
201
200
|
const entry = {
|
|
202
201
|
timestamp: new Date().toISOString(),
|
|
203
202
|
...event
|
|
@@ -217,7 +216,7 @@ export class WorkflowRuntime {
|
|
|
217
216
|
|
|
218
217
|
// Forward to remote if connected
|
|
219
218
|
if (this.remoteClient && this.remoteEnabled) {
|
|
220
|
-
this.remoteClient.sendEvent(entry);
|
|
219
|
+
await this.remoteClient.sendEvent(entry);
|
|
221
220
|
}
|
|
222
221
|
}
|
|
223
222
|
|
|
@@ -305,7 +304,7 @@ export class WorkflowRuntime {
|
|
|
305
304
|
if (!this.startedAt) this.startedAt = new Date().toISOString();
|
|
306
305
|
this.persist();
|
|
307
306
|
|
|
308
|
-
this.prependHistory({ event: 'WORKFLOW_STARTED' });
|
|
307
|
+
await this.prependHistory({ event: 'WORKFLOW_STARTED' });
|
|
309
308
|
|
|
310
309
|
const configPath = path.join(this.workflowDir, 'config.js');
|
|
311
310
|
if (!fs.existsSync(configPath)) {
|
|
@@ -334,7 +333,7 @@ export class WorkflowRuntime {
|
|
|
334
333
|
|
|
335
334
|
this.status = 'COMPLETED';
|
|
336
335
|
this.persist();
|
|
337
|
-
this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
|
|
336
|
+
await this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
|
|
338
337
|
|
|
339
338
|
console.log(`\n${C.green}✓ Workflow '${this.workflowName}' completed successfully!${C.reset}`);
|
|
340
339
|
} catch (err) {
|
|
@@ -342,7 +341,7 @@ export class WorkflowRuntime {
|
|
|
342
341
|
this._error = err.message;
|
|
343
342
|
this.persist();
|
|
344
343
|
|
|
345
|
-
this.prependHistory({
|
|
344
|
+
await this.prependHistory({
|
|
346
345
|
event: 'WORKFLOW_FAILED',
|
|
347
346
|
error: err.message
|
|
348
347
|
});
|
|
@@ -419,7 +418,7 @@ export class WorkflowRuntime {
|
|
|
419
418
|
const ask = () => {
|
|
420
419
|
if (resolved) return;
|
|
421
420
|
|
|
422
|
-
rl.question(`${C.dim}> ${C.reset}`, (answer) => {
|
|
421
|
+
rl.question(`${C.dim}> ${C.reset}`, async (answer) => {
|
|
423
422
|
if (resolved) return;
|
|
424
423
|
|
|
425
424
|
const a = answer.trim().toLowerCase();
|
|
@@ -427,7 +426,7 @@ export class WorkflowRuntime {
|
|
|
427
426
|
cleanup();
|
|
428
427
|
// Read and return the response from file
|
|
429
428
|
try {
|
|
430
|
-
const response = this.readInteractionResponse(filePath, slug, targetKey);
|
|
429
|
+
const response = await this.readInteractionResponse(filePath, slug, targetKey);
|
|
431
430
|
resolve(response);
|
|
432
431
|
} catch (err) {
|
|
433
432
|
reject(err);
|
|
@@ -458,7 +457,7 @@ export class WorkflowRuntime {
|
|
|
458
457
|
/**
|
|
459
458
|
* Read the user's response from an interaction file
|
|
460
459
|
*/
|
|
461
|
-
readInteractionResponse(filePath, slug, targetKey) {
|
|
460
|
+
async readInteractionResponse(filePath, slug, targetKey) {
|
|
462
461
|
if (!fs.existsSync(filePath)) {
|
|
463
462
|
throw new Error(`Interaction file not found: ${filePath}`);
|
|
464
463
|
}
|
|
@@ -474,7 +473,7 @@ export class WorkflowRuntime {
|
|
|
474
473
|
this._rawMemory[targetKey] = response;
|
|
475
474
|
this.persist();
|
|
476
475
|
|
|
477
|
-
this.prependHistory({
|
|
476
|
+
await this.prependHistory({
|
|
478
477
|
event: 'INTERACTION_RESOLVED',
|
|
479
478
|
slug,
|
|
480
479
|
targetKey
|
|
@@ -605,6 +604,7 @@ export class WorkflowRuntime {
|
|
|
605
604
|
* @param {string} serverUrl - Base URL of the remote server
|
|
606
605
|
* @param {object} [options]
|
|
607
606
|
* @param {string} [options.sessionToken] - Optional session token to reuse
|
|
607
|
+
* @param {boolean} [options.uiBaseUrl] - If true, return base URL for UI instead of /s/{token}
|
|
608
608
|
* @returns {Promise<string>} The remote URL for browser access
|
|
609
609
|
*/
|
|
610
610
|
async enableRemote(serverUrl, options = {}) {
|
|
@@ -612,6 +612,7 @@ export class WorkflowRuntime {
|
|
|
612
612
|
serverUrl,
|
|
613
613
|
workflowName: this.workflowName,
|
|
614
614
|
sessionToken: options.sessionToken,
|
|
615
|
+
uiBaseUrl: options.uiBaseUrl,
|
|
615
616
|
onInteractionResponse: (slug, targetKey, response) => {
|
|
616
617
|
this.handleRemoteInteraction(slug, targetKey, response);
|
|
617
618
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-state-machine",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.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",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"bin",
|
|
30
30
|
"lib",
|
|
31
|
+
"templates",
|
|
31
32
|
"vercel-server/local-server.js",
|
|
32
33
|
"vercel-server/public",
|
|
33
34
|
"vercel-server/ui",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# project-builder
|
|
2
|
+
|
|
3
|
+
A workflow created with agent-state-machine (native JS format).
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
\`\`\`
|
|
8
|
+
project-builder/
|
|
9
|
+
├── workflow.js # Native JS workflow (async/await)
|
|
10
|
+
├── config.js # Model/API key configuration
|
|
11
|
+
├── package.json # Sets "type": "module" for this workflow folder
|
|
12
|
+
├── agents/ # Custom agents (.js/.mjs/.cjs or .md)
|
|
13
|
+
├── interactions/ # Human-in-the-loop inputs (created at runtime)
|
|
14
|
+
├── state/ # Runtime state (current.json, history.jsonl)
|
|
15
|
+
└── steering/ # Steering configuration
|
|
16
|
+
\`\`\`
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Edit `config.js` to set models and API keys for this workflow.
|
|
21
|
+
|
|
22
|
+
Run the workflow (or resume if interrupted):
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
state-machine run project-builder
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
Check status:
|
|
28
|
+
\`\`\`bash
|
|
29
|
+
state-machine status project-builder
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
View history:
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
state-machine history project-builder
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
View trace logs in browser with live updates:
|
|
38
|
+
\`\`\`bash
|
|
39
|
+
state-machine follow project-builder
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
Reset state (clears memory/state):
|
|
43
|
+
\`\`\`bash
|
|
44
|
+
state-machine reset project-builder
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
Hard reset (clears everything: history/interactions/memory):
|
|
48
|
+
\`\`\`bash
|
|
49
|
+
state-machine reset-hard project-builder
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
## Writing Workflows
|
|
53
|
+
|
|
54
|
+
Edit `workflow.js` - write normal async JavaScript:
|
|
55
|
+
|
|
56
|
+
\`\`\`js
|
|
57
|
+
import { agent, memory, askHuman, parallel } from 'agent-state-machine';
|
|
58
|
+
|
|
59
|
+
export default async function() {
|
|
60
|
+
console.log('Starting project-builder workflow...');
|
|
61
|
+
|
|
62
|
+
// Example: Get user input (saved to memory)
|
|
63
|
+
const userLocation = await askHuman('Where do you live?');
|
|
64
|
+
console.log('Example prompt answer:', userLocation);
|
|
65
|
+
|
|
66
|
+
const userInfo = await agent('yoda-name-collector');
|
|
67
|
+
memory.userInfo = userInfo;
|
|
68
|
+
|
|
69
|
+
// Provide context
|
|
70
|
+
// const userInfo = await agent('yoda-name-collector', { name: 'Luke' });
|
|
71
|
+
|
|
72
|
+
console.log('Example agent memory.userInfo:', memory.userInfo || userInfo);
|
|
73
|
+
|
|
74
|
+
// Context is provided automatically
|
|
75
|
+
const { greeting } = await agent('yoda-greeter', { userLocation });
|
|
76
|
+
console.log('Example agent greeting:', greeting);
|
|
77
|
+
|
|
78
|
+
// Or you can provide context manually
|
|
79
|
+
// await agent('yoda-greeter', userInfo);
|
|
80
|
+
|
|
81
|
+
// Example: Parallel execution
|
|
82
|
+
// const [a, b, c] = await parallel([
|
|
83
|
+
// agent('yoda-greeter', { name: 'the names augustus but friends call me gus' }),
|
|
84
|
+
// agent('yoda-greeter', { name: 'uriah' }),
|
|
85
|
+
// agent('yoda-greeter', { name: 'lucas' })
|
|
86
|
+
// ]);
|
|
87
|
+
|
|
88
|
+
// console.log('a: ' + JSON.stringify(a))
|
|
89
|
+
// console.log('b: ' + JSON.stringify(b))
|
|
90
|
+
// console.log('c: ' + JSON.stringify(c))
|
|
91
|
+
|
|
92
|
+
notify(['project-builder', userInfo.name || userInfo + ' has been greeted!']);
|
|
93
|
+
|
|
94
|
+
console.log('Workflow completed!');
|
|
95
|
+
}
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Creating Agents
|
|
99
|
+
|
|
100
|
+
**JavaScript agent** (`agents/my-agent.js`):
|
|
101
|
+
|
|
102
|
+
\`\`\`js
|
|
103
|
+
import { llm } from 'agent-state-machine';
|
|
104
|
+
|
|
105
|
+
export default async function handler(context) {
|
|
106
|
+
const response = await llm(context, { model: 'smart', prompt: 'Hello!' });
|
|
107
|
+
return { greeting: response.text };
|
|
108
|
+
}
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
**Markdown agent** (`agents/greeter.md`):
|
|
112
|
+
|
|
113
|
+
\`\`\`md
|
|
114
|
+
---
|
|
115
|
+
model: fast
|
|
116
|
+
output: greeting
|
|
117
|
+
---
|
|
118
|
+
Generate a greeting for {{name}}.
|
|
119
|
+
\`\`\`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: med
|
|
3
|
+
format: json
|
|
4
|
+
interaction: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Assumptions Clarifier Agent
|
|
8
|
+
|
|
9
|
+
You are an assumptions and constraints analyst. Your job is to identify and validate assumptions before development.
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
Project Description: {{projectDescription}}
|
|
13
|
+
Scope: {{scope}}
|
|
14
|
+
Requirements: {{requirements}}
|
|
15
|
+
{{#if previousResponse}}
|
|
16
|
+
User's Previous Response: {{previousResponse}}
|
|
17
|
+
{{/if}}
|
|
18
|
+
|
|
19
|
+
## Instructions
|
|
20
|
+
|
|
21
|
+
Identify implicit assumptions that could impact the project. Consider:
|
|
22
|
+
|
|
23
|
+
**Technical Assumptions:**
|
|
24
|
+
- Technology stack preferences
|
|
25
|
+
- Development environment
|
|
26
|
+
- Existing infrastructure
|
|
27
|
+
- Third-party dependencies
|
|
28
|
+
|
|
29
|
+
**Business Assumptions:**
|
|
30
|
+
- Timeline expectations
|
|
31
|
+
- Budget constraints
|
|
32
|
+
- Team composition/skills
|
|
33
|
+
- Stakeholder availability
|
|
34
|
+
|
|
35
|
+
**Domain Assumptions:**
|
|
36
|
+
- Industry regulations
|
|
37
|
+
- Compliance requirements
|
|
38
|
+
- Domain-specific constraints
|
|
39
|
+
|
|
40
|
+
If assumptions need validation, ask using the interact format:
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"interact": "Please confirm or clarify these assumptions:\n\n1. Technology Stack:\n - A: I have a preferred stack (specify below)\n - B: Use best practices for the project type\n - C: Must integrate with existing system\n\n2. Development Timeline:\n - A: Prototype/MVP focus (speed over polish)\n - B: Production-ready from start\n - C: Iterative releases planned\n\n3. Existing Codebase:\n - A: Starting from scratch\n - B: Building on existing code\n - C: Migrating from legacy system\n\nPlease respond with your choices and details:"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
If assumptions are clear, return:
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
"assumptions": {
|
|
50
|
+
"technical": [
|
|
51
|
+
{"assumption": "...", "validated": true, "impact": "high"}
|
|
52
|
+
],
|
|
53
|
+
"business": [
|
|
54
|
+
{"assumption": "...", "validated": true, "impact": "medium"}
|
|
55
|
+
],
|
|
56
|
+
"domain": [
|
|
57
|
+
{"assumption": "...", "validated": true, "impact": "low"}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"risks": [
|
|
61
|
+
{"description": "...", "likelihood": "medium", "mitigation": "..."}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Flag high-risk assumptions that could derail the project if incorrect.
|