agent-state-machine 2.0.15 → 2.1.1
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 +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
- package/templates/project-builder/agents/code-reviewer.md +0 -1
- package/templates/project-builder/agents/code-writer.md +0 -1
- package/templates/project-builder/agents/requirements-clarifier.md +0 -1
- package/templates/project-builder/agents/response-interpreter.md +25 -0
- package/templates/project-builder/agents/roadmap-generator.md +0 -1
- 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 +0 -1
- package/templates/project-builder/agents/security-clarifier.md +0 -1
- package/templates/project-builder/agents/security-reviewer.md +0 -1
- package/templates/project-builder/agents/task-planner.md +0 -1
- package/templates/project-builder/agents/test-planner.md +0 -1
- package/templates/project-builder/scripts/interaction-helpers.js +33 -0
- package/templates/project-builder/scripts/workflow-helpers.js +2 -47
- package/templates/project-builder/workflow.js +214 -54
- 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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: fast
|
|
3
|
+
format: json
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are interpreting a user's natural language response against a structured interaction schema.
|
|
7
|
+
|
|
8
|
+
Return JSON only with:
|
|
9
|
+
- selectedKey (string or null)
|
|
10
|
+
- selectedKeys (array, optional)
|
|
11
|
+
- isCustom (boolean)
|
|
12
|
+
- customText (string, optional)
|
|
13
|
+
- confidence ("low" | "medium" | "high")
|
|
14
|
+
- reasoning (short string)
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- Prefer matching to interaction.options by key or label.
|
|
18
|
+
- If no clear match and allowCustom is true, set isCustom=true and include customText.
|
|
19
|
+
- If ambiguous, set confidence="low" and selectedKey=null.
|
|
20
|
+
|
|
21
|
+
Input:
|
|
22
|
+
{{userResponse}}
|
|
23
|
+
|
|
24
|
+
Schema:
|
|
25
|
+
{{interaction}}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
model: fast
|
|
3
|
+
format: json
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You generate executable sanity checks for the implemented task.
|
|
7
|
+
|
|
8
|
+
Input:
|
|
9
|
+
- task: { title, description, doneDefinition, sanityCheck }
|
|
10
|
+
- implementation: code-writer output
|
|
11
|
+
- testPlan: test-planner output
|
|
12
|
+
|
|
13
|
+
Return JSON only in this shape:
|
|
14
|
+
{
|
|
15
|
+
"checks": [
|
|
16
|
+
{
|
|
17
|
+
"id": 1,
|
|
18
|
+
"description": "What this verifies",
|
|
19
|
+
"type": "shell" | "file_exists" | "file_contains" | "test_suite",
|
|
20
|
+
"command": "shell command if type=shell/test_suite",
|
|
21
|
+
"expected": "expected output (optional)",
|
|
22
|
+
"comparison": "equals" | "contains" | "not_empty",
|
|
23
|
+
"path": "file path for file checks",
|
|
24
|
+
"pattern": "string or regex source for file_contains"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"setup": "optional setup command",
|
|
28
|
+
"teardown": "optional teardown command"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Guidelines:
|
|
32
|
+
- Use actual file paths and commands implied by the implementation.
|
|
33
|
+
- Prefer simple, local commands (curl, node, npm, cat, rg).
|
|
34
|
+
- If the task describes a server endpoint, include a curl check.
|
|
35
|
+
- Keep checks short, clear, and runnable.
|
|
36
|
+
- Include at least one file_exists or file_contains check when files are created/modified.
|
|
37
|
+
|
|
38
|
+
Task:
|
|
39
|
+
{{task}}
|
|
40
|
+
|
|
41
|
+
Implementation:
|
|
42
|
+
{{implementation}}
|
|
43
|
+
|
|
44
|
+
Test Plan:
|
|
45
|
+
{{testPlan}}
|