agent-relay 3.1.15 → 3.1.17
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/dist/index.cjs +683 -35
- package/dist/src/cli/commands/core.d.ts +2 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +1 -0
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +19 -2
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/README.md +4 -7
- package/packages/openclaw/dist/setup.d.ts.map +1 -1
- package/packages/openclaw/dist/setup.js +10 -3
- package/packages/openclaw/dist/setup.js.map +1 -1
- package/packages/openclaw/package.json +2 -2
- package/packages/openclaw/skill/SKILL.md +27 -5
- package/packages/openclaw/src/setup.ts +10 -3
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +16 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +1 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +640 -0
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -0
- package/packages/sdk/dist/client.d.ts +1 -0
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +3 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/workflows/cli.js +10 -0
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +51 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +655 -33
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts +22 -1
- package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/trajectory.js +55 -8
- package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +26 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +29 -0
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +778 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +484 -9
- package/packages/sdk/src/client.ts +4 -0
- package/packages/sdk/src/workflows/README.md +11 -0
- package/packages/sdk/src/workflows/cli.ts +10 -0
- package/packages/sdk/src/workflows/runner.ts +854 -34
- package/packages/sdk/src/workflows/schema.json +219 -40
- package/packages/sdk/src/workflows/trajectory.ts +89 -8
- package/packages/sdk/src/workflows/types.ts +29 -0
- package/packages/sdk/src/workflows/validator.ts +29 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +2 -0
- package/packages/sdk-py/src/agent_relay/types.py +33 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -9052,6 +9052,9 @@ var AgentRelayClient = class _AgentRelayClient {
|
|
|
9052
9052
|
this.stderrListeners.delete(listener);
|
|
9053
9053
|
};
|
|
9054
9054
|
}
|
|
9055
|
+
get brokerPid() {
|
|
9056
|
+
return this.child?.pid;
|
|
9057
|
+
}
|
|
9055
9058
|
async start() {
|
|
9056
9059
|
if (this.child) {
|
|
9057
9060
|
return;
|
|
@@ -47269,18 +47272,66 @@ var WorkflowTrajectory = class {
|
|
|
47269
47272
|
}
|
|
47270
47273
|
// ── Step events ────────────────────────────────────────────────────────────
|
|
47271
47274
|
/** Record step started — captures intent, not just assignment. */
|
|
47272
|
-
async stepStarted(step, agent) {
|
|
47275
|
+
async stepStarted(step, agent, participants) {
|
|
47276
|
+
if (!this.enabled || !this.trajectory)
|
|
47277
|
+
return;
|
|
47278
|
+
await this.registerAgent(agent, participants?.role ?? step.agent ?? "deterministic");
|
|
47279
|
+
if (participants?.owner && participants.owner !== agent) {
|
|
47280
|
+
await this.registerAgent(participants.owner, "owner");
|
|
47281
|
+
}
|
|
47282
|
+
if (participants?.specialist) {
|
|
47283
|
+
await this.registerAgent(participants.specialist, "specialist");
|
|
47284
|
+
}
|
|
47285
|
+
if (participants?.reviewer) {
|
|
47286
|
+
await this.registerAgent(participants.reviewer, "reviewer");
|
|
47287
|
+
}
|
|
47288
|
+
const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
|
|
47289
|
+
this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
|
|
47290
|
+
await this.flush();
|
|
47291
|
+
}
|
|
47292
|
+
async registerAgent(name, role) {
|
|
47273
47293
|
if (!this.enabled || !this.trajectory)
|
|
47274
47294
|
return;
|
|
47275
|
-
if (!this.trajectory.agents.some((a) => a.name ===
|
|
47295
|
+
if (!this.trajectory.agents.some((a) => a.name === name)) {
|
|
47276
47296
|
this.trajectory.agents.push({
|
|
47277
|
-
name
|
|
47278
|
-
role
|
|
47297
|
+
name,
|
|
47298
|
+
role,
|
|
47279
47299
|
joinedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
47280
47300
|
});
|
|
47301
|
+
await this.flush();
|
|
47281
47302
|
}
|
|
47282
|
-
|
|
47283
|
-
|
|
47303
|
+
}
|
|
47304
|
+
async stepSupervisionAssigned(step, supervised) {
|
|
47305
|
+
if (!this.enabled || !this.trajectory)
|
|
47306
|
+
return;
|
|
47307
|
+
await this.registerAgent(supervised.owner.name, "owner");
|
|
47308
|
+
await this.registerAgent(supervised.specialist.name, "specialist");
|
|
47309
|
+
if (supervised.reviewer?.name) {
|
|
47310
|
+
await this.registerAgent(supervised.reviewer.name, "reviewer");
|
|
47311
|
+
}
|
|
47312
|
+
const reviewerNote = supervised.reviewer?.name ? `, reviewer=${supervised.reviewer.name}` : "";
|
|
47313
|
+
this.addEvent("decision", `"${step.name}" supervision assigned \u2192 owner=${supervised.owner.name}, specialist=${supervised.specialist.name}${reviewerNote}`, "medium", {
|
|
47314
|
+
owner: supervised.owner.name,
|
|
47315
|
+
specialist: supervised.specialist.name,
|
|
47316
|
+
reviewer: supervised.reviewer?.name
|
|
47317
|
+
});
|
|
47318
|
+
await this.flush();
|
|
47319
|
+
}
|
|
47320
|
+
async ownerMonitoringEvent(stepName, owner, detail, raw) {
|
|
47321
|
+
if (!this.enabled || !this.trajectory)
|
|
47322
|
+
return;
|
|
47323
|
+
this.addEvent("note", `"${stepName}" owner ${owner}: ${detail}`, "medium", raw ? { owner, ...raw } : { owner });
|
|
47324
|
+
await this.flush();
|
|
47325
|
+
}
|
|
47326
|
+
async reviewCompleted(stepName, reviewerName, decision, reason) {
|
|
47327
|
+
if (!this.enabled || !this.trajectory)
|
|
47328
|
+
return;
|
|
47329
|
+
this.addEvent("review-completed", `"${stepName}" review ${decision} by ${reviewerName}`, "medium", {
|
|
47330
|
+
stepName,
|
|
47331
|
+
reviewer: reviewerName,
|
|
47332
|
+
decision,
|
|
47333
|
+
reason
|
|
47334
|
+
});
|
|
47284
47335
|
await this.flush();
|
|
47285
47336
|
}
|
|
47286
47337
|
/** Record step completed — captures what was accomplished. */
|
|
@@ -47610,6 +47661,10 @@ var WorkflowRunner = class _WorkflowRunner {
|
|
|
47610
47661
|
lastIdleLog = /* @__PURE__ */ new Map();
|
|
47611
47662
|
/** Tracks last logged activity type per agent to avoid duplicate status lines. */
|
|
47612
47663
|
lastActivity = /* @__PURE__ */ new Map();
|
|
47664
|
+
/** Runtime-name lookup for agents participating in supervised owner flows. */
|
|
47665
|
+
supervisedRuntimeAgents = /* @__PURE__ */ new Map();
|
|
47666
|
+
/** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
|
|
47667
|
+
resolvedPaths = /* @__PURE__ */ new Map();
|
|
47613
47668
|
constructor(options = {}) {
|
|
47614
47669
|
this.db = options.db ?? new InMemoryWorkflowDb();
|
|
47615
47670
|
this.workspaceId = options.workspaceId ?? "local";
|
|
@@ -47619,6 +47674,75 @@ var WorkflowRunner = class _WorkflowRunner {
|
|
|
47619
47674
|
this.workersPath = import_node_path8.default.join(this.cwd, ".agent-relay", "team", "workers.json");
|
|
47620
47675
|
this.executor = options.executor;
|
|
47621
47676
|
}
|
|
47677
|
+
// ── Path resolution ─────────────────────────────────────────────────────
|
|
47678
|
+
/** Expand environment variables like $HOME or $VAR in a path string. */
|
|
47679
|
+
static resolveEnvVars(p) {
|
|
47680
|
+
return p.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, varName) => {
|
|
47681
|
+
return process.env[varName] ?? _match;
|
|
47682
|
+
});
|
|
47683
|
+
}
|
|
47684
|
+
/**
|
|
47685
|
+
* Resolve and validate the top-level `paths` definitions from the config.
|
|
47686
|
+
* Returns a map of name → absolute directory path.
|
|
47687
|
+
* Throws if a required path does not exist.
|
|
47688
|
+
*/
|
|
47689
|
+
resolvePathDefinitions(pathDefs, baseCwd) {
|
|
47690
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
47691
|
+
const errors = [];
|
|
47692
|
+
const warnings = [];
|
|
47693
|
+
if (!pathDefs || pathDefs.length === 0)
|
|
47694
|
+
return { resolved, errors, warnings };
|
|
47695
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
47696
|
+
for (const pd of pathDefs) {
|
|
47697
|
+
if (seenNames.has(pd.name)) {
|
|
47698
|
+
errors.push(`Duplicate path name "${pd.name}"`);
|
|
47699
|
+
continue;
|
|
47700
|
+
}
|
|
47701
|
+
seenNames.add(pd.name);
|
|
47702
|
+
const expanded = _WorkflowRunner.resolveEnvVars(pd.path);
|
|
47703
|
+
const abs = import_node_path8.default.resolve(baseCwd, expanded);
|
|
47704
|
+
resolved.set(pd.name, abs);
|
|
47705
|
+
const isRequired = pd.required !== false;
|
|
47706
|
+
if (!(0, import_node_fs4.existsSync)(abs)) {
|
|
47707
|
+
if (isRequired) {
|
|
47708
|
+
errors.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (required)`);
|
|
47709
|
+
} else {
|
|
47710
|
+
warnings.push(`Path "${pd.name}" resolves to "${abs}" which does not exist (optional)`);
|
|
47711
|
+
}
|
|
47712
|
+
}
|
|
47713
|
+
}
|
|
47714
|
+
return { resolved, errors, warnings };
|
|
47715
|
+
}
|
|
47716
|
+
/**
|
|
47717
|
+
* Resolve an agent's effective working directory, considering `workdir` (named path reference)
|
|
47718
|
+
* and `cwd` (explicit path). `workdir` takes precedence when both are set.
|
|
47719
|
+
*/
|
|
47720
|
+
resolveAgentCwd(agent) {
|
|
47721
|
+
if (agent.workdir) {
|
|
47722
|
+
const resolved = this.resolvedPaths.get(agent.workdir);
|
|
47723
|
+
if (!resolved) {
|
|
47724
|
+
throw new Error(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
|
|
47725
|
+
}
|
|
47726
|
+
return resolved;
|
|
47727
|
+
}
|
|
47728
|
+
if (agent.cwd) {
|
|
47729
|
+
return import_node_path8.default.resolve(this.cwd, agent.cwd);
|
|
47730
|
+
}
|
|
47731
|
+
return this.cwd;
|
|
47732
|
+
}
|
|
47733
|
+
/**
|
|
47734
|
+
* Resolve a step's working directory from its `workdir` field (named path reference).
|
|
47735
|
+
* Returns undefined if no workdir is set.
|
|
47736
|
+
*/
|
|
47737
|
+
resolveStepWorkdir(step) {
|
|
47738
|
+
if (!step.workdir)
|
|
47739
|
+
return void 0;
|
|
47740
|
+
const resolved = this.resolvedPaths.get(step.workdir);
|
|
47741
|
+
if (!resolved) {
|
|
47742
|
+
throw new Error(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
|
|
47743
|
+
}
|
|
47744
|
+
return resolved;
|
|
47745
|
+
}
|
|
47622
47746
|
// ── Progress logging ────────────────────────────────────────────────────
|
|
47623
47747
|
/** Log a progress message with elapsed time since run start. */
|
|
47624
47748
|
log(msg) {
|
|
@@ -47852,6 +47976,15 @@ var WorkflowRunner = class _WorkflowRunner {
|
|
|
47852
47976
|
estimatedWaves: 0
|
|
47853
47977
|
};
|
|
47854
47978
|
}
|
|
47979
|
+
const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
|
|
47980
|
+
errors.push(...pathResult.errors);
|
|
47981
|
+
warnings.push(...pathResult.warnings);
|
|
47982
|
+
const dryRunPaths = pathResult.resolved;
|
|
47983
|
+
for (const agent of resolved.agents) {
|
|
47984
|
+
if (agent.workdir && !dryRunPaths.has(agent.workdir)) {
|
|
47985
|
+
errors.push(`Agent "${agent.name}" references workdir "${agent.workdir}" which is not defined in paths`);
|
|
47986
|
+
}
|
|
47987
|
+
}
|
|
47855
47988
|
const workflows = resolved.workflows ?? [];
|
|
47856
47989
|
const workflow2 = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
|
|
47857
47990
|
if (!workflow2) {
|
|
@@ -47909,6 +48042,11 @@ ${err.suggestion}`);
|
|
|
47909
48042
|
stepAgentCounts.set(step.agent, (stepAgentCounts.get(step.agent) ?? 0) + 1);
|
|
47910
48043
|
}
|
|
47911
48044
|
}
|
|
48045
|
+
for (const step of resolvedSteps) {
|
|
48046
|
+
if (step.workdir && !dryRunPaths.has(step.workdir)) {
|
|
48047
|
+
errors.push(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
|
|
48048
|
+
}
|
|
48049
|
+
}
|
|
47912
48050
|
for (const agent of resolved.agents) {
|
|
47913
48051
|
if (agent.cwd) {
|
|
47914
48052
|
const resolvedCwd = import_node_path8.default.resolve(this.cwd, agent.cwd);
|
|
@@ -47989,7 +48127,7 @@ ${err.suggestion}`);
|
|
|
47989
48127
|
name: a.name,
|
|
47990
48128
|
cli: a.cli,
|
|
47991
48129
|
role: a.role,
|
|
47992
|
-
cwd: a.cwd,
|
|
48130
|
+
cwd: a.workdir ? dryRunPaths.get(a.workdir) : a.cwd,
|
|
47993
48131
|
stepCount: stepAgentCounts.get(a.name) ?? 0
|
|
47994
48132
|
}));
|
|
47995
48133
|
const waves = [];
|
|
@@ -48235,6 +48373,17 @@ ${err.suggestion}`);
|
|
|
48235
48373
|
/** Execute a named workflow from a validated config. */
|
|
48236
48374
|
async execute(config3, workflowName, vars) {
|
|
48237
48375
|
const resolved = vars ? this.resolveVariables(config3, vars) : config3;
|
|
48376
|
+
const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
|
|
48377
|
+
if (pathResult.errors.length > 0) {
|
|
48378
|
+
throw new Error(`Path validation failed:
|
|
48379
|
+
${pathResult.errors.join("\n ")}`);
|
|
48380
|
+
}
|
|
48381
|
+
this.resolvedPaths = pathResult.resolved;
|
|
48382
|
+
if (this.resolvedPaths.size > 0) {
|
|
48383
|
+
for (const [name, abs] of this.resolvedPaths) {
|
|
48384
|
+
console.log(`[workflow] path "${name}" \u2192 ${abs}`);
|
|
48385
|
+
}
|
|
48386
|
+
}
|
|
48238
48387
|
const workflows = resolved.workflows ?? [];
|
|
48239
48388
|
const workflow2 = workflowName ? workflows.find((w) => w.name === workflowName) : workflows[0];
|
|
48240
48389
|
if (!workflow2) {
|
|
@@ -48294,6 +48443,12 @@ ${err.suggestion}`);
|
|
|
48294
48443
|
throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
|
|
48295
48444
|
}
|
|
48296
48445
|
const config3 = vars ? this.resolveVariables(run.config, vars) : run.config;
|
|
48446
|
+
const pathResult = this.resolvePathDefinitions(config3.paths, this.cwd);
|
|
48447
|
+
if (pathResult.errors.length > 0) {
|
|
48448
|
+
throw new Error(`Path validation failed:
|
|
48449
|
+
${pathResult.errors.join("\n ")}`);
|
|
48450
|
+
}
|
|
48451
|
+
this.resolvedPaths = pathResult.resolved;
|
|
48297
48452
|
const workflows = config3.workflows ?? [];
|
|
48298
48453
|
const workflow2 = workflows.find((w) => w.name === run.workflowName);
|
|
48299
48454
|
if (!workflow2) {
|
|
@@ -48418,6 +48573,10 @@ ${err.suggestion}`);
|
|
|
48418
48573
|
const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
|
|
48419
48574
|
const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
|
|
48420
48575
|
this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
|
|
48576
|
+
const supervision = this.supervisedRuntimeAgents.get(msg.from);
|
|
48577
|
+
if (supervision?.role === "owner") {
|
|
48578
|
+
void this.trajectory?.ownerMonitoringEvent(supervision.stepName, supervision.logicalName, `Messaged ${msg.to}: ${msg.text.slice(0, 120)}`, { to: msg.to, text: msg.text });
|
|
48579
|
+
}
|
|
48421
48580
|
};
|
|
48422
48581
|
this.relay.onAgentSpawned = (agent) => {
|
|
48423
48582
|
if (!this.activeAgentHandles.has(agent.name)) {
|
|
@@ -48520,6 +48679,7 @@ ${err.suggestion}`);
|
|
|
48520
48679
|
}
|
|
48521
48680
|
this.lastIdleLog.clear();
|
|
48522
48681
|
this.lastActivity.clear();
|
|
48682
|
+
this.supervisedRuntimeAgents.clear();
|
|
48523
48683
|
this.log("Shutting down broker...");
|
|
48524
48684
|
await this.relay?.shutdown();
|
|
48525
48685
|
this.relay = void 0;
|
|
@@ -48769,9 +48929,10 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
48769
48929
|
const value = this.resolveDotPath(key, stepOutputContext);
|
|
48770
48930
|
return value !== void 0 ? String(value) : _match;
|
|
48771
48931
|
});
|
|
48932
|
+
const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
|
|
48772
48933
|
try {
|
|
48773
48934
|
if (this.executor?.executeDeterministicStep) {
|
|
48774
|
-
const result = await this.executor.executeDeterministicStep(step, resolvedCommand,
|
|
48935
|
+
const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
|
|
48775
48936
|
const failOnError = step.failOnError !== false;
|
|
48776
48937
|
if (failOnError && result.exitCode !== 0) {
|
|
48777
48938
|
throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
|
|
@@ -48793,7 +48954,7 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
48793
48954
|
const output = await new Promise((resolve3, reject) => {
|
|
48794
48955
|
const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
|
|
48795
48956
|
stdio: "pipe",
|
|
48796
|
-
cwd:
|
|
48957
|
+
cwd: stepCwd,
|
|
48797
48958
|
env: { ...process.env }
|
|
48798
48959
|
});
|
|
48799
48960
|
const stdoutChunks = [];
|
|
@@ -48896,19 +49057,20 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
48896
49057
|
const baseBranch = step.baseBranch ? this.interpolateStepTask(step.baseBranch, stepOutputContext) : "HEAD";
|
|
48897
49058
|
const worktreePath = step.path ? this.interpolateStepTask(step.path, stepOutputContext) : import_node_path8.default.join(".worktrees", step.name);
|
|
48898
49059
|
const createBranch = step.createBranch !== false;
|
|
49060
|
+
const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
|
|
48899
49061
|
if (!branch) {
|
|
48900
49062
|
const errorMsg = 'Worktree step missing required "branch" field';
|
|
48901
49063
|
await this.markStepFailed(state, errorMsg, runId);
|
|
48902
49064
|
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
48903
49065
|
}
|
|
48904
49066
|
try {
|
|
48905
|
-
const absoluteWorktreePath = import_node_path8.default.resolve(
|
|
49067
|
+
const absoluteWorktreePath = import_node_path8.default.resolve(stepCwd, worktreePath);
|
|
48906
49068
|
const checkBranchCmd = `git rev-parse --verify --quiet ${branch} 2>/dev/null`;
|
|
48907
49069
|
let branchExists = false;
|
|
48908
49070
|
await new Promise((resolve3) => {
|
|
48909
49071
|
const checkChild = (0, import_node_child_process3.spawn)("sh", ["-c", checkBranchCmd], {
|
|
48910
49072
|
stdio: "pipe",
|
|
48911
|
-
cwd:
|
|
49073
|
+
cwd: stepCwd,
|
|
48912
49074
|
env: { ...process.env }
|
|
48913
49075
|
});
|
|
48914
49076
|
checkChild.on("close", (code) => {
|
|
@@ -48930,7 +49092,7 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
48930
49092
|
const output = await new Promise((resolve3, reject) => {
|
|
48931
49093
|
const child = (0, import_node_child_process3.spawn)("sh", ["-c", worktreeCmd], {
|
|
48932
49094
|
stdio: "pipe",
|
|
48933
|
-
cwd:
|
|
49095
|
+
cwd: stepCwd,
|
|
48934
49096
|
env: { ...process.env }
|
|
48935
49097
|
});
|
|
48936
49098
|
const stdoutChunks = [];
|
|
@@ -49024,10 +49186,19 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
49024
49186
|
if (!rawAgentDef) {
|
|
49025
49187
|
throw new Error(`Agent "${agentName}" not found in config`);
|
|
49026
49188
|
}
|
|
49027
|
-
const
|
|
49028
|
-
const
|
|
49189
|
+
const specialistDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
|
|
49190
|
+
const usesOwnerFlow = specialistDef.interactive !== false;
|
|
49191
|
+
const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
|
|
49192
|
+
const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
|
|
49193
|
+
const supervised = {
|
|
49194
|
+
specialist: specialistDef,
|
|
49195
|
+
owner: ownerDef,
|
|
49196
|
+
reviewer: reviewDef
|
|
49197
|
+
};
|
|
49198
|
+
const usesDedicatedOwner = usesOwnerFlow && ownerDef.name !== specialistDef.name;
|
|
49199
|
+
const maxRetries = step.retries ?? ownerDef.constraints?.retries ?? specialistDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
|
|
49029
49200
|
const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
|
|
49030
|
-
const timeoutMs = step.timeoutMs ??
|
|
49201
|
+
const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
|
|
49031
49202
|
let lastError;
|
|
49032
49203
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
49033
49204
|
this.checkAborted();
|
|
@@ -49051,41 +49222,95 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
49051
49222
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
49052
49223
|
});
|
|
49053
49224
|
this.emit({ type: "step:started", runId, stepName: step.name });
|
|
49054
|
-
this.postToChannel(`**[${step.name}]** Started (
|
|
49055
|
-
await this.trajectory?.stepStarted(step,
|
|
49225
|
+
this.postToChannel(`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
|
|
49226
|
+
await this.trajectory?.stepStarted(step, ownerDef.name, {
|
|
49227
|
+
role: usesDedicatedOwner ? "owner" : "specialist",
|
|
49228
|
+
owner: ownerDef.name,
|
|
49229
|
+
specialist: specialistDef.name,
|
|
49230
|
+
reviewer: reviewDef?.name
|
|
49231
|
+
});
|
|
49232
|
+
if (usesDedicatedOwner) {
|
|
49233
|
+
await this.trajectory?.stepSupervisionAssigned(step, supervised);
|
|
49234
|
+
}
|
|
49235
|
+
this.emit({
|
|
49236
|
+
type: "step:owner-assigned",
|
|
49237
|
+
runId,
|
|
49238
|
+
stepName: step.name,
|
|
49239
|
+
ownerName: ownerDef.name,
|
|
49240
|
+
specialistName: specialistDef.name
|
|
49241
|
+
});
|
|
49056
49242
|
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
49057
49243
|
let resolvedTask = this.interpolateStepTask(step.task ?? "", stepOutputContext);
|
|
49058
|
-
if (
|
|
49244
|
+
if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
|
|
49059
49245
|
const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
|
|
49060
49246
|
if (nonInteractiveInfo) {
|
|
49061
49247
|
resolvedTask += nonInteractiveInfo;
|
|
49062
49248
|
}
|
|
49063
49249
|
}
|
|
49064
|
-
|
|
49065
|
-
|
|
49066
|
-
|
|
49067
|
-
|
|
49250
|
+
const applyStepWorkdir = (def) => {
|
|
49251
|
+
if (step.workdir) {
|
|
49252
|
+
const stepWorkdir = this.resolveStepWorkdir(step);
|
|
49253
|
+
if (stepWorkdir) {
|
|
49254
|
+
return { ...def, cwd: stepWorkdir, workdir: void 0 };
|
|
49255
|
+
}
|
|
49256
|
+
}
|
|
49257
|
+
return def;
|
|
49258
|
+
};
|
|
49259
|
+
const effectiveSpecialist = applyStepWorkdir(specialistDef);
|
|
49260
|
+
const effectiveOwner = applyStepWorkdir(ownerDef);
|
|
49261
|
+
let specialistOutput;
|
|
49262
|
+
let ownerOutput;
|
|
49263
|
+
let ownerElapsed;
|
|
49264
|
+
if (usesDedicatedOwner) {
|
|
49265
|
+
const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
|
|
49266
|
+
specialistOutput = result.specialistOutput;
|
|
49267
|
+
ownerOutput = result.ownerOutput;
|
|
49268
|
+
ownerElapsed = result.ownerElapsed;
|
|
49269
|
+
} else {
|
|
49270
|
+
const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
|
|
49271
|
+
this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
|
|
49272
|
+
const resolvedStep = { ...step, task: ownerTask };
|
|
49273
|
+
const ownerStartTime = Date.now();
|
|
49274
|
+
const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
|
|
49275
|
+
ownerElapsed = Date.now() - ownerStartTime;
|
|
49276
|
+
this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
|
|
49277
|
+
if (usesOwnerFlow) {
|
|
49278
|
+
this.assertOwnerCompletionMarker(step, output, ownerTask);
|
|
49279
|
+
}
|
|
49280
|
+
specialistOutput = output;
|
|
49281
|
+
ownerOutput = output;
|
|
49282
|
+
}
|
|
49068
49283
|
if (step.verification) {
|
|
49069
|
-
this.runVerification(step.verification,
|
|
49284
|
+
this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
|
|
49285
|
+
}
|
|
49286
|
+
let combinedOutput = specialistOutput;
|
|
49287
|
+
if (usesOwnerFlow && reviewDef) {
|
|
49288
|
+
const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : void 0;
|
|
49289
|
+
const reviewOutput = await this.runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewDef, remainingMs);
|
|
49290
|
+
combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
|
|
49070
49291
|
}
|
|
49071
49292
|
state.row.status = "completed";
|
|
49072
|
-
state.row.output =
|
|
49293
|
+
state.row.output = combinedOutput;
|
|
49073
49294
|
state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
49074
49295
|
await this.db.updateStep(state.row.id, {
|
|
49075
49296
|
status: "completed",
|
|
49076
|
-
output,
|
|
49297
|
+
output: combinedOutput,
|
|
49077
49298
|
completedAt: state.row.completedAt,
|
|
49078
49299
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
49079
49300
|
});
|
|
49080
|
-
await this.persistStepOutput(runId, step.name,
|
|
49081
|
-
this.emit({ type: "step:completed", runId, stepName: step.name, output });
|
|
49082
|
-
await this.trajectory?.stepCompleted(step,
|
|
49301
|
+
await this.persistStepOutput(runId, step.name, combinedOutput);
|
|
49302
|
+
this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput });
|
|
49303
|
+
await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
|
|
49083
49304
|
return;
|
|
49084
49305
|
} catch (err) {
|
|
49085
49306
|
lastError = err instanceof Error ? err.message : String(err);
|
|
49307
|
+
const ownerTimedOut = usesDedicatedOwner ? /\bowner timed out\b/i.test(lastError) : /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
|
|
49308
|
+
if (ownerTimedOut) {
|
|
49309
|
+
this.emit({ type: "step:owner-timeout", runId, stepName: step.name, ownerName: ownerDef.name });
|
|
49310
|
+
}
|
|
49086
49311
|
}
|
|
49087
49312
|
}
|
|
49088
|
-
const nonInteractive =
|
|
49313
|
+
const nonInteractive = ownerDef.interactive === false || ["worker", "reviewer", "analyst"].includes(ownerDef.preset ?? "");
|
|
49089
49314
|
const verificationValue = typeof step.verification === "object" && "value" in step.verification ? String(step.verification.value) : void 0;
|
|
49090
49315
|
await this.trajectory?.stepFailed(step, lastError ?? "Unknown error", maxRetries + 1, maxRetries, {
|
|
49091
49316
|
agent: agentName,
|
|
@@ -49096,6 +49321,423 @@ ${trimmedOutput.slice(0, 200)}`);
|
|
|
49096
49321
|
await this.markStepFailed(state, lastError ?? "Unknown error", runId);
|
|
49097
49322
|
throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
|
|
49098
49323
|
}
|
|
49324
|
+
injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
|
|
49325
|
+
if (ownerDef.interactive === false)
|
|
49326
|
+
return resolvedTask;
|
|
49327
|
+
const specialistNote = ownerDef.name === specialistDef.name ? "" : `Specialist intended for this step: "${specialistDef.name}" (${specialistDef.role ?? specialistDef.cli}).`;
|
|
49328
|
+
return resolvedTask + `
|
|
49329
|
+
|
|
49330
|
+
---
|
|
49331
|
+
STEP OWNER CONTRACT:
|
|
49332
|
+
- You are the accountable owner for step "${step.name}".
|
|
49333
|
+
` + (specialistNote ? `- ${specialistNote}
|
|
49334
|
+
` : "") + `- If you delegate, you must still verify completion yourself.
|
|
49335
|
+
- Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}
|
|
49336
|
+
- Then self-terminate immediately with /exit.`;
|
|
49337
|
+
}
|
|
49338
|
+
buildOwnerSupervisorTask(step, originalTask, supervised, workerRuntimeName) {
|
|
49339
|
+
const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
|
|
49340
|
+
const channelLine = this.channel ? `#${this.channel}` : "(workflow channel unavailable)";
|
|
49341
|
+
return `You are the step owner/supervisor for step "${step.name}".
|
|
49342
|
+
|
|
49343
|
+
Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}
|
|
49344
|
+
Task: ${originalTask}
|
|
49345
|
+
|
|
49346
|
+
Your job: Monitor the worker and determine when the task is complete.
|
|
49347
|
+
|
|
49348
|
+
How to verify completion:
|
|
49349
|
+
- Watch ${channelLine} for the worker's progress messages and mirrored PTY output
|
|
49350
|
+
- Check file changes: run \`git diff --stat\` or inspect expected files directly
|
|
49351
|
+
- Ask the worker directly on ${channelLine} if you need a status update
|
|
49352
|
+
` + verificationGuide + `
|
|
49353
|
+
When you're satisfied the work is done correctly:
|
|
49354
|
+
Output exactly: STEP_COMPLETE:${step.name}`;
|
|
49355
|
+
}
|
|
49356
|
+
buildSupervisorVerificationGuide(verification) {
|
|
49357
|
+
if (!verification)
|
|
49358
|
+
return "";
|
|
49359
|
+
switch (verification.type) {
|
|
49360
|
+
case "output_contains":
|
|
49361
|
+
return `- Verification gate: confirm the worker output contains ${JSON.stringify(verification.value)}
|
|
49362
|
+
`;
|
|
49363
|
+
case "file_exists":
|
|
49364
|
+
return `- Verification gate: confirm the file exists at ${JSON.stringify(verification.value)}
|
|
49365
|
+
`;
|
|
49366
|
+
case "exit_code":
|
|
49367
|
+
return `- Verification gate: confirm the worker exits with code ${JSON.stringify(verification.value)}
|
|
49368
|
+
`;
|
|
49369
|
+
case "custom":
|
|
49370
|
+
return `- Verification gate: apply the custom verification rule ${JSON.stringify(verification.value)}
|
|
49371
|
+
`;
|
|
49372
|
+
default:
|
|
49373
|
+
return "";
|
|
49374
|
+
}
|
|
49375
|
+
}
|
|
49376
|
+
async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
|
|
49377
|
+
if (this.executor) {
|
|
49378
|
+
const supervisorTask2 = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
|
|
49379
|
+
const specialistStep2 = { ...step, task: resolvedTask };
|
|
49380
|
+
const ownerStep2 = {
|
|
49381
|
+
...step,
|
|
49382
|
+
name: `${step.name}-owner`,
|
|
49383
|
+
agent: supervised.owner.name,
|
|
49384
|
+
task: supervisorTask2
|
|
49385
|
+
};
|
|
49386
|
+
this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" and owner "${supervised.owner.name}"`);
|
|
49387
|
+
const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, resolvedTask, timeoutMs);
|
|
49388
|
+
const ownerStartTime2 = Date.now();
|
|
49389
|
+
const ownerOutput = await this.executor.executeAgentStep(ownerStep2, supervised.owner, supervisorTask2, timeoutMs);
|
|
49390
|
+
const ownerElapsed = Date.now() - ownerStartTime2;
|
|
49391
|
+
this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask2);
|
|
49392
|
+
const specialistOutput = await specialistPromise;
|
|
49393
|
+
return { specialistOutput, ownerOutput, ownerElapsed };
|
|
49394
|
+
}
|
|
49395
|
+
let workerHandle;
|
|
49396
|
+
let workerRuntimeName = supervised.specialist.name;
|
|
49397
|
+
let workerSpawned = false;
|
|
49398
|
+
let workerReleased = false;
|
|
49399
|
+
let resolveWorkerSpawn;
|
|
49400
|
+
let rejectWorkerSpawn;
|
|
49401
|
+
const workerReady = new Promise((resolve3, reject) => {
|
|
49402
|
+
resolveWorkerSpawn = resolve3;
|
|
49403
|
+
rejectWorkerSpawn = reject;
|
|
49404
|
+
});
|
|
49405
|
+
const specialistStep = { ...step, task: resolvedTask };
|
|
49406
|
+
this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
|
|
49407
|
+
const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
|
|
49408
|
+
agentNameSuffix: "worker",
|
|
49409
|
+
onSpawned: ({ actualName, agent }) => {
|
|
49410
|
+
workerHandle = agent;
|
|
49411
|
+
workerRuntimeName = actualName;
|
|
49412
|
+
this.supervisedRuntimeAgents.set(actualName, {
|
|
49413
|
+
stepName: step.name,
|
|
49414
|
+
role: "specialist",
|
|
49415
|
+
logicalName: supervised.specialist.name
|
|
49416
|
+
});
|
|
49417
|
+
if (!workerSpawned) {
|
|
49418
|
+
workerSpawned = true;
|
|
49419
|
+
resolveWorkerSpawn();
|
|
49420
|
+
}
|
|
49421
|
+
},
|
|
49422
|
+
onChunk: ({ agentName, chunk }) => {
|
|
49423
|
+
this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk);
|
|
49424
|
+
}
|
|
49425
|
+
}).catch((error95) => {
|
|
49426
|
+
if (!workerSpawned) {
|
|
49427
|
+
workerSpawned = true;
|
|
49428
|
+
rejectWorkerSpawn(error95);
|
|
49429
|
+
}
|
|
49430
|
+
throw error95;
|
|
49431
|
+
});
|
|
49432
|
+
const workerSettled = workerPromise.catch(() => void 0);
|
|
49433
|
+
workerPromise.then((output) => {
|
|
49434
|
+
workerReleased = true;
|
|
49435
|
+
this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
|
|
49436
|
+
if (step.verification?.type === "output_contains" && output.includes(step.verification.value)) {
|
|
49437
|
+
this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
|
|
49438
|
+
}
|
|
49439
|
+
}).catch((error95) => {
|
|
49440
|
+
const message = error95 instanceof Error ? error95.message : String(error95);
|
|
49441
|
+
this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`);
|
|
49442
|
+
});
|
|
49443
|
+
await workerReady;
|
|
49444
|
+
const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
|
|
49445
|
+
const ownerStep = {
|
|
49446
|
+
...step,
|
|
49447
|
+
name: `${step.name}-owner`,
|
|
49448
|
+
agent: supervised.owner.name,
|
|
49449
|
+
task: supervisorTask
|
|
49450
|
+
};
|
|
49451
|
+
this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
|
|
49452
|
+
const ownerStartTime = Date.now();
|
|
49453
|
+
try {
|
|
49454
|
+
const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
|
|
49455
|
+
agentNameSuffix: "owner",
|
|
49456
|
+
onSpawned: ({ actualName }) => {
|
|
49457
|
+
this.supervisedRuntimeAgents.set(actualName, {
|
|
49458
|
+
stepName: step.name,
|
|
49459
|
+
role: "owner",
|
|
49460
|
+
logicalName: supervised.owner.name
|
|
49461
|
+
});
|
|
49462
|
+
},
|
|
49463
|
+
onChunk: ({ chunk }) => {
|
|
49464
|
+
void this.recordOwnerMonitoringChunk(step, supervised.owner, chunk);
|
|
49465
|
+
}
|
|
49466
|
+
});
|
|
49467
|
+
const ownerElapsed = Date.now() - ownerStartTime;
|
|
49468
|
+
this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
|
|
49469
|
+
this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
|
|
49470
|
+
const specialistOutput = await workerPromise;
|
|
49471
|
+
return { specialistOutput, ownerOutput, ownerElapsed };
|
|
49472
|
+
} catch (error95) {
|
|
49473
|
+
const message = error95 instanceof Error ? error95.message : String(error95);
|
|
49474
|
+
if (!workerReleased && workerHandle) {
|
|
49475
|
+
await workerHandle.release().catch(() => void 0);
|
|
49476
|
+
}
|
|
49477
|
+
await workerSettled;
|
|
49478
|
+
if (/\btimed out\b/i.test(message)) {
|
|
49479
|
+
throw new Error(`Step "${step.name}" owner timed out after ${timeoutMs ?? "unknown"}ms`);
|
|
49480
|
+
}
|
|
49481
|
+
throw error95;
|
|
49482
|
+
}
|
|
49483
|
+
}
|
|
49484
|
+
forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk) {
|
|
49485
|
+
const lines = _WorkflowRunner.stripAnsi(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
|
|
49486
|
+
for (const line of lines) {
|
|
49487
|
+
this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
|
|
49488
|
+
}
|
|
49489
|
+
}
|
|
49490
|
+
async recordOwnerMonitoringChunk(step, ownerDef, chunk) {
|
|
49491
|
+
const stripped = _WorkflowRunner.stripAnsi(chunk);
|
|
49492
|
+
const details = [];
|
|
49493
|
+
if (/git diff --stat/i.test(stripped))
|
|
49494
|
+
details.push("Checked git diff stats");
|
|
49495
|
+
if (/\bls -la\b/i.test(stripped))
|
|
49496
|
+
details.push("Listed files for verification");
|
|
49497
|
+
if (/status update\?/i.test(stripped))
|
|
49498
|
+
details.push("Asked the worker for a status update");
|
|
49499
|
+
if (/STEP_COMPLETE:/i.test(stripped))
|
|
49500
|
+
details.push("Declared the step complete");
|
|
49501
|
+
for (const detail of details) {
|
|
49502
|
+
await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
|
|
49503
|
+
output: stripped.slice(0, 240)
|
|
49504
|
+
});
|
|
49505
|
+
}
|
|
49506
|
+
}
|
|
49507
|
+
resolveAutoStepOwner(specialistDef, agentMap) {
|
|
49508
|
+
if (specialistDef.interactive === false)
|
|
49509
|
+
return specialistDef;
|
|
49510
|
+
const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
|
|
49511
|
+
const candidates = allDefs.filter((d) => d.interactive !== false);
|
|
49512
|
+
const matchesHubRole = (text) => [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`, "i").test(text));
|
|
49513
|
+
const ownerish = (def) => {
|
|
49514
|
+
const nameLC = def.name.toLowerCase();
|
|
49515
|
+
const roleLC = def.role?.toLowerCase() ?? "";
|
|
49516
|
+
return matchesHubRole(nameLC) || matchesHubRole(roleLC);
|
|
49517
|
+
};
|
|
49518
|
+
const ownerPriority = (def) => {
|
|
49519
|
+
const roleLC = def.role?.toLowerCase() ?? "";
|
|
49520
|
+
const nameLC = def.name.toLowerCase();
|
|
49521
|
+
if (/\blead\b/.test(roleLC) || /\blead\b/.test(nameLC))
|
|
49522
|
+
return 6;
|
|
49523
|
+
if (/\bcoordinator\b/.test(roleLC) || /\bcoordinator\b/.test(nameLC))
|
|
49524
|
+
return 5;
|
|
49525
|
+
if (/\bsupervisor\b/.test(roleLC) || /\bsupervisor\b/.test(nameLC))
|
|
49526
|
+
return 4;
|
|
49527
|
+
if (/\borchestrator\b/.test(roleLC) || /\borchestrator\b/.test(nameLC))
|
|
49528
|
+
return 3;
|
|
49529
|
+
if (/\bhub\b/.test(roleLC) || /\bhub\b/.test(nameLC))
|
|
49530
|
+
return 2;
|
|
49531
|
+
return ownerish(def) ? 1 : 0;
|
|
49532
|
+
};
|
|
49533
|
+
const dedicatedOwner = candidates.filter((d) => d.name !== specialistDef.name && ownerish(d)).sort((a, b) => ownerPriority(b) - ownerPriority(a) || a.name.localeCompare(b.name))[0];
|
|
49534
|
+
if (dedicatedOwner)
|
|
49535
|
+
return dedicatedOwner;
|
|
49536
|
+
return specialistDef;
|
|
49537
|
+
}
|
|
49538
|
+
resolveAutoReviewAgent(ownerDef, agentMap) {
|
|
49539
|
+
const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
|
|
49540
|
+
const isReviewer = (def) => {
|
|
49541
|
+
const roleLC = def.role?.toLowerCase() ?? "";
|
|
49542
|
+
const nameLC = def.name.toLowerCase();
|
|
49543
|
+
return def.preset === "reviewer" || roleLC.includes("review") || roleLC.includes("critic") || roleLC.includes("verifier") || roleLC.includes("qa") || nameLC.includes("review");
|
|
49544
|
+
};
|
|
49545
|
+
const reviewerPriority = (def) => {
|
|
49546
|
+
if (def.preset === "reviewer")
|
|
49547
|
+
return 5;
|
|
49548
|
+
const roleLC = def.role?.toLowerCase() ?? "";
|
|
49549
|
+
const nameLC = def.name.toLowerCase();
|
|
49550
|
+
if (roleLC.includes("review") || nameLC.includes("review"))
|
|
49551
|
+
return 4;
|
|
49552
|
+
if (roleLC.includes("verifier") || roleLC.includes("qa"))
|
|
49553
|
+
return 3;
|
|
49554
|
+
if (roleLC.includes("critic"))
|
|
49555
|
+
return 2;
|
|
49556
|
+
return isReviewer(def) ? 1 : 0;
|
|
49557
|
+
};
|
|
49558
|
+
const dedicated = allDefs.filter((d) => d.name !== ownerDef.name && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
|
|
49559
|
+
if (dedicated)
|
|
49560
|
+
return dedicated;
|
|
49561
|
+
const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
|
|
49562
|
+
if (alternate)
|
|
49563
|
+
return alternate;
|
|
49564
|
+
return ownerDef;
|
|
49565
|
+
}
|
|
49566
|
+
assertOwnerCompletionMarker(step, output, injectedTaskText) {
|
|
49567
|
+
const marker = `STEP_COMPLETE:${step.name}`;
|
|
49568
|
+
const taskHasMarker = injectedTaskText.includes(marker);
|
|
49569
|
+
const first = output.indexOf(marker);
|
|
49570
|
+
if (first === -1) {
|
|
49571
|
+
throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
|
|
49572
|
+
}
|
|
49573
|
+
const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Output exactly: STEP_COMPLETE:");
|
|
49574
|
+
if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
|
|
49575
|
+
const hasSecond = output.includes(marker, first + marker.length);
|
|
49576
|
+
if (!hasSecond) {
|
|
49577
|
+
throw new Error(`Step "${step.name}" owner completion marker missing in agent response: "${marker}"`);
|
|
49578
|
+
}
|
|
49579
|
+
}
|
|
49580
|
+
}
|
|
49581
|
+
async runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewerDef, timeoutMs) {
|
|
49582
|
+
const reviewSnippetMax = 12e3;
|
|
49583
|
+
let specialistSnippet = specialistOutput;
|
|
49584
|
+
if (specialistOutput.length > reviewSnippetMax) {
|
|
49585
|
+
const head = Math.floor(reviewSnippetMax / 2);
|
|
49586
|
+
const tail = reviewSnippetMax - head;
|
|
49587
|
+
const omitted = specialistOutput.length - head - tail;
|
|
49588
|
+
specialistSnippet = `${specialistOutput.slice(0, head)}
|
|
49589
|
+
...[truncated ${omitted} chars for review]...
|
|
49590
|
+
${specialistOutput.slice(specialistOutput.length - tail)}`;
|
|
49591
|
+
}
|
|
49592
|
+
let ownerSnippet = ownerOutput;
|
|
49593
|
+
if (ownerOutput.length > reviewSnippetMax) {
|
|
49594
|
+
const head = Math.floor(reviewSnippetMax / 2);
|
|
49595
|
+
const tail = reviewSnippetMax - head;
|
|
49596
|
+
const omitted = ownerOutput.length - head - tail;
|
|
49597
|
+
ownerSnippet = `${ownerOutput.slice(0, head)}
|
|
49598
|
+
...[truncated ${omitted} chars for review]...
|
|
49599
|
+
${ownerOutput.slice(ownerOutput.length - tail)}`;
|
|
49600
|
+
}
|
|
49601
|
+
const reviewTask = `Review workflow step "${step.name}" for completion and safe handoff.
|
|
49602
|
+
Step owner: ${ownerDef.name}
|
|
49603
|
+
Original objective:
|
|
49604
|
+
${resolvedTask}
|
|
49605
|
+
|
|
49606
|
+
Specialist output:
|
|
49607
|
+
${specialistSnippet}
|
|
49608
|
+
|
|
49609
|
+
Owner verification notes:
|
|
49610
|
+
${ownerSnippet}
|
|
49611
|
+
|
|
49612
|
+
Return exactly:
|
|
49613
|
+
REVIEW_DECISION: APPROVE or REJECT
|
|
49614
|
+
REVIEW_REASON: <one sentence>
|
|
49615
|
+
Then output /exit.`;
|
|
49616
|
+
const safetyTimeoutMs = timeoutMs ?? 6e5;
|
|
49617
|
+
const reviewStep = {
|
|
49618
|
+
name: `${step.name}-review`,
|
|
49619
|
+
type: "agent",
|
|
49620
|
+
agent: reviewerDef.name,
|
|
49621
|
+
task: reviewTask
|
|
49622
|
+
};
|
|
49623
|
+
await this.trajectory?.registerAgent(reviewerDef.name, "reviewer");
|
|
49624
|
+
this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
|
|
49625
|
+
const emitReviewCompleted = async (decision, reason) => {
|
|
49626
|
+
await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
|
|
49627
|
+
this.emit({
|
|
49628
|
+
type: "step:review-completed",
|
|
49629
|
+
runId: this.currentRunId ?? "",
|
|
49630
|
+
stepName: step.name,
|
|
49631
|
+
reviewerName: reviewerDef.name,
|
|
49632
|
+
decision
|
|
49633
|
+
});
|
|
49634
|
+
};
|
|
49635
|
+
if (this.executor) {
|
|
49636
|
+
const reviewOutput2 = await this.executor.executeAgentStep(reviewStep, reviewerDef, reviewTask, safetyTimeoutMs);
|
|
49637
|
+
const parsed = this.parseReviewDecision(reviewOutput2);
|
|
49638
|
+
if (!parsed) {
|
|
49639
|
+
throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
|
|
49640
|
+
}
|
|
49641
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
49642
|
+
if (parsed.decision === "rejected") {
|
|
49643
|
+
throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
|
|
49644
|
+
}
|
|
49645
|
+
this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
|
|
49646
|
+
return reviewOutput2;
|
|
49647
|
+
}
|
|
49648
|
+
let reviewerHandle;
|
|
49649
|
+
let reviewerReleased = false;
|
|
49650
|
+
let reviewOutput = "";
|
|
49651
|
+
let completedReview;
|
|
49652
|
+
let reviewCompletionPromise;
|
|
49653
|
+
const reviewCompletionStarted = { value: false };
|
|
49654
|
+
const startReviewCompletion = (parsed) => {
|
|
49655
|
+
if (reviewCompletionStarted.value)
|
|
49656
|
+
return;
|
|
49657
|
+
reviewCompletionStarted.value = true;
|
|
49658
|
+
completedReview = parsed;
|
|
49659
|
+
reviewCompletionPromise = (async () => {
|
|
49660
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
49661
|
+
if (reviewerHandle && !reviewerReleased) {
|
|
49662
|
+
reviewerReleased = true;
|
|
49663
|
+
await reviewerHandle.release().catch(() => void 0);
|
|
49664
|
+
}
|
|
49665
|
+
})();
|
|
49666
|
+
};
|
|
49667
|
+
try {
|
|
49668
|
+
reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
|
|
49669
|
+
onSpawned: ({ agent }) => {
|
|
49670
|
+
reviewerHandle = agent;
|
|
49671
|
+
},
|
|
49672
|
+
onChunk: ({ chunk }) => {
|
|
49673
|
+
const nextOutput = reviewOutput + _WorkflowRunner.stripAnsi(chunk);
|
|
49674
|
+
reviewOutput = nextOutput;
|
|
49675
|
+
const parsed = this.parseReviewDecision(nextOutput);
|
|
49676
|
+
if (parsed) {
|
|
49677
|
+
startReviewCompletion(parsed);
|
|
49678
|
+
}
|
|
49679
|
+
}
|
|
49680
|
+
});
|
|
49681
|
+
await reviewCompletionPromise;
|
|
49682
|
+
} catch (error95) {
|
|
49683
|
+
const message = error95 instanceof Error ? error95.message : String(error95);
|
|
49684
|
+
if (/\btimed out\b/i.test(message)) {
|
|
49685
|
+
this.log(`[${step.name}] Review safety backstop timeout fired after ${safetyTimeoutMs}ms`);
|
|
49686
|
+
throw new Error(`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`);
|
|
49687
|
+
}
|
|
49688
|
+
throw error95;
|
|
49689
|
+
}
|
|
49690
|
+
if (!completedReview) {
|
|
49691
|
+
const parsed = this.parseReviewDecision(reviewOutput);
|
|
49692
|
+
if (!parsed) {
|
|
49693
|
+
throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
|
|
49694
|
+
}
|
|
49695
|
+
completedReview = parsed;
|
|
49696
|
+
await emitReviewCompleted(parsed.decision, parsed.reason);
|
|
49697
|
+
}
|
|
49698
|
+
if (completedReview.decision === "rejected") {
|
|
49699
|
+
throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
|
|
49700
|
+
}
|
|
49701
|
+
this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
|
|
49702
|
+
return reviewOutput;
|
|
49703
|
+
}
|
|
49704
|
+
parseReviewDecision(reviewOutput) {
|
|
49705
|
+
const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
|
|
49706
|
+
const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
|
|
49707
|
+
if (decisionMatches.length === 0) {
|
|
49708
|
+
return null;
|
|
49709
|
+
}
|
|
49710
|
+
const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
|
|
49711
|
+
const decisionMatch = outputLikelyContainsEchoedPrompt && decisionMatches.length > 1 ? decisionMatches[decisionMatches.length - 1] : decisionMatches[0];
|
|
49712
|
+
const decision = decisionMatch?.[1]?.toUpperCase();
|
|
49713
|
+
if (decision !== "APPROVE" && decision !== "REJECT") {
|
|
49714
|
+
return null;
|
|
49715
|
+
}
|
|
49716
|
+
const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
|
|
49717
|
+
const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
|
|
49718
|
+
const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
|
|
49719
|
+
const reason = reasonMatch?.[1]?.trim();
|
|
49720
|
+
return {
|
|
49721
|
+
decision: decision === "APPROVE" ? "approved" : "rejected",
|
|
49722
|
+
reason: reason && reason !== "<one sentence>" ? reason : void 0
|
|
49723
|
+
};
|
|
49724
|
+
}
|
|
49725
|
+
combineStepAndReviewOutput(stepOutput, reviewOutput) {
|
|
49726
|
+
const primary = stepOutput.trimEnd();
|
|
49727
|
+
const review = reviewOutput.trim();
|
|
49728
|
+
if (!review)
|
|
49729
|
+
return primary;
|
|
49730
|
+
if (!primary)
|
|
49731
|
+
return `REVIEW_OUTPUT
|
|
49732
|
+
${review}
|
|
49733
|
+
`;
|
|
49734
|
+
return `${primary}
|
|
49735
|
+
|
|
49736
|
+
---
|
|
49737
|
+
REVIEW_OUTPUT
|
|
49738
|
+
${review}
|
|
49739
|
+
`;
|
|
49740
|
+
}
|
|
49099
49741
|
/**
|
|
49100
49742
|
* Build the CLI command and arguments for a non-interactive agent execution.
|
|
49101
49743
|
* Each CLI has a specific flag for one-shot prompt mode.
|
|
@@ -49206,7 +49848,7 @@ DO NOT:
|
|
|
49206
49848
|
const output = await new Promise((resolve3, reject) => {
|
|
49207
49849
|
const child = (0, import_node_child_process3.spawn)(cmd, args, {
|
|
49208
49850
|
stdio: ["ignore", "pipe", "pipe"],
|
|
49209
|
-
cwd:
|
|
49851
|
+
cwd: this.resolveAgentCwd(agentDef),
|
|
49210
49852
|
env: this.getRelayEnv() ?? { ...process.env }
|
|
49211
49853
|
});
|
|
49212
49854
|
this.registerWorker(agentName, agentDef.cli, step.task ?? "", child.pid, false);
|
|
@@ -49289,17 +49931,18 @@ DO NOT:
|
|
|
49289
49931
|
this.unregisterWorker(agentName);
|
|
49290
49932
|
}
|
|
49291
49933
|
}
|
|
49292
|
-
async spawnAndWait(agentDef, step, timeoutMs) {
|
|
49934
|
+
async spawnAndWait(agentDef, step, timeoutMs, options = {}) {
|
|
49293
49935
|
if (agentDef.interactive === false) {
|
|
49294
49936
|
return this.execNonInteractive(agentDef, step, timeoutMs);
|
|
49295
49937
|
}
|
|
49296
49938
|
if (!this.relay) {
|
|
49297
49939
|
throw new Error("AgentRelay not initialized");
|
|
49298
49940
|
}
|
|
49299
|
-
|
|
49941
|
+
const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
|
|
49942
|
+
let agentName = requestedName;
|
|
49300
49943
|
const role = agentDef.role?.toLowerCase() ?? "";
|
|
49301
49944
|
const nameLC = agentDef.name.toLowerCase();
|
|
49302
|
-
const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) =>
|
|
49945
|
+
const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
|
|
49303
49946
|
const pattern = this.currentConfig?.swarm.pattern;
|
|
49304
49947
|
const isHubPattern = pattern && _WorkflowRunner.HUB_PATTERNS.has(pattern);
|
|
49305
49948
|
const delegationGuidance = isHub || !isHubPattern ? this.buildDelegationGuidance(agentDef.cli, timeoutMs) : "";
|
|
@@ -49313,6 +49956,7 @@ DO NOT:
|
|
|
49313
49956
|
const stripped = _WorkflowRunner.stripAnsi(chunk);
|
|
49314
49957
|
this.ptyOutputBuffers.get(agentName)?.push(stripped);
|
|
49315
49958
|
logStream.write(chunk);
|
|
49959
|
+
options.onChunk?.({ agentName, chunk });
|
|
49316
49960
|
});
|
|
49317
49961
|
const agentChannels = this.channel ? [this.channel] : agentDef.channels;
|
|
49318
49962
|
let agent;
|
|
@@ -49320,6 +49964,7 @@ DO NOT:
|
|
|
49320
49964
|
let stopHeartbeat;
|
|
49321
49965
|
let ptyChunks = [];
|
|
49322
49966
|
try {
|
|
49967
|
+
const agentCwd = this.resolveAgentCwd(agentDef);
|
|
49323
49968
|
agent = await this.relay.spawnPty({
|
|
49324
49969
|
name: agentName,
|
|
49325
49970
|
cli: agentDef.cli,
|
|
@@ -49328,7 +49973,7 @@ DO NOT:
|
|
|
49328
49973
|
channels: agentChannels,
|
|
49329
49974
|
task: taskWithExit,
|
|
49330
49975
|
idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
|
|
49331
|
-
cwd:
|
|
49976
|
+
cwd: agentCwd !== this.cwd ? agentCwd : void 0
|
|
49332
49977
|
});
|
|
49333
49978
|
if (agent.name !== agentName) {
|
|
49334
49979
|
const oldName = agentName;
|
|
@@ -49354,10 +49999,12 @@ DO NOT:
|
|
|
49354
49999
|
const stripped = _WorkflowRunner.stripAnsi(chunk);
|
|
49355
50000
|
this.ptyOutputBuffers.get(agent.name)?.push(stripped);
|
|
49356
50001
|
newLogStream.write(chunk);
|
|
50002
|
+
options.onChunk?.({ agentName: agent.name, chunk });
|
|
49357
50003
|
});
|
|
49358
50004
|
}
|
|
49359
50005
|
agentName = agent.name;
|
|
49360
50006
|
}
|
|
50007
|
+
await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
|
|
49361
50008
|
let workerPid;
|
|
49362
50009
|
try {
|
|
49363
50010
|
const rawAgents = await this.relay.listAgentsRaw();
|
|
@@ -49410,6 +50057,7 @@ DO NOT:
|
|
|
49410
50057
|
this.ptyLogStreams.delete(agentName);
|
|
49411
50058
|
}
|
|
49412
50059
|
this.unregisterWorker(agentName);
|
|
50060
|
+
this.supervisedRuntimeAgents.delete(agentName);
|
|
49413
50061
|
}
|
|
49414
50062
|
let output;
|
|
49415
50063
|
if (ptyChunks.length > 0) {
|
|
@@ -49535,7 +50183,7 @@ DO NOT:
|
|
|
49535
50183
|
continue;
|
|
49536
50184
|
const role = agentDef.role?.toLowerCase() ?? "";
|
|
49537
50185
|
const nameLC = agentDef.name.toLowerCase();
|
|
49538
|
-
if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) =>
|
|
50186
|
+
if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role))) {
|
|
49539
50187
|
const handle = this.activeAgentHandles.get(agentDef.name);
|
|
49540
50188
|
if (handle)
|
|
49541
50189
|
return handle;
|