@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.
- package/README.md +10 -3
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/client.d.ts +108 -196
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +336 -824
- package/dist/client.js.map +1 -1
- package/dist/examples/example.js +2 -5
- package/dist/examples/example.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/relay-adapter.d.ts +9 -26
- package/dist/relay-adapter.d.ts.map +1 -1
- package/dist/relay-adapter.js +75 -47
- package/dist/relay-adapter.js.map +1 -1
- package/dist/relay.d.ts +24 -5
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +213 -43
- package/dist/relay.js.map +1 -1
- package/dist/transport.d.ts +58 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +184 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/workflows/cli.js +46 -2
- package/dist/workflows/cli.js.map +1 -1
- package/dist/workflows/file-db.d.ts +2 -0
- package/dist/workflows/file-db.d.ts.map +1 -1
- package/dist/workflows/file-db.js +20 -3
- package/dist/workflows/file-db.js.map +1 -1
- package/dist/workflows/runner.d.ts +6 -1
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +157 -11
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/validator.d.ts.map +1 -1
- package/dist/workflows/validator.js +17 -2
- package/dist/workflows/validator.js.map +1 -1
- package/package.json +2 -2
package/dist/workflows/runner.js
CHANGED
|
@@ -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
|
-
|
|
1470
|
+
let run = await this.db.getRun(runId);
|
|
1471
|
+
let stepStates = new Map();
|
|
1471
1472
|
if (!run) {
|
|
1472
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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');
|