agent-relay 3.1.16 → 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.
Files changed (39) hide show
  1. package/bin/agent-relay-broker-linux-arm64 +0 -0
  2. package/dist/index.cjs +565 -32
  3. package/package.json +8 -8
  4. package/packages/acp-bridge/package.json +2 -2
  5. package/packages/config/package.json +1 -1
  6. package/packages/hooks/package.json +4 -4
  7. package/packages/memory/package.json +2 -2
  8. package/packages/openclaw/package.json +2 -2
  9. package/packages/policy/package.json +2 -2
  10. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +16 -0
  11. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +1 -0
  12. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +640 -0
  13. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -0
  14. package/packages/sdk/dist/workflows/cli.js +10 -0
  15. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  16. package/packages/sdk/dist/workflows/runner.d.ts +31 -0
  17. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  18. package/packages/sdk/dist/workflows/runner.js +534 -31
  19. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  20. package/packages/sdk/dist/workflows/trajectory.d.ts +22 -1
  21. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  22. package/packages/sdk/dist/workflows/trajectory.js +55 -8
  23. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  24. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  25. package/packages/sdk/dist/workflows/validator.js +29 -0
  26. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  27. package/packages/sdk/package.json +2 -2
  28. package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +778 -0
  29. package/packages/sdk/src/__tests__/workflow-runner.test.ts +484 -9
  30. package/packages/sdk/src/workflows/README.md +11 -0
  31. package/packages/sdk/src/workflows/cli.ts +10 -0
  32. package/packages/sdk/src/workflows/runner.ts +706 -33
  33. package/packages/sdk/src/workflows/trajectory.ts +89 -8
  34. package/packages/sdk/src/workflows/validator.ts +29 -0
  35. package/packages/sdk-py/pyproject.toml +1 -1
  36. package/packages/telemetry/package.json +1 -1
  37. package/packages/trajectory/package.json +2 -2
  38. package/packages/user-directory/package.json +2 -2
  39. package/packages/utils/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -47272,18 +47272,66 @@ var WorkflowTrajectory = class {
47272
47272
  }
47273
47273
  // ── Step events ────────────────────────────────────────────────────────────
47274
47274
  /** Record step started — captures intent, not just assignment. */
47275
- async stepStarted(step, agent) {
47275
+ async stepStarted(step, agent, participants) {
47276
47276
  if (!this.enabled || !this.trajectory)
47277
47277
  return;
47278
- if (!this.trajectory.agents.some((a) => a.name === agent)) {
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) {
47293
+ if (!this.enabled || !this.trajectory)
47294
+ return;
47295
+ if (!this.trajectory.agents.some((a) => a.name === name)) {
47279
47296
  this.trajectory.agents.push({
47280
- name: agent,
47281
- role: step.agent ?? "deterministic",
47297
+ name,
47298
+ role,
47282
47299
  joinedAt: (/* @__PURE__ */ new Date()).toISOString()
47283
47300
  });
47301
+ await this.flush();
47284
47302
  }
47285
- const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47286
- this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
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
+ });
47287
47335
  await this.flush();
47288
47336
  }
47289
47337
  /** Record step completed — captures what was accomplished. */
@@ -47613,6 +47661,8 @@ var WorkflowRunner = class _WorkflowRunner {
47613
47661
  lastIdleLog = /* @__PURE__ */ new Map();
47614
47662
  /** Tracks last logged activity type per agent to avoid duplicate status lines. */
47615
47663
  lastActivity = /* @__PURE__ */ new Map();
47664
+ /** Runtime-name lookup for agents participating in supervised owner flows. */
47665
+ supervisedRuntimeAgents = /* @__PURE__ */ new Map();
47616
47666
  /** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
47617
47667
  resolvedPaths = /* @__PURE__ */ new Map();
47618
47668
  constructor(options = {}) {
@@ -48523,6 +48573,10 @@ ${err.suggestion}`);
48523
48573
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
48524
48574
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
48525
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
+ }
48526
48580
  };
48527
48581
  this.relay.onAgentSpawned = (agent) => {
48528
48582
  if (!this.activeAgentHandles.has(agent.name)) {
@@ -48625,6 +48679,7 @@ ${err.suggestion}`);
48625
48679
  }
