agent-scenario-loop 0.1.5 → 0.1.6

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.
@@ -20,7 +20,7 @@ exports.runProfileCli = runProfileCli;
20
20
  exports.runProfileMobile = runProfileMobile;
21
21
  exports.hashScenarioContract = hashScenarioContract;
22
22
  exports.usage = usage;
23
- const { execFile } = require('node:child_process');
23
+ const { spawn } = require('node:child_process');
24
24
  const fs = require('node:fs');
25
25
  const fsp = require('node:fs/promises');
26
26
  const path = require('node:path');
@@ -35,6 +35,7 @@ const { writeUsage } = require('./cli');
35
35
  const CAPTURE_EVIDENCE_KINDS = new Set(['screenshot', 'uiTree', 'video']);
36
36
  const PROVIDER_EVIDENCE_KINDS = new Set(['accessibility', 'logs', 'profiler']);
37
37
  const SIGNAL_EVIDENCE_KINDS = new Set(['js', 'memory', 'network']);
38
+ const DEFAULT_PROVIDER_COMMAND_TIMEOUT_MS = 180_000;
38
39
  /**
39
40
  * Prints CLI usage to stderr.
40
41
  *
@@ -188,23 +189,83 @@ async function hashFileSha256(filePath) {
188
189
  return crypto.createHash('sha256').update(content).digest('hex');
189
190
  }
190
191
  /**
191
- * Runs one provider command without a shell and captures its output.
192
+ * Resolves the timeout applied to provider commands.
192
193
  *
193
- * @param {{command: string, args: string[], cwd?: string, env?: Record<string, string>}} options
194
+ * @returns {number}
195
+ */
196
+ function resolveProviderCommandTimeoutMs() {
197
+ return readPositiveInteger(process.env.ASL_PROVIDER_COMMAND_TIMEOUT_MS, DEFAULT_PROVIDER_COMMAND_TIMEOUT_MS);
198
+ }
199
+ /**
200
+ * Runs one provider command without a shell, streaming output to raw files.
201
+ *
202
+ * @param {{command: string, args: string[], cwd?: string, env?: Record<string, string>, stderrPath: string, stdoutPath: string, timeoutMs: number}} options
194
203
  * @returns {Promise<ProviderCommandResult>}
195
204
  */
