@agent-relay/sdk 3.2.21 → 4.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.
Files changed (45) hide show
  1. package/README.md +10 -3
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/client.d.ts +108 -196
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +336 -824
  9. package/dist/client.js.map +1 -1
  10. package/dist/examples/example.js +2 -5
  11. package/dist/examples/example.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/relay-adapter.d.ts +9 -26
  17. package/dist/relay-adapter.d.ts.map +1 -1
  18. package/dist/relay-adapter.js +75 -47
  19. package/dist/relay-adapter.js.map +1 -1
  20. package/dist/relay.d.ts +24 -5
  21. package/dist/relay.d.ts.map +1 -1
  22. package/dist/relay.js +213 -43
  23. package/dist/relay.js.map +1 -1
  24. package/dist/transport.d.ts +58 -0
  25. package/dist/transport.d.ts.map +1 -0
  26. package/dist/transport.js +184 -0
  27. package/dist/transport.js.map +1 -0
  28. package/dist/types.d.ts +69 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/types.js +5 -0
  31. package/dist/types.js.map +1 -0
  32. package/dist/workflows/cli.js +46 -2
  33. package/dist/workflows/cli.js.map +1 -1
  34. package/dist/workflows/file-db.d.ts +2 -0
  35. package/dist/workflows/file-db.d.ts.map +1 -1
  36. package/dist/workflows/file-db.js +20 -3
  37. package/dist/workflows/file-db.js.map +1 -1
  38. package/dist/workflows/runner.d.ts +6 -1
  39. package/dist/workflows/runner.d.ts.map +1 -1
  40. package/dist/workflows/runner.js +157 -11
  41. package/dist/workflows/runner.js.map +1 -1
  42. package/dist/workflows/validator.d.ts.map +1 -1
  43. package/dist/workflows/validator.js +17 -2
  44. package/dist/workflows/validator.js.map +1 -1
  45. package/package.json +2 -2
@@ -1463,34 +1463,46 @@ export class WorkflowRunner {
1463
1463
  });
1464
1464
  }
1465
1465
  /** Resume a previously paused or partially completed run. */
