agent-state-machine 1.0.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/README.md +299 -0
- package/bin/cli.js +246 -0
- package/lib/index.js +110 -0
- package/lib/index.mjs +9 -0
- package/lib/llm.js +472 -0
- package/lib/runtime/agent.js +359 -0
- package/lib/runtime/index.js +35 -0
- package/lib/runtime/memory.js +88 -0
- package/lib/runtime/parallel.js +66 -0
- package/lib/runtime/prompt.js +118 -0
- package/lib/runtime/runtime.js +387 -0
- package/lib/setup.js +398 -0
- package/lib/state-machine.js +1359 -0
- package/package.json +32 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/agent.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Agent execution module for native JS workflows
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import { pathToFileURL } from 'url';
|
|
13
|
+
import { getCurrentRuntime } from './runtime.js';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run an agent with context
|
|
19
|
+
* @param {string} name - Agent name (file basename)
|
|
20
|
+
* @param {object} params - Parameters passed to agent
|
|
21
|
+
*/
|
|
22
|
+
export async function agent(name, params = {}) {
|
|
23
|
+
const runtime = getCurrentRuntime();
|
|
24
|
+
if (!runtime) {
|
|
25
|
+
throw new Error('agent() must be called within a workflow context');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(` [Agent: ${name}] Starting...`);
|
|
29
|
+
runtime.prependHistory({
|
|
30
|
+
event: 'AGENT_STARTED',
|
|
31
|
+
agent: name
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await executeAgent(runtime, name, params);
|
|
36
|
+
|
|
37
|
+
console.log(` [Agent: ${name}] Completed`);
|
|
38
|
+
runtime.prependHistory({
|
|
39
|
+
event: 'AGENT_COMPLETED',
|
|
40
|
+
agent: name,
|
|
41
|
+
output: result
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
runtime.prependHistory({
|
|
47
|
+
event: 'AGENT_FAILED',
|
|
48
|
+
agent: name,
|
|
49
|
+
error: error.message
|
|
50
|
+
});
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute an agent (load and run)
|
|
57
|
+
*/
|
|
58
|
+
export async function executeAgent(runtime, name, params) {
|
|
59
|
+
const agentsDir = runtime.agentsDir;
|
|
60
|
+
|
|
61
|
+
// Try JS agents (.js/.mjs/.cjs)
|
|
62
|
+
const jsCandidates = [
|
|
63
|
+
path.join(agentsDir, `${name}.js`),
|
|
64
|
+
path.join(agentsDir, `${name}.mjs`),
|
|
65
|
+
path.join(agentsDir, `${name}.cjs`)
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const p of jsCandidates) {
|
|
69
|
+
if (fs.existsSync(p)) {
|
|
70
|
+
return executeJSAgent(runtime, p, name, params);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try Markdown agent
|
|
75
|
+
const mdPath = path.join(agentsDir, `${name}.md`);
|
|
76
|
+
if (fs.existsSync(mdPath)) {
|
|
77
|
+
return executeMDAgent(runtime, mdPath, name, params);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Agent not found: ${name} (looked for ${jsCandidates
|
|
82
|
+
.map((p) => path.basename(p))
|
|
83
|
+
.join(', ')} or ${name}.md in ${agentsDir})`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute a JavaScript agent
|
|
89
|
+
* - ESM (.js/.mjs): loaded via dynamic import with cache-bust for hot reload
|
|
90
|
+
* - CJS (.cjs): loaded via require() with cache clear
|
|
91
|
+
*/
|
|
92
|
+
async function executeJSAgent(runtime, agentPath, name, params) {
|
|
93
|
+
const ext = path.extname(agentPath).toLowerCase();
|
|
94
|
+
|
|
95
|
+
let agentModule;
|
|
96
|
+
if (ext === '.cjs') {
|
|
97
|
+
// Clear require cache for hot reloading
|
|
98
|
+
delete require.cache[require.resolve(agentPath)];
|
|
99
|
+
agentModule = require(agentPath);
|
|
100
|
+
} else {
|
|
101
|
+
// Cache-busted import for hot reload behavior
|
|
102
|
+
const url = pathToFileURL(agentPath).href;
|
|
103
|
+
agentModule = await import(`${url}?t=${Date.now()}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handler =
|
|
107
|
+
agentModule?.handler ||
|
|
108
|
+
agentModule?.default ||
|
|
109
|
+
agentModule;
|
|
110
|
+
|
|
111
|
+
if (typeof handler !== 'function') {
|
|
112
|
+
throw new Error(`Agent ${name} does not export a function`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build context
|
|
116
|
+
const context = {
|
|
117
|
+
...runtime._rawMemory,
|
|
118
|
+
...params,
|
|
119
|
+
_steering: runtime.steering,
|
|
120
|
+
_config: {
|
|
121
|
+
models: runtime.workflowConfig.models,
|
|
122
|
+
apiKeys: runtime.workflowConfig.apiKeys,
|
|
123
|
+
workflowDir: runtime.workflowDir
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = await handler(context);
|
|
128
|
+
|
|
129
|
+
// Handle interaction response from JS agent
|
|
130
|
+
if (result && result._interaction) {
|
|
131
|
+
const interactionResponse = await handleInteraction(runtime, result._interaction);
|
|
132
|
+
|
|
133
|
+
// Use the interaction response as the primary output if it exists
|
|
134
|
+
// This allows the workflow to receive the user's input directly
|
|
135
|
+
if (typeof result === 'object') {
|
|
136
|
+
// Merge response into result or replace?
|
|
137
|
+
// For consistency with MDAgent, we might want to return { result: response }
|
|
138
|
+
// but JS agents are more flexible.
|
|
139
|
+
// Let's assume the agent wants the response mixed in or as the return.
|
|
140
|
+
// A safe bet is to return the response if the agent explicitly asked for interaction.
|
|
141
|
+
return { ...result, result: interactionResponse };
|
|
142
|
+
}
|
|
143
|
+
return interactionResponse;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Clean internal properties from result
|
|
147
|
+
if (result && typeof result === 'object') {
|
|
148
|
+
const cleanResult = { ...result };
|
|
149
|
+
delete cleanResult._steering;
|
|
150
|
+
delete cleanResult._config;
|
|
151
|
+
delete cleanResult._loop;
|
|
152
|
+
delete cleanResult._interaction;
|
|
153
|
+
|
|
154
|
+
// If agent returned a context-like object, only return non-internal keys
|
|
155
|
+
const meaningfulKeys = Object.keys(cleanResult).filter((k) => !k.startsWith('_'));
|
|
156
|
+
if (meaningfulKeys.length > 0) {
|
|
157
|
+
const output = {};
|
|
158
|
+
for (const key of meaningfulKeys) output[key] = cleanResult[key];
|
|
159
|
+
return output;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cleanResult;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Execute a Markdown agent (prompt-based)
|
|
170
|
+
*/
|
|
171
|
+
async function executeMDAgent(runtime, agentPath, name, params) {
|
|
172
|
+
const { llm, parseJSON, parseInteractionRequest } = await import('../llm.js');
|
|
173
|
+
|
|
174
|
+
const content = fs.readFileSync(agentPath, 'utf-8');
|
|
175
|
+
const { config, prompt } = parseMarkdownAgent(content);
|
|
176
|
+
|
|
177
|
+
const outputKey = config.output || 'result';
|
|
178
|
+
const targetKey = config.interactionKey || outputKey;
|
|
179
|
+
|
|
180
|
+
// Build context
|
|
181
|
+
const context = {
|
|
182
|
+
...runtime._rawMemory,
|
|
183
|
+
...params,
|
|
184
|
+
_steering: runtime.steering,
|
|
185
|
+
_config: {
|
|
186
|
+
models: runtime.workflowConfig.models,
|
|
187
|
+
apiKeys: runtime.workflowConfig.apiKeys,
|
|
188
|
+
workflowDir: runtime.workflowDir
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Interpolate variables in prompt
|
|
193
|
+
const interpolatedPrompt = interpolatePrompt(prompt, context);
|
|
194
|
+
|
|
195
|
+
const model = config.model || 'fast';
|
|
196
|
+
|
|
197
|
+
console.log(` Using model: ${model}`);
|
|
198
|
+
|
|
199
|
+
const response = await llm(context, {
|
|
200
|
+
model: model,
|
|
201
|
+
prompt: interpolatedPrompt,
|
|
202
|
+
includeContext: config.includeContext !== 'false'
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Parse output based on format
|
|
206
|
+
let output = response.text;
|
|
207
|
+
if (config.format === 'json') {
|
|
208
|
+
try {
|
|
209
|
+
output = parseJSON(response.text);
|
|
210
|
+
} catch {
|
|
211
|
+
console.warn(` Warning: Failed to parse JSON output`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check for interaction request
|
|
216
|
+
const explicitInteraction =
|
|
217
|
+
config.format === 'interaction' ||
|
|
218
|
+
config.interaction === 'true' ||
|
|
219
|
+
(typeof config.interaction === 'string' && config.interaction.length > 0);
|
|
220
|
+
|
|
221
|
+
const parsedInteraction = parseInteractionRequest(response.text);
|
|
222
|
+
const structuredInteraction =
|
|
223
|
+
config.autoInteract !== 'false' && parsedInteraction.isInteraction;
|
|
224
|
+
|
|
225
|
+
if (explicitInteraction || structuredInteraction) {
|
|
226
|
+
const slugRaw =
|
|
227
|
+
(typeof config.interaction === 'string' && config.interaction !== 'true'
|
|
228
|
+
? config.interaction
|
|
229
|
+
: null) ||
|
|
230
|
+
config.interactionSlug ||
|
|
231
|
+
config.interactionKey ||
|
|
232
|
+
outputKey ||
|
|
233
|
+
name;
|
|
234
|
+
|
|
235
|
+
const slug = sanitizeSlug(slugRaw);
|
|
236
|
+
const targetKey = config.interactionKey || outputKey || slug;
|
|
237
|
+
const interactionContent = structuredInteraction ? parsedInteraction.question : response.text;
|
|
238
|
+
|
|
239
|
+
const userResponse = await handleInteraction(runtime, {
|
|
240
|
+
slug,
|
|
241
|
+
targetKey,
|
|
242
|
+
content: interactionContent
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Return the user's response as the agent result
|
|
246
|
+
return { [outputKey]: userResponse };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Return result object
|
|
250
|
+
return { [outputKey]: output };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse markdown agent with simple frontmatter
|
|
255
|
+
*/
|
|
256
|
+
function parseMarkdownAgent(content) {
|
|
257
|
+
// Frontmatter format:
|
|
258
|
+
// ---
|
|
259
|
+
// key: value
|
|
260
|
+
// ---
|
|
261
|
+
// prompt...
|
|
262
|
+
if (content.startsWith('---')) {
|
|
263
|
+
const parts = content.split('---');
|
|
264
|
+
if (parts.length >= 3) {
|
|
265
|
+
const frontmatter = parts[1].trim();
|
|
266
|
+
const prompt = parts.slice(2).join('---').trim();
|
|
267
|
+
|
|
268
|
+
const config = {};
|
|
269
|
+
frontmatter.split('\n').forEach((line) => {
|
|
270
|
+
const [key, ...rest] = line.split(':');
|
|
271
|
+
if (key && rest.length) {
|
|
272
|
+
const value = rest.join(':').trim();
|
|
273
|
+
config[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return { config, prompt };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { config: {}, prompt: content.trim() };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Interpolate {{variables}} in prompt template
|
|
286
|
+
*/
|
|
287
|
+
function interpolatePrompt(template, context) {
|
|
288
|
+
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, pathStr) => {
|
|
289
|
+
const value = getByPath(context, pathStr);
|
|
290
|
+
return value !== undefined ? String(value) : match;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get nested value by dot-notation path
|
|
296
|
+
*/
|
|
297
|
+
function getByPath(obj, pathStr) {
|
|
298
|
+
const trimmed = String(pathStr || '').trim();
|
|
299
|
+
if (!trimmed) return undefined;
|
|
300
|
+
|
|
301
|
+
const normalized = trimmed.startsWith('context.')
|
|
302
|
+
? trimmed.slice('context.'.length)
|
|
303
|
+
: trimmed;
|
|
304
|
+
|
|
305
|
+
const parts = normalized.split('.').filter(Boolean);
|
|
306
|
+
let current = obj;
|
|
307
|
+
|
|
308
|
+
for (const part of parts) {
|
|
309
|
+
if (current == null) return undefined;
|
|
310
|
+
current = current[String(part)];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return current;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sanitizeSlug(input) {
|
|
317
|
+
const raw = String(input || '').trim();
|
|
318
|
+
const cleaned = raw
|
|
319
|
+
.toLowerCase()
|
|
320
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
321
|
+
.replace(/-+/g, '-')
|
|
322
|
+
.replace(/^-|-$/g, '');
|
|
323
|
+
|
|
324
|
+
return cleaned || 'interaction';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle interaction (create file, wait for user, return response)
|
|
329
|
+
*/
|
|
330
|
+
async function handleInteraction(runtime, interaction) {
|
|
331
|
+
const slug = sanitizeSlug(interaction.slug);
|
|
332
|
+
const targetKey = String(interaction.targetKey || slug);
|
|
333
|
+
const content = String(interaction.content || '').trim();
|
|
334
|
+
|
|
335
|
+
const filePath = path.join(runtime.interactionsDir, `${slug}.md`);
|
|
336
|
+
|
|
337
|
+
// Create interaction file
|
|
338
|
+
const fileContent = `# ${slug}
|
|
339
|
+
|
|
340
|
+
${content}
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
Enter your response below:
|
|
344
|
+
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
fs.writeFileSync(filePath, fileContent);
|
|
348
|
+
|
|
349
|
+
runtime.prependHistory({
|
|
350
|
+
event: 'INTERACTION_REQUESTED',
|
|
351
|
+
slug,
|
|
352
|
+
targetKey
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Block and wait for user input (instead of throwing)
|
|
356
|
+
const response = await runtime.waitForInteraction(filePath, slug, targetKey);
|
|
357
|
+
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/index.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime module exports for native JS workflows
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
WorkflowRuntime,
|
|
11
|
+
getCurrentRuntime,
|
|
12
|
+
setCurrentRuntime,
|
|
13
|
+
clearCurrentRuntime
|
|
14
|
+
} from './runtime.js';
|
|
15
|
+
|
|
16
|
+
export { agent, executeAgent } from './agent.js';
|
|
17
|
+
export { initialPrompt } from './prompt.js';
|
|
18
|
+
export { parallel, parallelLimit } from './parallel.js';
|
|
19
|
+
export { createMemoryProxy } from './memory.js';
|
|
20
|
+
|
|
21
|
+
import { getCurrentRuntime } from './runtime.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the current workflow's memory object
|
|
25
|
+
* Returns a proxy that auto-persists on mutation
|
|
26
|
+
*/
|
|
27
|
+
export function getMemory() {
|
|
28
|
+
const runtime = getCurrentRuntime();
|
|
29
|
+
if (!runtime) {
|
|
30
|
+
// Return empty object when not in workflow context
|
|
31
|
+
// This allows imports to work without throwing
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
return runtime.memory;
|
|
35
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/memory.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Memory Proxy Module
|
|
7
|
+
* Creates a proxy object that auto-persists to disk on mutation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let persistTimeout = null;
|
|
11
|
+
const DEBOUNCE_MS = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Schedule a debounced persistence call
|
|
15
|
+
*/
|
|
16
|
+
function schedulePersist(fn) {
|
|
17
|
+
if (persistTimeout) clearTimeout(persistTimeout);
|
|
18
|
+
persistTimeout = setTimeout(() => {
|
|
19
|
+
fn();
|
|
20
|
+
persistTimeout = null;
|
|
21
|
+
}, DEBOUNCE_MS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Force immediate persistence (clears any pending debounce)
|
|
26
|
+
*/
|
|
27
|
+
export function flushPersist(fn) {
|
|
28
|
+
if (persistTimeout) {
|
|
29
|
+
clearTimeout(persistTimeout);
|
|
30
|
+
persistTimeout = null;
|
|
31
|
+
}
|
|
32
|
+
fn();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a proxy that auto-saves on mutation
|
|
37
|
+
* @param {object} data - The raw data object to proxy
|
|
38
|
+
* @param {function} persistFn - Function to call when data changes
|
|
39
|
+
* @param {object} root - Root object for nested proxy tracking (internal)
|
|
40
|
+
* @returns {Proxy} Proxied memory object
|
|
41
|
+
*/
|
|
42
|
+
export function createMemoryProxy(data, persistFn, root = null) {
|
|
43
|
+
const handler = {
|
|
44
|
+
get(target, prop) {
|
|
45
|
+
// Return primitives and functions as-is
|
|
46
|
+
if (prop === '_raw') return target;
|
|
47
|
+
if (prop === '_flush') return () => flushPersist(persistFn);
|
|
48
|
+
|
|
49
|
+
const value = target[prop];
|
|
50
|
+
|
|
51
|
+
// Don't proxy internal properties, functions, or primitives
|
|
52
|
+
if (typeof prop === 'symbol') return value;
|
|
53
|
+
if (prop.startsWith('_')) return value;
|
|
54
|
+
if (typeof value === 'function') return value;
|
|
55
|
+
if (value === null || typeof value !== 'object') return value;
|
|
56
|
+
|
|
57
|
+
// Wrap nested objects in proxies for deep observation
|
|
58
|
+
return createMemoryProxy(value, persistFn, root || data);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
set(target, prop, value) {
|
|
62
|
+
// Skip internal properties (don't trigger persist)
|
|
63
|
+
if (typeof prop === 'symbol' || prop.startsWith('_')) {
|
|
64
|
+
target[prop] = value;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
target[prop] = value;
|
|
69
|
+
schedulePersist(persistFn);
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
deleteProperty(target, prop) {
|
|
74
|
+
if (typeof prop === 'symbol' || prop.startsWith('_')) {
|
|
75
|
+
delete target[prop];
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
delete target[prop];
|
|
80
|
+
schedulePersist(persistFn);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return new Proxy(data, handler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { schedulePersist };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/parallel.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parallel execution module for running multiple agents concurrently
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute multiple agent calls in parallel
|
|
11
|
+
* @param {Promise[]} agentCalls - Array of agent() call promises
|
|
12
|
+
* @returns {Promise<any[]>} Array of results in same order as input
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const [review1, review2] = await parallel([
|
|
16
|
+
* agent('code-review', { file: 'src/a.js' }),
|
|
17
|
+
* agent('code-review', { file: 'src/b.js' })
|
|
18
|
+
* ]);
|
|
19
|
+
*/
|
|
20
|
+
export async function parallel(agentCalls) {
|
|
21
|
+
if (!Array.isArray(agentCalls)) {
|
|
22
|
+
throw new Error('parallel() expects an array of promises');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (agentCalls.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Promise.all maintains order
|
|
30
|
+
return Promise.all(agentCalls);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute agent calls in parallel with a concurrency limit
|
|
35
|
+
* @param {Promise[]} agentCalls - Array of agent() call promises
|
|
36
|
+
* @param {number} limit - Maximum concurrent executions
|
|
37
|
+
* @returns {Promise<any[]>} Array of results in same order as input
|
|
38
|
+
*/
|
|
39
|
+
export async function parallelLimit(agentCalls, limit = 3) {
|
|
40
|
+
if (!Array.isArray(agentCalls)) {
|
|
41
|
+
throw new Error('parallelLimit() expects an array of promises');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (agentCalls.length === 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const results = new Array(agentCalls.length);
|
|
49
|
+
let currentIndex = 0;
|
|
50
|
+
|
|
51
|
+
async function worker() {
|
|
52
|
+
while (currentIndex < agentCalls.length) {
|
|
53
|
+
const index = currentIndex++;
|
|
54
|
+
results[index] = await agentCalls[index];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Start up to 'limit' workers
|
|
59
|
+
const workers = [];
|
|
60
|
+
for (let i = 0; i < Math.min(limit, agentCalls.length); i++) {
|
|
61
|
+
workers.push(worker());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await Promise.all(workers);
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/runtime/prompt.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initial prompt module for user input collection
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
import { getCurrentRuntime } from './runtime.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request user input with memory-based resume support
|
|
16
|
+
* @param {string} question - Question to ask the user
|
|
17
|
+
* @param {object} options - Options
|
|
18
|
+
* @param {string} options.slug - Unique identifier for this prompt (for file)
|
|
19
|
+
* @returns {Promise<string>} User's response
|
|
20
|
+
*/
|
|
21
|
+
export async function initialPrompt(question, options = {}) {
|
|
22
|
+
const runtime = getCurrentRuntime();
|
|
23
|
+
if (!runtime) {
|
|
24
|
+
throw new Error('initialPrompt() must be called within a workflow context');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const slug = options.slug || generateSlug(question);
|
|
28
|
+
const memoryKey = `_interaction_${slug}`;
|
|
29
|
+
|
|
30
|
+
runtime.prependHistory({
|
|
31
|
+
event: 'PROMPT_REQUESTED',
|
|
32
|
+
slug,
|
|
33
|
+
question
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Check if we're in TTY mode (interactive terminal)
|
|
37
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
38
|
+
// Interactive mode - prompt directly
|
|
39
|
+
console.log('');
|
|
40
|
+
const answer = await askQuestion(question);
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
// Save the response to memory
|
|
44
|
+
runtime._rawMemory[memoryKey] = answer;
|
|
45
|
+
runtime.persist();
|
|
46
|
+
|
|
47
|
+
runtime.prependHistory({
|
|
48
|
+
event: 'PROMPT_ANSWERED',
|
|
49
|
+
slug,
|
|
50
|
+
answer: answer.substring(0, 100) + (answer.length > 100 ? '...' : '')
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return answer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Non-TTY mode - create interaction file and wait inline
|
|
57
|
+
const interactionFile = path.join(runtime.interactionsDir, `${slug}.md`);
|
|
58
|
+
|
|
59
|
+
const fileContent = `# ${slug}
|
|
60
|
+
|
|
61
|
+
${question}
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
Enter your response below:
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
fs.writeFileSync(interactionFile, fileContent);
|
|
69
|
+
|
|
70
|
+
runtime.prependHistory({
|
|
71
|
+
event: 'INTERACTION_REQUESTED',
|
|
72
|
+
slug,
|
|
73
|
+
targetKey: memoryKey,
|
|
74
|
+
file: interactionFile
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Block and wait for user input (instead of throwing)
|
|
78
|
+
const answer = await runtime.waitForInteraction(interactionFile, slug, memoryKey);
|
|
79
|
+
|
|
80
|
+
runtime._rawMemory[memoryKey] = answer;
|
|
81
|
+
runtime.persist();
|
|
82
|
+
|
|
83
|
+
runtime.prependHistory({
|
|
84
|
+
event: 'PROMPT_ANSWERED',
|
|
85
|
+
slug,
|
|
86
|
+
answer: answer.substring(0, 100) + (answer.length > 100 ? '...' : '')
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return answer;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Interactive terminal question
|
|
94
|
+
*/
|
|
95
|
+
function askQuestion(question) {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const rl = readline.createInterface({
|
|
98
|
+
input: process.stdin,
|
|
99
|
+
output: process.stdout
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
rl.question(`${question}\n> `, (answer) => {
|
|
103
|
+
rl.close();
|
|
104
|
+
resolve(answer.trim());
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate a slug from question text
|
|
111
|
+
*/
|
|
112
|
+
function generateSlug(question) {
|
|
113
|
+
return question
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
116
|
+
.replace(/^-+|-+$/g, '')
|
|
117
|
+
.substring(0, 30);
|
|
118
|
+
}
|