196
- function execProviderCommand({ args, command, cwd, env, }) {
205
+ function execProviderCommand({ args, command, cwd, env, stderrPath, stdoutPath, timeoutMs, }) {
197
206
  return new Promise((resolve) => {
198
- execFile(command, args, {
207
+ const child = spawn(command, args, {
199
208
  ...(cwd ? { cwd } : {}),
200
209
  env: env ? { ...process.env, ...env } : process.env,
201
- }, (error, stdout, stderr) => {
210
+ shell: false,
211
+ stdio: ['ignore', 'pipe', 'pipe'],
212
+ });
213
+ const stdoutChunks = [];
214
+ const stderrChunks = [];
215
+ let timedOut = false;
216
+ let settled = false;
217
+ const timeout = setTimeout(() => {
218
+ timedOut = true;
219
+ child.kill('SIGTERM');
220
+ setTimeout(() => {
221
+ if (!settled) {
222
+ child.kill('SIGKILL');
223
+ }
224
+ }, 1000).unref();
225
+ }, timeoutMs);
226
+ timeout.unref();
227
+ child.stdout.on('data', (chunk) => {
228
+ stdoutChunks.push(chunk);
229
+ fs.appendFileSync(stdoutPath, chunk);
230
+ });
231
+ child.stderr.on('data', (chunk) => {
232
+ stderrChunks.push(chunk);
233
+ fs.appendFileSync(stderrPath, chunk);
234
+ });
235
+ child.on('error', (error) => {
236
+ if (settled) {
237
+ return;
238
+ }
239
+ clearTimeout(timeout);
240
+ settled = true;
241
+ const stderr = error.message;
242
+ fs.appendFileSync(stderrPath, `${stderr}\n`, 'utf8');
202
243
  resolve({
203
244
  args,
204
245
  command,
205
- exitCode: error && typeof error.code === 'number' ? error.code : error ? 1 : 0,
246
+ exitCode: 1,
247
+ signal: null,
248
+ stderr,
249
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
250
+ timedOut,
251
+ });
252
+ });
253
+ child.on('close', (exitCode, signal) => {
254
+ if (settled) {
255
+ return;
256
+ }
257
+ clearTimeout(timeout);
258
+ settled = true;
259
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
260
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
261
+ resolve({
262
+ args,
263
+ command,
264
+ exitCode: typeof exitCode === 'number' ? exitCode : timedOut ? 124 : 1,
265
+ signal,
206
266
  stderr,
207
267
  stdout,
268
+ timedOut,
208
269
  });
209
270
  });
210
271
  });
@@ -396,32 +457,66 @@ async function executeProviderCommands({ args, layout, platform, runDir, runId,
396
457
  ? resolveProviderPath({ context, manifestDir, value: providerCommand.cwd })
397
458
  : manifestDir;
398
459
  const resolvedEnv = Object.fromEntries(Object.entries(providerCommand.env ?? {}).map(([key, value]) => [key, applyProviderPlaceholders(value, context)]));
460
+ const commandRecordFileName = `${providerId}-${providerCommand.id}.json`;
461
+ const stdoutFileName = `${providerId}-${providerCommand.id}.stdout.txt`;
462
+ const stderrFileName = `${providerId}-${providerCommand.id}.stderr.txt`;
463
+ const commandRecordPath = path.join(commandRecordDir, commandRecordFileName);
464
+ const stdoutPath = path.join(commandRecordDir, stdoutFileName);
465
+ const stderrPath = path.join(commandRecordDir, stderrFileName);
466
+ const timeoutMs = resolveProviderCommandTimeoutMs();
467
+ const startedAt = new Date().toISOString();
468
+ await fsp.writeFile(stdoutPath, '', 'utf8');
469
+ await fsp.writeFile(stderrPath, '', 'utf8');
470
+ await fsp.writeFile(commandRecordPath, `${JSON.stringify({
471
+ args: resolvedArgs,
472
+ command: resolvedCommand,
473
+ phase: providerCommand.phase,
474
+ providerId,
475
+ startedAt,
476
+ status: 'started',
477
+ stderrPath: `raw/provider-commands/${stderrFileName}`,
478
+ stdoutPath: `raw/provider-commands/${stdoutFileName}`,
479
+ timeoutMs,
480
+ }, null, 2)}\n`, 'utf8');
399
481
  const commandResult = await execProviderCommand({
400
482
  args: resolvedArgs,
401
483
  command: resolvedCommand,
402
484
  cwd: resolvedCwd,
403
485
  env: resolvedEnv,
486
+ stderrPath,
487
+ stdoutPath,
488
+ timeoutMs,
404
489
  });
405
- const commandRecordFileName = `${providerId}-${providerCommand.id}.json`;
406
- const commandRecordPath = path.join(commandRecordDir, commandRecordFileName);
407
490
  await fsp.writeFile(commandRecordPath, `${JSON.stringify({
408
491
  args: commandResult.args,
409
492
  command: commandResult.command,
493
+ endedAt: new Date().toISOString(),
410
494
  exitCode: commandResult.exitCode,
411
495
  phase: providerCommand.phase,
412
496
  providerId,
497
+ signal: commandResult.signal,
413
498
  stderr: commandResult.stderr,
499
+ stderrPath: `raw/provider-commands/${stderrFileName}`,
500
+ status: commandResult.timedOut ? 'timed_out' : commandResult.exitCode === 0 ? 'completed' : 'failed',
414
501
  stdout: commandResult.stdout,
502
+ stdoutPath: `raw/provider-commands/${stdoutFileName}`,
503
+ timedOut: commandResult.timedOut,
504
+ timeoutMs,
415
505
  }, null, 2)}\n`, 'utf8');
416
506
  if (commandResult.exitCode !== 0) {
507
+ const timedOut = commandResult.timedOut;
417
508
  failures.push({
418
509
  commandId: providerCommand.id,
419
- code: 'provider_command_failed',
510
+ code: timedOut ? 'provider_liveness_timeout' : 'provider_command_failed',
420
511
  exitCode: commandResult.exitCode,
421
- message: `Evidence provider command ${providerId}/${providerCommand.id} failed with exit code ${commandResult.exitCode}.`,
512
+ message: timedOut
513
+ ? `Evidence provider command ${providerId}/${providerCommand.id} did not finish before the ${timeoutMs}ms timeout.`
514
+ : `Evidence provider command ${providerId}/${providerCommand.id} failed with exit code ${commandResult.exitCode}.`,
422
515
  name: 'evidence_provider_command_completed',
423
- nextAction: `Inspect raw/provider-commands/${commandRecordFileName}, fix the provider command or its environment, then rerun the profile.`,
424
- nextActionCode: 'fix_provider_command',
516
+ nextAction: timedOut
517
+ ? `Inspect raw/provider-commands/${commandRecordFileName}, raw/provider-commands/${stdoutFileName}, and raw/provider-commands/${stderrFileName}; fix the provider liveness issue or increase ASL_PROVIDER_COMMAND_TIMEOUT_MS only if the provider is making progress.`
518
+ : `Inspect raw/provider-commands/${commandRecordFileName}, fix the provider command or its environment, then rerun the profile.`,
519
+ nextActionCode: timedOut ? 'fix_provider_liveness' : 'fix_provider_command',
425
520
  phase: providerCommand.phase,
426
521
  providerId,
427
522
  rawPath: `raw/provider-commands/${commandRecordFileName}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-scenario-loop",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": false,
5
5
  "description": "Scenario orchestration and evidence collection for agent-driven software development. Bring your own runner. Keep your scenarios. Keep your evidence.",
6
6
  "license": "MIT",