1466
- async resume(runId, vars) {
1466
+ async resume(runId, vars, config) {
1467
1467
  // Set up abort controller early so callers can abort() even during setup
1468
1468
  this.abortController = new AbortController();
1469
1469
  this.paused = false;
1470
- const run = await this.db.getRun(runId);
1470
+ let run = await this.db.getRun(runId);
1471
+ let stepStates = new Map();
1471
1472
  if (!run) {
1472
- throw new Error(`Run "${runId}" not found`);
1473
+ const reconstructed = this.reconstructRunFromCache(runId, config);
1474
+ if (!reconstructed) {
1475
+ throw new Error(`Run "${runId}" not found (no database entry or cached step outputs)`);
1476
+ }
1477
+ this.log('[resume] Reconstructing run from cached step outputs (workflow-runs.jsonl missing)');
1478
+ run = reconstructed.run;
1479
+ stepStates = reconstructed.stepStates;
1480
+ await this.db.insertRun(run);
1481
+ for (const [, state] of stepStates) {
1482
+ await this.db.insertStep(state.row);
1483
+ }
1473
1484
  }
1474
1485
  this.persistRunIdHint(runId);
1475
1486
  if (run.status !== 'running' && run.status !== 'failed') {
1476
1487
  throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
1477
1488
  }
1478
- const config = vars ? this.resolveVariables(run.config, vars) : run.config;
1489
+ const resolvedConfig = vars ? this.resolveVariables(run.config, vars) : run.config;
1479
1490
  // Resolve path definitions (same as execute()) so workdir lookups work on resume
1480
- const pathResult = this.resolvePathDefinitions(config.paths, this.cwd);
1491
+ const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd);
1481
1492
  if (pathResult.errors.length > 0) {
1482
1493
  throw new Error(`Path validation failed:\n ${pathResult.errors.join('\n ')}`);
1483
1494
  }
1484
1495
  this.resolvedPaths = pathResult.resolved;
1485
- const workflows = config.workflows ?? [];
1496
+ const workflows = resolvedConfig.workflows ?? [];
1486
1497
  const workflow = workflows.find((w) => w.name === run.workflowName);
1487
1498
  if (!workflow) {
1488
1499
  throw new Error(`Workflow "${run.workflowName}" not found in stored config`);
1489
1500
  }
1490
- const existingSteps = await this.db.getStepsByRunId(runId);
1491
- const stepStates = new Map();
1492
- for (const stepRow of existingSteps) {
1493
- stepStates.set(stepRow.stepName, { row: stepRow });
1501
+ if (stepStates.size === 0) {
1502
+ const existingSteps = await this.db.getStepsByRunId(runId);
1503
+ for (const stepRow of existingSteps) {
1504
+ stepStates.set(stepRow.stepName, { row: stepRow });
1505
+ }
1494
1506
  }
1495
1507
  // Reset failed steps to pending for retry
1496
1508
  for (const [, state] of stepStates) {
@@ -1509,7 +1521,7 @@ export class WorkflowRunner {
1509
1521
  return this.runWorkflowCore({
1510
1522
  run,
1511
1523
  workflow,
1512
- config,
1524
+ config: resolvedConfig,
1513
1525
  stepStates,
1514
1526
  isResume: true,
1515
1527
  });
@@ -5133,8 +5145,15 @@ export class WorkflowRunner {
5133
5145
  .replace(/-+/g, '-')
5134
5146
  .slice(0, 32);
5135
5147
  }
5148
+ /** Validate that a runId is safe for use in file paths (no traversal). */
5149
+ validateRunId(runId) {
5150
+ if (/[/\\]|^\.\.?$/.test(runId) || runId.includes('..')) {
5151
+ throw new Error(`Invalid runId: "${runId}" contains path traversal characters`);
5152
+ }
5153
+ }
5136
5154
  /** Directory for persisted step outputs: .agent-relay/step-outputs/{runId}/ */
5137
5155
  getStepOutputDir(runId) {
5156
+ this.validateRunId(runId);
5138
5157
  return path.join(this.cwd, '.agent-relay', 'step-outputs', runId);
5139
5158
  }
5140
5159
  /** Persist step output to disk and post full output as a channel message. */
@@ -5219,6 +5238,133 @@ export class WorkflowRunner {
5219
5238
  return undefined;
5220
5239
  }
5221
5240
  }
5241
+ /** Match the best workflow from config given a set of cached step names. */
5242
+ matchWorkflowFromCache(workflows, cachedStepNames) {
5243
+ if (workflows.length === 1)
5244
+ return workflows[0];
5245
+ if (cachedStepNames.size === 0) {
5246
+ // No cached steps to disambiguate — ambiguous when multiple workflows exist
5247
+ this.log('[resume] Multiple workflows in config with empty cache — cannot disambiguate');
5248
+ return null;
5249
+ }
5250
+ // Score each workflow by how many cached steps match, excluding those with unknown steps
5251
+ const scored = workflows
5252
+ .map((candidate) => ({
5253
+ workflow: candidate,
5254
+ matchedSteps: candidate.steps.filter((step) => cachedStepNames.has(step.name)).length,
5255
+ unknownSteps: [...cachedStepNames].filter((name) => !candidate.steps.some((step) => step.name === name)).length,
5256
+ }))
5257
+ .filter((candidate) => candidate.unknownSteps === 0)
5258
+ .sort((a, b) => b.matchedSteps - a.matchedSteps);
5259
+ return scored[0]?.workflow ?? null;
5260
+ }
5261
+ reconstructRunFromCache(runId, config) {
5262
+ const stepOutputDir = this.getStepOutputDir(runId);
5263
+ if (!existsSync(stepOutputDir))
5264
+ return null;
5265
+ let resumeConfig = config ?? this.currentConfig;
5266
+ if (!resumeConfig) {
5267
+ // Attempt to load config from relay.yaml on disk (resume() may call before runWorkflowCore sets currentConfig)
5268
+ const yamlPath = path.join(this.cwd, 'relay.yaml');
5269
+ if (existsSync(yamlPath)) {
5270
+ try {
5271
+ const raw = readFileSync(yamlPath, 'utf-8');
5272
+ resumeConfig = this.parseYamlString(raw, yamlPath);
5273
+ }
5274
+ catch {
5275
+ return null;
5276
+ }
5277
+ }
5278
+ else {
5279
+ return null;
5280
+ }
5281
+ }
5282
+ let entries;
5283
+ try {
5284
+ entries = readdirSync(stepOutputDir, { withFileTypes: true });
5285
+ }
5286
+ catch {
5287
+ return null;
5288
+ }
5289
+ const cachedStepNames = new Set(entries
5290
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
5291
+ .map((entry) => entry.name.slice(0, -3))
5292
+ .filter(Boolean));
5293
+ const workflows = resumeConfig.workflows ?? [];
5294
+ if (workflows.length === 0)
5295
+ return null;
5296
+ // Empty cache directory is valid — all steps will be re-run
5297
+ const workflow = this.matchWorkflowFromCache(workflows, cachedStepNames);
5298
+ if (!workflow)
5299
+ return null;
5300
+ // Use actual file modification times from cached outputs instead of synthetic timestamps
5301
+ const stepMtimes = new Map();
5302
+ let earliestMtime = Date.now();
5303
+ for (const stepName of cachedStepNames) {
5304
+ try {
5305
+ const mdPath = path.join(stepOutputDir, `${stepName}.md`);
5306
+ const reportPath = path.join(stepOutputDir, `${stepName}.report.json`);
5307
+ const mdStat = existsSync(mdPath) ? statSync(mdPath) : null;
5308
+ const reportStat = existsSync(reportPath) ? statSync(reportPath) : null;
5309
+ // Use the latest mtime between .md and .report.json
5310
+ const mtime = Math.max(mdStat?.mtimeMs ?? 0, reportStat?.mtimeMs ?? 0);
5311
+ if (mtime > 0) {
5312
+ stepMtimes.set(stepName, new Date(mtime).toISOString());
5313
+ if (mtime < earliestMtime)
5314
+ earliestMtime = mtime;
5315
+ }
5316
+ }
5317
+ catch {
5318
+ // Fall back to current time if stat fails
5319
+ }
5320
+ }
5321
+ const fallbackTime = new Date().toISOString();
5322
+ const completedSteps = new Set(workflow.steps.filter((step) => cachedStepNames.has(step.name)).map((step) => step.name));
5323
+ // Heuristic: mark the first eligible non-completed step as failed (the likely failure point)
5324
+ const failedStepName = workflow.steps.find((step) => !completedSteps.has(step.name) && (step.dependsOn ?? []).every((dep) => completedSteps.has(dep)))?.name;
5325
+ const runStartedAt = new Date(earliestMtime).toISOString();
5326
+ const run = {
5327
+ id: runId,
5328
+ workspaceId: this.workspaceId,
5329
+ workflowName: workflow.name,
5330
+ pattern: resumeConfig.swarm.pattern,
5331
+ status: 'failed',
5332
+ config: resumeConfig,
5333
+ startedAt: runStartedAt,
5334
+ createdAt: runStartedAt,
5335
+ updatedAt: fallbackTime,
5336
+ };
5337
+ const stepStates = new Map();
5338
+ for (const step of workflow.steps) {
5339
+ const isNonAgent = step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
5340
+ const cachedOutput = completedSteps.has(step.name) ? this.loadStepOutput(runId, step.name) : undefined;
5341
+ const status = completedSteps.has(step.name) ? 'completed' : step.name === failedStepName ? 'failed' : 'pending';
5342
+ const stepRow = {
5343
+ id: this.generateId(),
5344
+ runId,
5345
+ stepName: step.name,
5346
+ agentName: isNonAgent ? null : (step.agent ?? null),
5347
+ stepType: isNonAgent ? step.type : 'agent',
5348
+ status,
5349
+ task: step.type === 'deterministic'
5350
+ ? (step.command ?? '')
5351
+ : step.type === 'worktree'
5352
+ ? (step.branch ?? '')
5353
+ : step.type === 'integration'
5354
+ ? (`${step.integration}.${step.action}`)
5355
+ : (step.task ?? ''),
5356
+ dependsOn: step.dependsOn ?? [],
5357
+ output: cachedOutput,
5358
+ error: status === 'failed' ? 'Recovered from cached step outputs' : undefined,
5359
+ completedAt: status === 'completed' ? (stepMtimes.get(step.name) ?? fallbackTime) : undefined,
5360
+ retryCount: 0,
5361
+ createdAt: stepMtimes.get(step.name) ?? fallbackTime,
5362
+ updatedAt: stepMtimes.get(step.name) ?? fallbackTime,
5363
+ };
5364
+ stepStates.set(step.name, { row: stepRow });
5365
+ }
5366
+ return { run, stepStates };
5367
+ }
5222
5368
  /** Get or create the worker logs directory (.agent-relay/team/worker-logs) */
5223
5369
  getWorkerLogsDir() {
5224
5370
  const logsDir = path.join(this.cwd, '.agent-relay', 'team', 'worker-logs');