agent-state-machine 2.2.0 → 2.2.2
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 +78 -2
- package/lib/remote/client.js +37 -8
- package/lib/runtime/agent.js +6 -2
- package/lib/runtime/interaction.js +2 -1
- package/lib/runtime/prompt.js +37 -1
- package/lib/runtime/runtime.js +67 -5
- package/package.json +1 -1
- package/templates/project-builder/README.md +304 -56
- package/templates/project-builder/agents/code-fixer.md +50 -0
- package/templates/project-builder/agents/code-writer.md +3 -0
- package/templates/project-builder/agents/sanity-checker.md +6 -0
- package/templates/project-builder/agents/sanity-runner.js +3 -1
- package/templates/project-builder/agents/test-planner.md +3 -1
- package/templates/project-builder/config.js +4 -4
- package/templates/project-builder/scripts/workflow-helpers.js +104 -2
- package/templates/project-builder/workflow.js +151 -14
- package/templates/starter/README.md +291 -42
- package/templates/starter/config.js +1 -1
- package/vercel-server/api/submit/[token].js +2 -13
- package/vercel-server/api/ws/cli.js +40 -2
- package/vercel-server/local-server.js +32 -22
- package/vercel-server/public/remote/assets/index-BsJsLDKc.css +1 -0
- package/vercel-server/public/remote/assets/index-CmtT6ADh.js +168 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/App.jsx +69 -62
- package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
- package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
- package/vercel-server/ui/src/components/ContentCard.jsx +600 -104
- package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
- package/vercel-server/ui/src/components/Footer.jsx +9 -4
- package/vercel-server/ui/src/components/Header.jsx +12 -3
- package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
- package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
- package/vercel-server/ui/src/index.css +82 -10
- package/vercel-server/public/remote/assets/index-BOKpYANC.js +0 -148
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import readline from 'readline';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
6
7
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
7
8
|
import { WorkflowRuntime } from '../lib/index.js';
|
|
8
9
|
import { setup } from '../lib/setup.js';
|
|
@@ -11,6 +12,41 @@ import { readRemotePathFromConfig, writeRemotePathToConfig } from '../lib/config
|
|
|
11
12
|
|
|
12
13
|
import { startLocalServer } from '../vercel-server/local-server.js';
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Prevent system sleep on macOS using caffeinate
|
|
17
|
+
* Returns a function to stop caffeinate, or null if not available
|
|
18
|
+
*/
|
|
19
|
+
function preventSleep() {
|
|
20
|
+
// Only works on macOS
|
|
21
|
+
if (process.platform !== 'darwin') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// -i: prevent idle sleep (system stays awake)
|
|
27
|
+
// -s: prevent sleep when on AC power
|
|
28
|
+
// Display can still sleep (screen goes black, requires password)
|
|
29
|
+
const caffeinate = spawn('caffeinate', ['-is'], {
|
|
30
|
+
stdio: 'ignore',
|
|
31
|
+
detached: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
caffeinate.on('error', () => {
|
|
35
|
+
// caffeinate not available, ignore
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
try {
|
|
40
|
+
caffeinate.kill();
|
|
41
|
+
} catch {
|
|
42
|
+
// Already dead, ignore
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
14
50
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
51
|
const __dirname = path.dirname(__filename);
|
|
16
52
|
|
|
@@ -61,6 +97,8 @@ Options:
|
|
|
61
97
|
--template, -t Template name for --setup (default: starter)
|
|
62
98
|
--local, -l Use local server instead of remote (starts on localhost:3000)
|
|
63
99
|
--new, -n Generate a new remote follow path
|
|
100
|
+
--full-auto, -a Auto-select first option for choice interactions (no blocking)
|
|
101
|
+
--delay, -d Seconds to wait before auto-select in full-auto mode (default: 20)
|
|
64
102
|
-reset Reset workflow state before running
|
|
65
103
|
-reset-hard Hard reset workflow before running
|
|
66
104
|
--help, -h Show help
|
|
@@ -202,7 +240,9 @@ async function runOrResume(
|
|
|
202
240
|
useLocalServer = false,
|
|
203
241
|
forceNewRemotePath = false,
|
|
204
242
|
preReset = false,
|
|
205
|
-
preResetHard = false
|
|
243
|
+
preResetHard = false,
|
|
244
|
+
fullAuto = false,
|
|
245
|
+
autoSelectDelay = null
|
|
206
246
|
} = {}
|
|
207
247
|
) {
|
|
208
248
|
const workflowDir = resolveWorkflowDir(workflowName);
|
|
@@ -258,9 +298,31 @@ async function runOrResume(
|
|
|
258
298
|
await runtime.enableRemote(remoteUrl, { sessionToken, uiBaseUrl: useLocalServer });
|
|
259
299
|
}
|
|
260
300
|
|
|
301
|
+
// Set full-auto mode from CLI flag (will be merged with config.js during runWorkflow)
|
|
302
|
+
if (fullAuto) {
|
|
303
|
+
runtime.workflowConfig.fullAuto = true;
|
|
304
|
+
if (autoSelectDelay !== null) {
|
|
305
|
+
runtime.workflowConfig.autoSelectDelay = autoSelectDelay;
|
|
306
|
+
}
|
|
307
|
+
const delay = runtime.workflowConfig.autoSelectDelay;
|
|
308
|
+
console.log(`\n\x1b[36m\x1b[1m⚡ Full-auto mode enabled\x1b[0m - Agent will auto-select recommended options after ${delay}s countdown`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Prevent system sleep while workflow runs (macOS only)
|
|
312
|
+
// Display can still sleep, but system stays awake for remote follow
|
|
313
|
+
const stopCaffeinate = preventSleep();
|
|
314
|
+
if (stopCaffeinate) {
|
|
315
|
+
console.log('☕ Preventing system sleep while workflow runs (display may still sleep)');
|
|
316
|
+
}
|
|
317
|
+
|
|
261
318
|
try {
|
|
262
319
|
await runtime.runWorkflow(workflowUrl);
|
|
263
320
|
} finally {
|
|
321
|
+
// Allow sleep again
|
|
322
|
+
if (stopCaffeinate) {
|
|
323
|
+
stopCaffeinate();
|
|
324
|
+
}
|
|
325
|
+
|
|
264
326
|
// Keep local server alive after run so the session remains accessible.
|
|
265
327
|
if (!useLocalServer && remoteUrl) {
|
|
266
328
|
await runtime.disableRemote();
|
|
@@ -322,14 +384,28 @@ async function main() {
|
|
|
322
384
|
const forceNewRemotePath = args.includes('--new') || args.includes('-n');
|
|
323
385
|
const preReset = args.includes('-reset');
|
|
324
386
|
const preResetHard = args.includes('-reset-hard');
|
|
387
|
+
const fullAuto = args.includes('--full-auto') || args.includes('-a');
|
|
325
388
|
const remoteEnabled = !useLocalServer; // Use Vercel if not local
|
|
389
|
+
|
|
390
|
+
// Parse --delay or -d flag
|
|
391
|
+
let autoSelectDelay = null;
|
|
392
|
+
const delayFlagIndex = args.findIndex((arg) => arg === '--delay' || arg === '-d');
|
|
393
|
+
if (delayFlagIndex !== -1 && args[delayFlagIndex + 1]) {
|
|
394
|
+
const delayValue = parseInt(args[delayFlagIndex + 1], 10);
|
|
395
|
+
if (!isNaN(delayValue) && delayValue >= 0) {
|
|
396
|
+
autoSelectDelay = delayValue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
326
400
|
try {
|
|
327
401
|
await runOrResume(workflowName, {
|
|
328
402
|
remoteEnabled,
|
|
329
403
|
useLocalServer,
|
|
330
404
|
forceNewRemotePath,
|
|
331
405
|
preReset,
|
|
332
|
-
preResetHard
|
|
406
|
+
preResetHard,
|
|
407
|
+
fullAuto,
|
|
408
|
+
autoSelectDelay
|
|
333
409
|
});
|
|
334
410
|
} catch (err) {
|
|
335
411
|
console.error('Error:', err.message || String(err));
|
package/lib/remote/client.js
CHANGED
|
@@ -29,9 +29,13 @@ export function generateSessionToken() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Make an HTTP/HTTPS request
|
|
32
|
+
* Make an HTTP/HTTPS request with timeout
|
|
33
|
+
* @param {string} url - Request URL
|
|
34
|
+
* @param {object} options - Request options
|
|
35
|
+
* @param {object|null} body - Request body
|
|
36
|
+
* @param {number} timeoutMs - Request timeout in milliseconds
|
|
33
37
|
*/
|
|
34
|
-
function makeRequest(url, options, body = null) {
|
|
38
|
+
function makeRequest(url, options, body = null, timeoutMs = 60000) {
|
|
35
39
|
return new Promise((resolve, reject) => {
|
|
36
40
|
const parsedUrl = new URL(url);
|
|
37
41
|
const client = parsedUrl.protocol === 'https:' ? https : http;
|
|
@@ -60,6 +64,12 @@ function makeRequest(url, options, body = null) {
|
|
|
60
64
|
});
|
|
61
65
|
});
|
|
62
66
|
|
|
67
|
+
// Timeout prevents hanging on sleep/wake cycles
|
|
68
|
+
req.setTimeout(timeoutMs, () => {
|
|
69
|
+
req.destroy();
|
|
70
|
+
reject(new Error('Request timeout'));
|
|
71
|
+
});
|
|
72
|
+
|
|
63
73
|
req.on('error', reject);
|
|
64
74
|
|
|
65
75
|
if (body) {
|
|
@@ -222,28 +232,47 @@ export class RemoteClient {
|
|
|
222
232
|
|
|
223
233
|
/**
|
|
224
234
|
* Poll for interaction responses
|
|
235
|
+
* Uses 35s timeout to stay under Vercel's 50s limit with buffer
|
|
225
236
|
*/
|
|
226
237
|
async poll() {
|
|
238
|
+
let consecutiveErrors = 0;
|
|
239
|
+
|
|
227
240
|
while (this.polling && this.connected) {
|
|
228
241
|
try {
|
|
242
|
+
// Request 30s poll from server, with 35s client timeout
|
|
229
243
|
const url = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}&timeout=30000`;
|
|
230
|
-
const response = await makeRequest(url, { method: 'GET' });
|
|
244
|
+
const response = await makeRequest(url, { method: 'GET' }, null, 35000);
|
|
245
|
+
|
|
246
|
+
consecutiveErrors = 0; // Reset on success
|
|
231
247
|
|
|
232
248
|
if (response.status === 200 && response.data) {
|
|
233
249
|
const { type, slug, targetKey, response: interactionResponse } = response.data;
|
|
234
250
|
|
|
235
251
|
if (type === 'interaction_response' && this.onInteractionResponse) {
|
|
252
|
+
// Confirm receipt BEFORE processing - removes from Redis pending queue
|
|
253
|
+
// This ensures we don't lose the interaction if processing fails
|
|
254
|
+
try {
|
|
255
|
+
const confirmUrl = `${this.serverUrl}/api/ws/cli?token=${this.sessionToken}`;
|
|
256
|
+
await makeRequest(confirmUrl, { method: 'DELETE' }, null, 10000);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
// Non-fatal - interaction will be re-delivered on next poll
|
|
259
|
+
console.error(`${C.dim}Remote: Failed to confirm receipt: ${err.message}${C.reset}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
236
262
|
this.onInteractionResponse(slug, targetKey, interactionResponse);
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
// If 204 (no content), just continue polling
|
|
241
|
-
// Small delay
|
|
242
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
266
|
+
// If 204 (no content), just continue polling immediately
|
|
267
|
+
// Small delay only on success to prevent tight loop
|
|
268
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
243
269
|
|
|
244
270
|
} catch (err) {
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
consecutiveErrors++;
|
|
272
|
+
|
|
273
|
+
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
274
|
+
const backoff = Math.min(1000 * Math.pow(2, consecutiveErrors - 1), 10000);
|
|
275
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
247
276
|
}
|
|
248
277
|
}
|
|
249
278
|
}
|
package/lib/runtime/agent.js
CHANGED
|
@@ -533,7 +533,10 @@ ${content}
|
|
|
533
533
|
validation: interaction.validation,
|
|
534
534
|
confirmLabel: interaction.confirmLabel,
|
|
535
535
|
cancelLabel: interaction.cancelLabel,
|
|
536
|
-
context: interaction.context
|
|
536
|
+
context: interaction.context,
|
|
537
|
+
// Include full-auto info for remote UI countdown
|
|
538
|
+
fullAuto: runtime.workflowConfig.fullAuto || false,
|
|
539
|
+
autoSelectDelay: runtime.workflowConfig.autoSelectDelay ?? 20
|
|
537
540
|
});
|
|
538
541
|
|
|
539
542
|
if (effectiveAgentName) {
|
|
@@ -541,7 +544,8 @@ ${content}
|
|
|
541
544
|
}
|
|
542
545
|
|
|
543
546
|
// Block and wait for user input (instead of throwing)
|
|
544
|
-
|
|
547
|
+
// Pass the full interaction object for full-auto mode support
|
|
548
|
+
const response = await runtime.waitForInteraction(filePath, slug, targetKey, interaction);
|
|
545
549
|
|
|
546
550
|
return response;
|
|
547
551
|
}
|
|
@@ -117,8 +117,9 @@ export function formatInteractionPrompt(interaction) {
|
|
|
117
117
|
interaction.options.forEach((opt, index) => {
|
|
118
118
|
const letter = String.fromCharCode(65 + index);
|
|
119
119
|
const label = opt.label || opt.key || `Option ${index + 1}`;
|
|
120
|
+
const recommended = index === 0 ? ' (Recommended)' : '';
|
|
120
121
|
const desc = opt.description ? ` - ${opt.description}` : '';
|
|
121
|
-
lines.push(`- ${letter}: ${label}${desc}`);
|
|
122
|
+
lines.push(`- ${letter}: ${label}${recommended}${desc}`);
|
|
122
123
|
});
|
|
123
124
|
if (interaction.allowCustom) {
|
|
124
125
|
lines.push('- Other: Provide a custom response');
|
package/lib/runtime/prompt.js
CHANGED
|
@@ -51,9 +51,45 @@ export async function askHuman(question, options = {}) {
|
|
|
51
51
|
validation: interaction?.validation,
|
|
52
52
|
confirmLabel: interaction?.confirmLabel,
|
|
53
53
|
cancelLabel: interaction?.cancelLabel,
|
|
54
|
-
context: interaction?.context
|
|
54
|
+
context: interaction?.context,
|
|
55
|
+
// Include full-auto info for remote UI countdown
|
|
56
|
+
fullAuto: runtime.workflowConfig.fullAuto || false,
|
|
57
|
+
autoSelectDelay: runtime.workflowConfig.autoSelectDelay ?? 20
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
// Full-auto mode: show countdown and auto-select first option for choice interactions
|
|
61
|
+
if (runtime.workflowConfig.fullAuto && interaction?.type === 'choice') {
|
|
62
|
+
const options = interaction.options || [];
|
|
63
|
+
if (options.length > 0) {
|
|
64
|
+
const firstOption = options[0];
|
|
65
|
+
const autoResponse = firstOption.key || firstOption.label;
|
|
66
|
+
const delay = runtime.workflowConfig.autoSelectDelay ?? 20;
|
|
67
|
+
|
|
68
|
+
console.log(`\n${C.cyan}${C.bold}${interaction.prompt || 'Choice required'}${C.reset}`);
|
|
69
|
+
if (runtime.remoteEnabled && runtime.remoteUrl) {
|
|
70
|
+
console.log(`${C.dim}(Remote: ${runtime.remoteUrl})${C.reset}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Countdown timer
|
|
74
|
+
for (let i = delay; i > 0; i--) {
|
|
75
|
+
process.stdout.write(`\r${C.yellow}⚡ Agent deciding for you in ${i}...${C.reset} `);
|
|
76
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
77
|
+
}
|
|
78
|
+
console.log(`\r${C.green}${C.bold}⚡ Auto-selected: ${autoResponse}${C.reset} \n`);
|
|
79
|
+
|
|
80
|
+
runtime._rawMemory[memoryKey] = autoResponse;
|
|
81
|
+
runtime.persist();
|
|
82
|
+
|
|
83
|
+
await runtime.prependHistory({
|
|
84
|
+
event: 'PROMPT_AUTO_ANSWERED',
|
|
85
|
+
slug,
|
|
86
|
+
autoSelected: autoResponse
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return autoResponse;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
// Check if we're in TTY mode (interactive terminal)
|
|
58
94
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
59
95
|
// Interactive mode - prompt directly, with remote support
|
package/lib/runtime/runtime.js
CHANGED
|
@@ -80,7 +80,11 @@ export class WorkflowRuntime {
|
|
|
80
80
|
projectRoot: path.resolve(workflowDir, '../..'),
|
|
81
81
|
fileTracking: true,
|
|
82
82
|
fileTrackingIgnore: DEFAULT_IGNORE,
|
|
83
|
-
fileTrackingKeepDeleted: false
|
|
83
|
+
fileTrackingKeepDeleted: false,
|
|
84
|
+
// Full-auto mode (auto-select first option for choice interactions)
|
|
85
|
+
fullAuto: false,
|
|
86
|
+
maxQuickFixAttempts: 10,
|
|
87
|
+
autoSelectDelay: 20 // seconds before auto-selecting in full-auto mode
|
|
84
88
|
};
|
|
85
89
|
|
|
86
90
|
// Load steering
|
|
@@ -320,6 +324,8 @@ export class WorkflowRuntime {
|
|
|
320
324
|
configUrl.searchParams.set('t', Date.now().toString());
|
|
321
325
|
const configModule = await import(configUrl.href);
|
|
322
326
|
const cfg = configModule.config || configModule.default || {};
|
|
327
|
+
// Preserve CLI-set fullAuto (it takes precedence over config.js)
|
|
328
|
+
const cliFullAuto = this.workflowConfig.fullAuto;
|
|
323
329
|
this.workflowConfig = {
|
|
324
330
|
models: cfg.models || {},
|
|
325
331
|
apiKeys: cfg.apiKeys || {},
|
|
@@ -328,7 +334,11 @@ export class WorkflowRuntime {
|
|
|
328
334
|
projectRoot: cfg.projectRoot || path.resolve(this.workflowDir, '../..'),
|
|
329
335
|
fileTracking: cfg.fileTracking ?? true,
|
|
330
336
|
fileTrackingIgnore: cfg.fileTrackingIgnore || DEFAULT_IGNORE,
|
|
331
|
-
fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false
|
|
337
|
+
fileTrackingKeepDeleted: cfg.fileTrackingKeepDeleted ?? false,
|
|
338
|
+
// Full-auto mode: CLI flag takes precedence, then config.js, then default false
|
|
339
|
+
fullAuto: cliFullAuto || cfg.fullAuto || false,
|
|
340
|
+
maxQuickFixAttempts: cfg.maxQuickFixAttempts ?? 10,
|
|
341
|
+
autoSelectDelay: cfg.autoSelectDelay ?? this.workflowConfig.autoSelectDelay // seconds before auto-selecting
|
|
332
342
|
};
|
|
333
343
|
|
|
334
344
|
// Import workflow module
|
|
@@ -368,8 +378,17 @@ export class WorkflowRuntime {
|
|
|
368
378
|
/**
|
|
369
379
|
* Wait for user to confirm interaction is complete, then return the response
|
|
370
380
|
* Supports both local TTY input and remote browser responses
|
|
381
|
+
* @param {string} filePath - Path to the interaction file
|
|
382
|
+
* @param {string} slug - Interaction slug
|
|
383
|
+
* @param {string} targetKey - Memory key to store response
|
|
384
|
+
* @param {object} [interaction] - Optional interaction object for full-auto mode
|
|
371
385
|
*/
|
|
372
|
-
async waitForInteraction(filePath, slug, targetKey) {
|
|
386
|
+
async waitForInteraction(filePath, slug, targetKey, interaction = null) {
|
|
387
|
+
// Determine if we're in full-auto mode for choice interactions
|
|
388
|
+
const isFullAutoChoice = this.workflowConfig.fullAuto && interaction?.type === 'choice' && interaction.options?.length > 0;
|
|
389
|
+
const autoResponse = isFullAutoChoice ? (interaction.options[0].key || interaction.options[0].label) : null;
|
|
390
|
+
const delay = isFullAutoChoice ? (this.workflowConfig.autoSelectDelay ?? 20) : 0;
|
|
391
|
+
|
|
373
392
|
console.log(`\n${C.yellow}${C.bold}⏸ Interaction required.${C.reset}`);
|
|
374
393
|
|
|
375
394
|
if (this.remoteEnabled && this.remoteUrl) {
|
|
@@ -387,11 +406,16 @@ export class WorkflowRuntime {
|
|
|
387
406
|
return new Promise((resolve, reject) => {
|
|
388
407
|
// Track if we've already resolved (to prevent double-resolution)
|
|
389
408
|
let resolved = false;
|
|
409
|
+
let countdownTimer = null;
|
|
390
410
|
|
|
391
411
|
const cleanup = () => {
|
|
392
412
|
resolved = true;
|
|
393
413
|
rl.close();
|
|
394
414
|
this.pendingRemoteInteraction = null;
|
|
415
|
+
if (countdownTimer) {
|
|
416
|
+
clearTimeout(countdownTimer);
|
|
417
|
+
countdownTimer = null;
|
|
418
|
+
}
|
|
395
419
|
};
|
|
396
420
|
|
|
397
421
|
// Set up remote interaction listener
|
|
@@ -414,7 +438,9 @@ export class WorkflowRuntime {
|
|
|
414
438
|
source: 'remote'
|
|
415
439
|
});
|
|
416
440
|
|
|
441
|
+
const displayResponse = typeof response === 'object' ? JSON.stringify(response) : response;
|
|
417
442
|
console.log(`\n${C.green}✓ Interaction resolved (remote): ${slug}${C.reset}`);
|
|
443
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${displayResponse}`);
|
|
418
444
|
resolve(response);
|
|
419
445
|
},
|
|
420
446
|
reject: (err) => {
|
|
@@ -425,7 +451,38 @@ export class WorkflowRuntime {
|
|
|
425
451
|
};
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
//
|
|
454
|
+
// Full-auto countdown (can be interrupted by remote submission or local input)
|
|
455
|
+
if (isFullAutoChoice) {
|
|
456
|
+
let remaining = delay;
|
|
457
|
+
const tick = () => {
|
|
458
|
+
if (resolved) return;
|
|
459
|
+
if (remaining <= 0) {
|
|
460
|
+
// Auto-select
|
|
461
|
+
cleanup();
|
|
462
|
+
console.log(`\r ${C.cyan}${C.bold}⚡ Auto-selected: ${autoResponse}${C.reset} `);
|
|
463
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${autoResponse}`);
|
|
464
|
+
|
|
465
|
+
this._rawMemory[targetKey] = autoResponse;
|
|
466
|
+
this.persist();
|
|
467
|
+
|
|
468
|
+
this.prependHistory({
|
|
469
|
+
event: 'INTERACTION_AUTO_RESOLVED',
|
|
470
|
+
slug,
|
|
471
|
+
targetKey,
|
|
472
|
+
autoSelected: autoResponse
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
resolve(autoResponse);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
process.stdout.write(`\r ${C.cyan}${C.bold}⚡ Agent deciding for you in ${remaining}...${C.reset} `);
|
|
479
|
+
remaining--;
|
|
480
|
+
countdownTimer = setTimeout(tick, 1000);
|
|
481
|
+
};
|
|
482
|
+
tick();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Local TTY input loop (always active - can interrupt countdown in full-auto mode)
|
|
429
486
|
const ask = () => {
|
|
430
487
|
if (resolved) return;
|
|
431
488
|
|
|
@@ -438,13 +495,18 @@ export class WorkflowRuntime {
|
|
|
438
495
|
// Read and return the response from file
|
|
439
496
|
try {
|
|
440
497
|
const response = await this.readInteractionResponse(filePath, slug, targetKey);
|
|
498
|
+
console.log(` ${C.cyan}Selected:${C.reset} ${typeof response === 'object' ? JSON.stringify(response) : response}`);
|
|
441
499
|
resolve(response);
|
|
442
500
|
} catch (err) {
|
|
443
501
|
reject(err);
|
|
444
502
|
}
|
|
445
503
|
} else if (a === 'q') {
|
|
446
504
|
cleanup();
|
|
447
|
-
|
|
505
|
+
console.log(`\n${C.yellow}⏹ Workflow stopped by user${C.reset}`);
|
|
506
|
+
this.status = 'STOPPED';
|
|
507
|
+
this.persist();
|
|
508
|
+
this.prependHistory({ event: 'WORKFLOW_STOPPED', reason: 'user_quit' });
|
|
509
|
+
process.exit(0);
|
|
448
510
|
} else {
|
|
449
511
|
ask();
|
|
450
512
|
}
|