48626
48680
  this.lastIdleLog.clear();
48627
48681
  this.lastActivity.clear();
48682
+ this.supervisedRuntimeAgents.clear();
48628
48683
  this.log("Shutting down broker...");
48629
48684
  await this.relay?.shutdown();
48630
48685
  this.relay = void 0;
@@ -49131,10 +49186,19 @@ ${trimmedOutput.slice(0, 200)}`);
49131
49186
  if (!rawAgentDef) {
49132
49187
  throw new Error(`Agent "${agentName}" not found in config`);
49133
49188
  }
49134
- const agentDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
49135
- const maxRetries = step.retries ?? agentDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
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;
49136
49200
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49137
- const timeoutMs = step.timeoutMs ?? agentDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49201
+ const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49138
49202
  let lastError;
49139
49203
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49140
49204
  this.checkAborted();
@@ -49158,48 +49222,95 @@ ${trimmedOutput.slice(0, 200)}`);
49158
49222
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49159
49223
  });
49160
49224
  this.emit({ type: "step:started", runId, stepName: step.name });
49161
- this.postToChannel(`**[${step.name}]** Started (agent: ${agentDef.name})`);
49162
- await this.trajectory?.stepStarted(step, agentDef.name);
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
+ });
49163
49242
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49164
49243
  let resolvedTask = this.interpolateStepTask(step.task ?? "", stepOutputContext);
49165
- if (agentDef.interactive !== false) {
49244
+ if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
49166
49245
  const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
49167
49246
  if (nonInteractiveInfo) {
49168
49247
  resolvedTask += nonInteractiveInfo;
49169
49248
  }
49170
49249
  }
49171
- let effectiveAgentDef = agentDef;
49172
- if (step.workdir) {
49173
- const stepWorkdir = this.resolveStepWorkdir(step);
49174
- if (stepWorkdir) {
49175
- effectiveAgentDef = { ...agentDef, cwd: stepWorkdir, workdir: void 0 };
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);
49176
49279
  }
49280
+ specialistOutput = output;
49281
+ ownerOutput = output;
49177
49282
  }
49178
- this.log(`[${step.name}] Spawning agent "${effectiveAgentDef.name}" (cli: ${effectiveAgentDef.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
49179
- const resolvedStep = { ...step, task: resolvedTask };
49180
- const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveAgentDef, resolvedTask, timeoutMs) : await this.spawnAndWait(effectiveAgentDef, resolvedStep, timeoutMs);
49181
- this.log(`[${step.name}] Agent "${agentDef.name}" exited`);
49182
49283
  if (step.verification) {
49183
- this.runVerification(step.verification, output, step.name, resolvedTask);
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);
49184
49291
  }
49185
49292
  state.row.status = "completed";
49186
- state.row.output = output;
49293
+ state.row.output = combinedOutput;
49187
49294
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49188
49295
  await this.db.updateStep(state.row.id, {
49189
49296
  status: "completed",
49190
- output,
49297
+ output: combinedOutput,
49191
49298
  completedAt: state.row.completedAt,
49192
49299
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49193
49300
  });
49194
- await this.persistStepOutput(runId, step.name, output);
49195
- this.emit({ type: "step:completed", runId, stepName: step.name, output });
49196
- await this.trajectory?.stepCompleted(step, output, attempt + 1);
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);
49197
49304
  return;
49198
49305
  } catch (err) {
49199
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
+ }
49200
49311
  }
49201
49312
  }
49202
- const nonInteractive = agentDef.interactive === false || ["worker", "reviewer", "analyst"].includes(agentDef.preset ?? "");
49313
+ const nonInteractive = ownerDef.interactive === false || ["worker", "reviewer", "analyst"].includes(ownerDef.preset ?? "");
49203
49314
  const verificationValue = typeof step.verification === "object" && "value" in step.verification ? String(step.verification.value) : void 0;
49204
49315
  await this.trajectory?.stepFailed(step, lastError ?? "Unknown error", maxRetries + 1, maxRetries, {
49205
49316
  agent: agentName,
@@ -49210,6 +49321,423 @@ ${trimmedOutput.slice(0, 200)}`);
49210
49321
  await this.markStepFailed(state, lastError ?? "Unknown error", runId);
49211
49322
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
49212
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
+ }
49213
49741
  /**
49214
49742
  * Build the CLI command and arguments for a non-interactive agent execution.
49215
49743
  * Each CLI has a specific flag for one-shot prompt mode.
@@ -49403,17 +49931,18 @@ DO NOT:
49403
49931
  this.unregisterWorker(agentName);
49404
49932
  }
49405
49933
  }
49406
- async spawnAndWait(agentDef, step, timeoutMs) {
49934
+ async spawnAndWait(agentDef, step, timeoutMs, options = {}) {
49407
49935
  if (agentDef.interactive === false) {
49408
49936
  return this.execNonInteractive(agentDef, step, timeoutMs);
49409
49937
  }
49410
49938
  if (!this.relay) {
49411
49939
  throw new Error("AgentRelay not initialized");
49412
49940
  }
49413
- let agentName = `${step.name}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49941
+ const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49942
+ let agentName = requestedName;
49414
49943
  const role = agentDef.role?.toLowerCase() ?? "";
49415
49944
  const nameLC = agentDef.name.toLowerCase();
49416
- const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r));
49945
+ const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
49417
49946
  const pattern = this.currentConfig?.swarm.pattern;
49418
49947
  const isHubPattern = pattern && _WorkflowRunner.HUB_PATTERNS.has(pattern);
49419
49948
  const delegationGuidance = isHub || !isHubPattern ? this.buildDelegationGuidance(agentDef.cli, timeoutMs) : "";
@@ -49427,6 +49956,7 @@ DO NOT:
49427
49956
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49428
49957
  this.ptyOutputBuffers.get(agentName)?.push(stripped);
49429
49958
  logStream.write(chunk);
49959
+ options.onChunk?.({ agentName, chunk });
49430
49960
  });
49431
49961
  const agentChannels = this.channel ? [this.channel] : agentDef.channels;
49432
49962
  let agent;
@@ -49469,10 +49999,12 @@ DO NOT:
49469
49999
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49470
50000
  this.ptyOutputBuffers.get(agent.name)?.push(stripped);
49471
50001
  newLogStream.write(chunk);
50002
+ options.onChunk?.({ agentName: agent.name, chunk });
49472
50003
  });
49473
50004
  }
49474
50005
  agentName = agent.name;
49475
50006
  }
50007
+ await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
49476
50008
  let workerPid;
49477
50009
  try {
49478
50010
  const rawAgents = await this.relay.listAgentsRaw();
@@ -49525,6 +50057,7 @@ DO NOT:
49525
50057
  this.ptyLogStreams.delete(agentName);
49526
50058
  }
49527
50059
  this.unregisterWorker(agentName);
50060
+ this.supervisedRuntimeAgents.delete(agentName);
49528
50061
  }
49529
50062
  let output;
49530
50063
  if (ptyChunks.length > 0) {
@@ -49650,7 +50183,7 @@ DO NOT:
49650
50183
  continue;
49651
50184
  const role = agentDef.role?.toLowerCase() ?? "";
49652
50185
  const nameLC = agentDef.name.toLowerCase();
49653
- if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r))) {
50186
+ if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role))) {
49654
50187
  const handle = this.activeAgentHandles.get(agentDef.name);
49655
50188
  if (handle)
49656
50189
  return handle;