agent-relay 3.1.16 → 3.1.18

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 (51) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +573 -32
  6. package/package.json +8 -8
  7. package/packages/acp-bridge/package.json +2 -2
  8. package/packages/config/package.json +1 -1
  9. package/packages/hooks/package.json +4 -4
  10. package/packages/memory/package.json +2 -2
  11. package/packages/openclaw/package.json +2 -2
  12. package/packages/policy/package.json +2 -2
  13. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +16 -0
  14. package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +1 -0
  15. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +640 -0
  16. package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +1 -0
  17. package/packages/sdk/dist/client.d.ts +2 -0
  18. package/packages/sdk/dist/client.d.ts.map +1 -1
  19. package/packages/sdk/dist/client.js +2 -0
  20. package/packages/sdk/dist/client.js.map +1 -1
  21. package/packages/sdk/dist/protocol.d.ts +4 -0
  22. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  23. package/packages/sdk/dist/workflows/cli.js +10 -0
  24. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  25. package/packages/sdk/dist/workflows/runner.d.ts +31 -0
  26. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  27. package/packages/sdk/dist/workflows/runner.js +542 -31
  28. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  29. package/packages/sdk/dist/workflows/trajectory.d.ts +22 -1
  30. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  31. package/packages/sdk/dist/workflows/trajectory.js +55 -8
  32. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  33. package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
  34. package/packages/sdk/dist/workflows/validator.js +29 -0
  35. package/packages/sdk/dist/workflows/validator.js.map +1 -1
  36. package/packages/sdk/package.json +2 -2
  37. package/packages/sdk/src/__tests__/e2e-owner-review.test.ts +778 -0
  38. package/packages/sdk/src/__tests__/workflow-runner.test.ts +484 -9
  39. package/packages/sdk/src/client.ts +4 -0
  40. package/packages/sdk/src/protocol.ts +4 -0
  41. package/packages/sdk/src/workflows/README.md +11 -0
  42. package/packages/sdk/src/workflows/cli.ts +10 -0
  43. package/packages/sdk/src/workflows/runner.ts +714 -33
  44. package/packages/sdk/src/workflows/trajectory.ts +89 -8
  45. package/packages/sdk/src/workflows/validator.ts +29 -0
  46. package/packages/sdk-py/pyproject.toml +1 -1
  47. package/packages/telemetry/package.json +1 -1
  48. package/packages/trajectory/package.json +2 -2
  49. package/packages/user-directory/package.json +2 -2
  50. package/packages/utils/package.json +2 -2
  51. package/relay-snippets/agent-relay-snippet.md +12 -0
package/dist/index.cjs CHANGED
@@ -9189,6 +9189,8 @@ var AgentRelayClient = class _AgentRelayClient {
9189
9189
  text: input.text,
9190
9190
  from: input.from,
9191
9191
  thread_id: input.threadId,
9192
+ workspace_id: input.workspaceId,
9193
+ workspace_alias: input.workspaceAlias,
9192
9194
  priority: input.priority,
9193
9195
  data: input.data
9194
9196
  });
@@ -47272,18 +47274,66 @@ var WorkflowTrajectory = class {
47272
47274
  }
47273
47275
  // ── Step events ────────────────────────────────────────────────────────────
47274
47276
  /** Record step started — captures intent, not just assignment. */
47275
- async stepStarted(step, agent) {
47277
+ async stepStarted(step, agent, participants) {
47276
47278
  if (!this.enabled || !this.trajectory)
47277
47279
  return;
47278
- if (!this.trajectory.agents.some((a) => a.name === agent)) {
47280
+ await this.registerAgent(agent, participants?.role ?? step.agent ?? "deterministic");
47281
+ if (participants?.owner && participants.owner !== agent) {
47282
+ await this.registerAgent(participants.owner, "owner");
47283
+ }
47284
+ if (participants?.specialist) {
47285
+ await this.registerAgent(participants.specialist, "specialist");
47286
+ }
47287
+ if (participants?.reviewer) {
47288
+ await this.registerAgent(participants.reviewer, "reviewer");
47289
+ }
47290
+ const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47291
+ this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
47292
+ await this.flush();
47293
+ }
47294
+ async registerAgent(name, role) {
47295
+ if (!this.enabled || !this.trajectory)
47296
+ return;
47297
+ if (!this.trajectory.agents.some((a) => a.name === name)) {
47279
47298
  this.trajectory.agents.push({
47280
- name: agent,
47281
- role: step.agent ?? "deterministic",
47299
+ name,
47300
+ role,
47282
47301
  joinedAt: (/* @__PURE__ */ new Date()).toISOString()
47283
47302
  });
47303
+ await this.flush();
47284
47304
  }
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 });
47305
+ }
47306
+ async stepSupervisionAssigned(step, supervised) {
47307
+ if (!this.enabled || !this.trajectory)
47308
+ return;
47309
+ await this.registerAgent(supervised.owner.name, "owner");
47310
+ await this.registerAgent(supervised.specialist.name, "specialist");
47311
+ if (supervised.reviewer?.name) {
47312
+ await this.registerAgent(supervised.reviewer.name, "reviewer");
47313
+ }
47314
+ const reviewerNote = supervised.reviewer?.name ? `, reviewer=${supervised.reviewer.name}` : "";
47315
+ this.addEvent("decision", `"${step.name}" supervision assigned \u2192 owner=${supervised.owner.name}, specialist=${supervised.specialist.name}${reviewerNote}`, "medium", {
47316
+ owner: supervised.owner.name,
47317
+ specialist: supervised.specialist.name,
47318
+ reviewer: supervised.reviewer?.name
47319
+ });
47320
+ await this.flush();
47321
+ }
47322
+ async ownerMonitoringEvent(stepName, owner, detail, raw) {
47323
+ if (!this.enabled || !this.trajectory)
47324
+ return;
47325
+ this.addEvent("note", `"${stepName}" owner ${owner}: ${detail}`, "medium", raw ? { owner, ...raw } : { owner });
47326
+ await this.flush();
47327
+ }
47328
+ async reviewCompleted(stepName, reviewerName, decision, reason) {
47329
+ if (!this.enabled || !this.trajectory)
47330
+ return;
47331
+ this.addEvent("review-completed", `"${stepName}" review ${decision} by ${reviewerName}`, "medium", {
47332
+ stepName,
47333
+ reviewer: reviewerName,
47334
+ decision,
47335
+ reason
47336
+ });
47287
47337
  await this.flush();
47288
47338
  }
47289
47339
  /** Record step completed — captures what was accomplished. */
@@ -47613,6 +47663,8 @@ var WorkflowRunner = class _WorkflowRunner {
47613
47663
  lastIdleLog = /* @__PURE__ */ new Map();
47614
47664
  /** Tracks last logged activity type per agent to avoid duplicate status lines. */
47615
47665
  lastActivity = /* @__PURE__ */ new Map();
47666
+ /** Runtime-name lookup for agents participating in supervised owner flows. */
47667
+ supervisedRuntimeAgents = /* @__PURE__ */ new Map();
47616
47668
  /** Resolved named paths from the top-level `paths` config, keyed by name → absolute directory. */
47617
47669
  resolvedPaths = /* @__PURE__ */ new Map();
47618
47670
  constructor(options = {}) {
@@ -48523,6 +48575,10 @@ ${err.suggestion}`);
48523
48575
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
48524
48576
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
48525
48577
  this.log(`[msg] ${fromShort} \u2192 ${toShort}: ${body}`);
48578
+ const supervision = this.supervisedRuntimeAgents.get(msg.from);
48579
+ if (supervision?.role === "owner") {
48580
+ void this.trajectory?.ownerMonitoringEvent(supervision.stepName, supervision.logicalName, `Messaged ${msg.to}: ${msg.text.slice(0, 120)}`, { to: msg.to, text: msg.text });
48581
+ }
48526
48582
  };
48527
48583
  this.relay.onAgentSpawned = (agent) => {
48528
48584
  if (!this.activeAgentHandles.has(agent.name)) {
@@ -48625,6 +48681,7 @@ ${err.suggestion}`);
48625
48681
  }
48626
48682
  this.lastIdleLog.clear();
48627
48683
  this.lastActivity.clear();
48684
+ this.supervisedRuntimeAgents.clear();
48628
48685
  this.log("Shutting down broker...");
48629
48686
  await this.relay?.shutdown();
48630
48687
  this.relay = void 0;
@@ -49131,10 +49188,19 @@ ${trimmedOutput.slice(0, 200)}`);
49131
49188
  if (!rawAgentDef) {
49132
49189
  throw new Error(`Agent "${agentName}" not found in config`);
49133
49190
  }
49134
- const agentDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
49135
- const maxRetries = step.retries ?? agentDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
49191
+ const specialistDef = _WorkflowRunner.resolveAgentDef(rawAgentDef);
49192
+ const usesOwnerFlow = specialistDef.interactive !== false;
49193
+ const ownerDef = usesOwnerFlow ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
49194
+ const reviewDef = usesOwnerFlow ? this.resolveAutoReviewAgent(ownerDef, agentMap) : void 0;
49195
+ const supervised = {
49196
+ specialist: specialistDef,
49197
+ owner: ownerDef,
49198
+ reviewer: reviewDef
49199
+ };
49200
+ const usesDedicatedOwner = usesOwnerFlow && ownerDef.name !== specialistDef.name;
49201
+ const maxRetries = step.retries ?? ownerDef.constraints?.retries ?? specialistDef.constraints?.retries ?? errorHandling?.maxRetries ?? 0;
49136
49202
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49137
- const timeoutMs = step.timeoutMs ?? agentDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49203
+ const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49138
49204
  let lastError;
49139
49205
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49140
49206
  this.checkAborted();
@@ -49158,48 +49224,95 @@ ${trimmedOutput.slice(0, 200)}`);
49158
49224
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49159
49225
  });
49160
49226
  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);
49227
+ this.postToChannel(`**[${step.name}]** Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
49228
+ await this.trajectory?.stepStarted(step, ownerDef.name, {
49229
+ role: usesDedicatedOwner ? "owner" : "specialist",
49230
+ owner: ownerDef.name,
49231
+ specialist: specialistDef.name,
49232
+ reviewer: reviewDef?.name
49233
+ });
49234
+ if (usesDedicatedOwner) {
49235
+ await this.trajectory?.stepSupervisionAssigned(step, supervised);
49236
+ }
49237
+ this.emit({
49238
+ type: "step:owner-assigned",
49239
+ runId,
49240
+ stepName: step.name,
49241
+ ownerName: ownerDef.name,
49242
+ specialistName: specialistDef.name
49243
+ });
49163
49244
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49164
49245
  let resolvedTask = this.interpolateStepTask(step.task ?? "", stepOutputContext);
49165
- if (agentDef.interactive !== false) {
49246
+ if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
49166
49247
  const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
49167
49248
  if (nonInteractiveInfo) {
49168
49249
  resolvedTask += nonInteractiveInfo;
49169
49250
  }
49170
49251
  }
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 };
49252
+ const applyStepWorkdir = (def) => {
49253
+ if (step.workdir) {
49254
+ const stepWorkdir = this.resolveStepWorkdir(step);
49255
+ if (stepWorkdir) {
49256
+ return { ...def, cwd: stepWorkdir, workdir: void 0 };
49257
+ }
49176
49258
  }
49259
+ return def;
49260
+ };
49261
+ const effectiveSpecialist = applyStepWorkdir(specialistDef);
49262
+ const effectiveOwner = applyStepWorkdir(ownerDef);
49263
+ let specialistOutput;
49264
+ let ownerOutput;
49265
+ let ownerElapsed;
49266
+ if (usesDedicatedOwner) {
49267
+ const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
49268
+ specialistOutput = result.specialistOutput;
49269
+ ownerOutput = result.ownerOutput;
49270
+ ownerElapsed = result.ownerElapsed;
49271
+ } else {
49272
+ const ownerTask = this.injectStepOwnerContract(step, resolvedTask, effectiveOwner, effectiveSpecialist);
49273
+ this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
49274
+ const resolvedStep = { ...step, task: ownerTask };
49275
+ const ownerStartTime = Date.now();
49276
+ const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
49277
+ ownerElapsed = Date.now() - ownerStartTime;
49278
+ this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
49279
+ if (usesOwnerFlow) {
49280
+ this.assertOwnerCompletionMarker(step, output, ownerTask);
49281
+ }
49282
+ specialistOutput = output;
49283
+ ownerOutput = output;
49177
49284
  }
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
49285
  if (step.verification) {
49183
- this.runVerification(step.verification, output, step.name, resolvedTask);
49286
+ this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
49287
+ }
49288
+ let combinedOutput = specialistOutput;
49289
+ if (usesOwnerFlow && reviewDef) {
49290
+ const remainingMs = timeoutMs ? Math.max(0, timeoutMs - ownerElapsed) : void 0;
49291
+ const reviewOutput = await this.runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewDef, remainingMs);
49292
+ combinedOutput = this.combineStepAndReviewOutput(specialistOutput, reviewOutput);
49184
49293
  }
49185
49294
  state.row.status = "completed";
49186
- state.row.output = output;
49295
+ state.row.output = combinedOutput;
49187
49296
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49188
49297
  await this.db.updateStep(state.row.id, {
49189
49298
  status: "completed",
49190
- output,
49299
+ output: combinedOutput,
49191
49300
  completedAt: state.row.completedAt,
49192
49301
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49193
49302
  });
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);
49303
+ await this.persistStepOutput(runId, step.name, combinedOutput);
49304
+ this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput });
49305
+ await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
49197
49306
  return;
49198
49307
  } catch (err) {
49199
49308
  lastError = err instanceof Error ? err.message : String(err);
49309
+ const ownerTimedOut = usesDedicatedOwner ? /\bowner timed out\b/i.test(lastError) : /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
49310
+ if (ownerTimedOut) {
49311
+ this.emit({ type: "step:owner-timeout", runId, stepName: step.name, ownerName: ownerDef.name });
49312
+ }
49200
49313
  }
49201
49314
  }
49202
- const nonInteractive = agentDef.interactive === false || ["worker", "reviewer", "analyst"].includes(agentDef.preset ?? "");
49315
+ const nonInteractive = ownerDef.interactive === false || ["worker", "reviewer", "analyst"].includes(ownerDef.preset ?? "");
49203
49316
  const verificationValue = typeof step.verification === "object" && "value" in step.verification ? String(step.verification.value) : void 0;
49204
49317
  await this.trajectory?.stepFailed(step, lastError ?? "Unknown error", maxRetries + 1, maxRetries, {
49205
49318
  agent: agentName,
@@ -49210,6 +49323,429 @@ ${trimmedOutput.slice(0, 200)}`);
49210
49323
  await this.markStepFailed(state, lastError ?? "Unknown error", runId);
49211
49324
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
49212
49325
  }
49326
+ injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
49327
+ if (ownerDef.interactive === false)
49328
+ return resolvedTask;
49329
+ const specialistNote = ownerDef.name === specialistDef.name ? "" : `Specialist intended for this step: "${specialistDef.name}" (${specialistDef.role ?? specialistDef.cli}).`;
49330
+ return resolvedTask + `
49331
+
49332
+ ---
49333
+ STEP OWNER CONTRACT:
49334
+ - You are the accountable owner for step "${step.name}".
49335
+ ` + (specialistNote ? `- ${specialistNote}
49336
+ ` : "") + `- If you delegate, you must still verify completion yourself.
49337
+ - Before exiting, provide an explicit completion line: STEP_COMPLETE:${step.name}
49338
+ - Then self-terminate immediately with /exit.`;
49339
+ }
49340
+ buildOwnerSupervisorTask(step, originalTask, supervised, workerRuntimeName) {
49341
+ const verificationGuide = this.buildSupervisorVerificationGuide(step.verification);
49342
+ const channelLine = this.channel ? `#${this.channel}` : "(workflow channel unavailable)";
49343
+ return `You are the step owner/supervisor for step "${step.name}".
49344
+
49345
+ Worker: ${supervised.specialist.name} (runtime: ${workerRuntimeName}) on ${channelLine}
49346
+ Task: ${originalTask}
49347
+
49348
+ Your job: Monitor the worker and determine when the task is complete.
49349
+
49350
+ How to verify completion:
49351
+ - Watch ${channelLine} for the worker's progress messages and mirrored PTY output
49352
+ - Check file changes: run \`git diff --stat\` or inspect expected files directly
49353
+ - Ask the worker directly on ${channelLine} if you need a status update
49354
+ ` + verificationGuide + `
49355
+ When you're satisfied the work is done correctly:
49356
+ Output exactly: STEP_COMPLETE:${step.name}`;
49357
+ }
49358
+ buildSupervisorVerificationGuide(verification) {
49359
+ if (!verification)
49360
+ return "";
49361
+ switch (verification.type) {
49362
+ case "output_contains":
49363
+ return `- Verification gate: confirm the worker output contains ${JSON.stringify(verification.value)}
49364
+ `;
49365
+ case "file_exists":
49366
+ return `- Verification gate: confirm the file exists at ${JSON.stringify(verification.value)}
49367
+ `;
49368
+ case "exit_code":
49369
+ return `- Verification gate: confirm the worker exits with code ${JSON.stringify(verification.value)}
49370
+ `;
49371
+ case "custom":
49372
+ return `- Verification gate: apply the custom verification rule ${JSON.stringify(verification.value)}
49373
+ `;
49374
+ default:
49375
+ return "";
49376
+ }
49377
+ }
49378
+ async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
49379
+ if (this.executor) {
49380
+ const supervisorTask2 = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
49381
+ const specialistStep2 = { ...step, task: resolvedTask };
49382
+ const ownerStep2 = {
49383
+ ...step,
49384
+ name: `${step.name}-owner`,
49385
+ agent: supervised.owner.name,
49386
+ task: supervisorTask2
49387
+ };
49388
+ this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" and owner "${supervised.owner.name}"`);
49389
+ const specialistPromise = this.executor.executeAgentStep(specialistStep2, supervised.specialist, resolvedTask, timeoutMs);
49390
+ const specialistSettled = specialistPromise.catch(() => void 0);
49391
+ try {
49392
+ const ownerStartTime2 = Date.now();
49393
+ const ownerOutput = await this.executor.executeAgentStep(ownerStep2, supervised.owner, supervisorTask2, timeoutMs);
49394
+ const ownerElapsed = Date.now() - ownerStartTime2;
49395
+ this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask2);
49396
+ const specialistOutput = await specialistPromise;
49397
+ return { specialistOutput, ownerOutput, ownerElapsed };
49398
+ } catch (error95) {
49399
+ await specialistSettled;
49400
+ throw error95;
49401
+ }
49402
+ }
49403
+ let workerHandle;
49404
+ let workerRuntimeName = supervised.specialist.name;
49405
+ let workerSpawned = false;
49406
+ let workerReleased = false;
49407
+ let resolveWorkerSpawn;
49408
+ let rejectWorkerSpawn;
49409
+ const workerReady = new Promise((resolve3, reject) => {
49410
+ resolveWorkerSpawn = resolve3;
49411
+ rejectWorkerSpawn = reject;
49412
+ });
49413
+ const specialistStep = { ...step, task: resolvedTask };
49414
+ this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
49415
+ const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
49416
+ agentNameSuffix: "worker",
49417
+ onSpawned: ({ actualName, agent }) => {
49418
+ workerHandle = agent;
49419
+ workerRuntimeName = actualName;
49420
+ this.supervisedRuntimeAgents.set(actualName, {
49421
+ stepName: step.name,
49422
+ role: "specialist",
49423
+ logicalName: supervised.specialist.name
49424
+ });
49425
+ if (!workerSpawned) {
49426
+ workerSpawned = true;
49427
+ resolveWorkerSpawn();
49428
+ }
49429
+ },
49430
+ onChunk: ({ agentName, chunk }) => {
49431
+ this.forwardAgentChunkToChannel(step.name, "Worker", agentName, chunk);
49432
+ }
49433
+ }).catch((error95) => {
49434
+ if (!workerSpawned) {
49435
+ workerSpawned = true;
49436
+ rejectWorkerSpawn(error95);
49437
+ }
49438
+ throw error95;
49439
+ });
49440
+ const workerSettled = workerPromise.catch(() => void 0);
49441
+ workerPromise.then((output) => {
49442
+ workerReleased = true;
49443
+ this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
49444
+ if (step.verification?.type === "output_contains" && output.includes(step.verification.value)) {
49445
+ this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
49446
+ }
49447
+ }).catch((error95) => {
49448
+ const message = error95 instanceof Error ? error95.message : String(error95);
49449
+ this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited with error: ${message}`);
49450
+ });
49451
+ await workerReady;
49452
+ const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, workerRuntimeName);
49453
+ const ownerStep = {
49454
+ ...step,
49455
+ name: `${step.name}-owner`,
49456
+ agent: supervised.owner.name,
49457
+ task: supervisorTask
49458
+ };
49459
+ this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
49460
+ const ownerStartTime = Date.now();
49461
+ try {
49462
+ const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49463
+ agentNameSuffix: "owner",
49464
+ onSpawned: ({ actualName }) => {
49465
+ this.supervisedRuntimeAgents.set(actualName, {
49466
+ stepName: step.name,
49467
+ role: "owner",
49468
+ logicalName: supervised.owner.name
49469
+ });
49470
+ },
49471
+ onChunk: ({ chunk }) => {
49472
+ void this.recordOwnerMonitoringChunk(step, supervised.owner, chunk);
49473
+ }
49474
+ });
49475
+ const ownerElapsed = Date.now() - ownerStartTime;
49476
+ this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
49477
+ this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
49478
+ const specialistOutput = await workerPromise;
49479
+ return { specialistOutput, ownerOutput, ownerElapsed };
49480
+ } catch (error95) {
49481
+ const message = error95 instanceof Error ? error95.message : String(error95);
49482
+ if (!workerReleased && workerHandle) {
49483
+ await workerHandle.release().catch(() => void 0);
49484
+ }
49485
+ await workerSettled;
49486
+ if (/\btimed out\b/i.test(message)) {
49487
+ throw new Error(`Step "${step.name}" owner timed out after ${timeoutMs ?? "unknown"}ms`);
49488
+ }
49489
+ throw error95;
49490
+ }
49491
+ }
49492
+ forwardAgentChunkToChannel(stepName, roleLabel, agentName, chunk) {
49493
+ const lines = _WorkflowRunner.stripAnsi(chunk).split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 3);
49494
+ for (const line of lines) {
49495
+ this.postToChannel(`**[${stepName}]** ${roleLabel} \`${agentName}\`: ${line.slice(0, 280)}`);
49496
+ }
49497
+ }
49498
+ async recordOwnerMonitoringChunk(step, ownerDef, chunk) {
49499
+ const stripped = _WorkflowRunner.stripAnsi(chunk);
49500
+ const details = [];
49501
+ if (/git diff --stat/i.test(stripped))
49502
+ details.push("Checked git diff stats");
49503
+ if (/\bls -la\b/i.test(stripped))
49504
+ details.push("Listed files for verification");
49505
+ if (/status update\?/i.test(stripped))
49506
+ details.push("Asked the worker for a status update");
49507
+ if (/STEP_COMPLETE:/i.test(stripped))
49508
+ details.push("Declared the step complete");
49509
+ for (const detail of details) {
49510
+ await this.trajectory?.ownerMonitoringEvent(step.name, ownerDef.name, detail, {
49511
+ output: stripped.slice(0, 240)
49512
+ });
49513
+ }
49514
+ }
49515
+ resolveAutoStepOwner(specialistDef, agentMap) {
49516
+ if (specialistDef.interactive === false)
49517
+ return specialistDef;
49518
+ const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
49519
+ const candidates = allDefs.filter((d) => d.interactive !== false);
49520
+ const matchesHubRole = (text) => [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`, "i").test(text));
49521
+ const ownerish = (def) => {
49522
+ const nameLC = def.name.toLowerCase();
49523
+ const roleLC = def.role?.toLowerCase() ?? "";
49524
+ return matchesHubRole(nameLC) || matchesHubRole(roleLC);
49525
+ };
49526
+ const ownerPriority = (def) => {
49527
+ const roleLC = def.role?.toLowerCase() ?? "";
49528
+ const nameLC = def.name.toLowerCase();
49529
+ if (/\blead\b/.test(roleLC) || /\blead\b/.test(nameLC))
49530
+ return 6;
49531
+ if (/\bcoordinator\b/.test(roleLC) || /\bcoordinator\b/.test(nameLC))
49532
+ return 5;
49533
+ if (/\bsupervisor\b/.test(roleLC) || /\bsupervisor\b/.test(nameLC))
49534
+ return 4;
49535
+ if (/\borchestrator\b/.test(roleLC) || /\borchestrator\b/.test(nameLC))
49536
+ return 3;
49537
+ if (/\bhub\b/.test(roleLC) || /\bhub\b/.test(nameLC))
49538
+ return 2;
49539
+ return ownerish(def) ? 1 : 0;
49540
+ };
49541
+ const dedicatedOwner = candidates.filter((d) => d.name !== specialistDef.name && ownerish(d)).sort((a, b) => ownerPriority(b) - ownerPriority(a) || a.name.localeCompare(b.name))[0];
49542
+ if (dedicatedOwner)
49543
+ return dedicatedOwner;
49544
+ return specialistDef;
49545
+ }
49546
+ resolveAutoReviewAgent(ownerDef, agentMap) {
49547
+ const allDefs = [...agentMap.values()].map((d) => _WorkflowRunner.resolveAgentDef(d));
49548
+ const isReviewer = (def) => {
49549
+ const roleLC = def.role?.toLowerCase() ?? "";
49550
+ const nameLC = def.name.toLowerCase();
49551
+ return def.preset === "reviewer" || roleLC.includes("review") || roleLC.includes("critic") || roleLC.includes("verifier") || roleLC.includes("qa") || nameLC.includes("review");
49552
+ };
49553
+ const reviewerPriority = (def) => {
49554
+ if (def.preset === "reviewer")
49555
+ return 5;
49556
+ const roleLC = def.role?.toLowerCase() ?? "";
49557
+ const nameLC = def.name.toLowerCase();
49558
+ if (roleLC.includes("review") || nameLC.includes("review"))
49559
+ return 4;
49560
+ if (roleLC.includes("verifier") || roleLC.includes("qa"))
49561
+ return 3;
49562
+ if (roleLC.includes("critic"))
49563
+ return 2;
49564
+ return isReviewer(def) ? 1 : 0;
49565
+ };
49566
+ const dedicated = allDefs.filter((d) => d.name !== ownerDef.name && isReviewer(d)).sort((a, b) => reviewerPriority(b) - reviewerPriority(a) || a.name.localeCompare(b.name))[0];
49567
+ if (dedicated)
49568
+ return dedicated;
49569
+ const alternate = allDefs.find((d) => d.name !== ownerDef.name && d.interactive !== false);
49570
+ if (alternate)
49571
+ return alternate;
49572
+ return ownerDef;
49573
+ }
49574
+ assertOwnerCompletionMarker(step, output, injectedTaskText) {
49575
+ const marker = `STEP_COMPLETE:${step.name}`;
49576
+ const taskHasMarker = injectedTaskText.includes(marker);
49577
+ const first = output.indexOf(marker);
49578
+ if (first === -1) {
49579
+ throw new Error(`Step "${step.name}" owner completion marker missing: "${marker}"`);
49580
+ }
49581
+ const outputLikelyContainsInjectedPrompt = output.includes("STEP OWNER CONTRACT") || output.includes("Output exactly: STEP_COMPLETE:");
49582
+ if (taskHasMarker && outputLikelyContainsInjectedPrompt) {
49583
+ const hasSecond = output.includes(marker, first + marker.length);
49584
+ if (!hasSecond) {
49585
+ throw new Error(`Step "${step.name}" owner completion marker missing in agent response: "${marker}"`);
49586
+ }
49587
+ }
49588
+ }
49589
+ async runStepReviewGate(step, resolvedTask, specialistOutput, ownerOutput, ownerDef, reviewerDef, timeoutMs) {
49590
+ const reviewSnippetMax = 12e3;
49591
+ let specialistSnippet = specialistOutput;
49592
+ if (specialistOutput.length > reviewSnippetMax) {
49593
+ const head = Math.floor(reviewSnippetMax / 2);
49594
+ const tail = reviewSnippetMax - head;
49595
+ const omitted = specialistOutput.length - head - tail;
49596
+ specialistSnippet = `${specialistOutput.slice(0, head)}
49597
+ ...[truncated ${omitted} chars for review]...
49598
+ ${specialistOutput.slice(specialistOutput.length - tail)}`;
49599
+ }
49600
+ let ownerSnippet = ownerOutput;
49601
+ if (ownerOutput.length > reviewSnippetMax) {
49602
+ const head = Math.floor(reviewSnippetMax / 2);
49603
+ const tail = reviewSnippetMax - head;
49604
+ const omitted = ownerOutput.length - head - tail;
49605
+ ownerSnippet = `${ownerOutput.slice(0, head)}
49606
+ ...[truncated ${omitted} chars for review]...
49607
+ ${ownerOutput.slice(ownerOutput.length - tail)}`;
49608
+ }
49609
+ const reviewTask = `Review workflow step "${step.name}" for completion and safe handoff.
49610
+ Step owner: ${ownerDef.name}
49611
+ Original objective:
49612
+ ${resolvedTask}
49613
+
49614
+ Specialist output:
49615
+ ${specialistSnippet}
49616
+
49617
+ Owner verification notes:
49618
+ ${ownerSnippet}
49619
+
49620
+ Return exactly:
49621
+ REVIEW_DECISION: APPROVE or REJECT
49622
+ REVIEW_REASON: <one sentence>
49623
+ Then output /exit.`;
49624
+ const safetyTimeoutMs = timeoutMs ?? 6e5;
49625
+ const reviewStep = {
49626
+ name: `${step.name}-review`,
49627
+ type: "agent",
49628
+ agent: reviewerDef.name,
49629
+ task: reviewTask
49630
+ };
49631
+ await this.trajectory?.registerAgent(reviewerDef.name, "reviewer");
49632
+ this.postToChannel(`**[${step.name}]** Review started (reviewer: ${reviewerDef.name})`);
49633
+ const emitReviewCompleted = async (decision, reason) => {
49634
+ await this.trajectory?.reviewCompleted(step.name, reviewerDef.name, decision, reason);
49635
+ this.emit({
49636
+ type: "step:review-completed",
49637
+ runId: this.currentRunId ?? "",
49638
+ stepName: step.name,
49639
+ reviewerName: reviewerDef.name,
49640
+ decision
49641
+ });
49642
+ };
49643
+ if (this.executor) {
49644
+ const reviewOutput2 = await this.executor.executeAgentStep(reviewStep, reviewerDef, reviewTask, safetyTimeoutMs);
49645
+ const parsed = this.parseReviewDecision(reviewOutput2);
49646
+ if (!parsed) {
49647
+ throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
49648
+ }
49649
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49650
+ if (parsed.decision === "rejected") {
49651
+ throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
49652
+ }
49653
+ this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
49654
+ return reviewOutput2;
49655
+ }
49656
+ let reviewerHandle;
49657
+ let reviewerReleased = false;
49658
+ let reviewOutput = "";
49659
+ let completedReview;
49660
+ let reviewCompletionPromise;
49661
+ const reviewCompletionStarted = { value: false };
49662
+ const startReviewCompletion = (parsed) => {
49663
+ if (reviewCompletionStarted.value)
49664
+ return;
49665
+ reviewCompletionStarted.value = true;
49666
+ completedReview = parsed;
49667
+ reviewCompletionPromise = (async () => {
49668
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49669
+ if (reviewerHandle && !reviewerReleased) {
49670
+ reviewerReleased = true;
49671
+ await reviewerHandle.release().catch(() => void 0);
49672
+ }
49673
+ })();
49674
+ };
49675
+ try {
49676
+ reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49677
+ onSpawned: ({ agent }) => {
49678
+ reviewerHandle = agent;
49679
+ },
49680
+ onChunk: ({ chunk }) => {
49681
+ const nextOutput = reviewOutput + _WorkflowRunner.stripAnsi(chunk);
49682
+ reviewOutput = nextOutput;
49683
+ const parsed = this.parseReviewDecision(nextOutput);
49684
+ if (parsed) {
49685
+ startReviewCompletion(parsed);
49686
+ }
49687
+ }
49688
+ });
49689
+ await reviewCompletionPromise;
49690
+ } catch (error95) {
49691
+ const message = error95 instanceof Error ? error95.message : String(error95);
49692
+ if (/\btimed out\b/i.test(message)) {
49693
+ this.log(`[${step.name}] Review safety backstop timeout fired after ${safetyTimeoutMs}ms`);
49694
+ throw new Error(`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`);
49695
+ }
49696
+ throw error95;
49697
+ }
49698
+ if (!completedReview) {
49699
+ const parsed = this.parseReviewDecision(reviewOutput);
49700
+ if (!parsed) {
49701
+ throw new Error(`Step "${step.name}" review response malformed from "${reviewerDef.name}" (missing REVIEW_DECISION)`);
49702
+ }
49703
+ completedReview = parsed;
49704
+ await emitReviewCompleted(parsed.decision, parsed.reason);
49705
+ }
49706
+ if (completedReview.decision === "rejected") {
49707
+ throw new Error(`Step "${step.name}" review rejected by "${reviewerDef.name}"`);
49708
+ }
49709
+ this.postToChannel(`**[${step.name}]** Review approved by \`${reviewerDef.name}\``);
49710
+ return reviewOutput;
49711
+ }
49712
+ parseReviewDecision(reviewOutput) {
49713
+ const decisionPattern = /REVIEW_DECISION:\s*(APPROVE|REJECT)/gi;
49714
+ const decisionMatches = [...reviewOutput.matchAll(decisionPattern)];
49715
+ if (decisionMatches.length === 0) {
49716
+ return null;
49717
+ }
49718
+ const outputLikelyContainsEchoedPrompt = reviewOutput.includes("Return exactly") || reviewOutput.includes("REVIEW_DECISION: APPROVE or REJECT");
49719
+ const decisionMatch = outputLikelyContainsEchoedPrompt && decisionMatches.length > 1 ? decisionMatches[decisionMatches.length - 1] : decisionMatches[0];
49720
+ const decision = decisionMatch?.[1]?.toUpperCase();
49721
+ if (decision !== "APPROVE" && decision !== "REJECT") {
49722
+ return null;
49723
+ }
49724
+ const reasonPattern = /REVIEW_REASON:\s*(.+)/gi;
49725
+ const reasonMatches = [...reviewOutput.matchAll(reasonPattern)];
49726
+ const reasonMatch = outputLikelyContainsEchoedPrompt && reasonMatches.length > 1 ? reasonMatches[reasonMatches.length - 1] : reasonMatches[0];
49727
+ const reason = reasonMatch?.[1]?.trim();
49728
+ return {
49729
+ decision: decision === "APPROVE" ? "approved" : "rejected",
49730
+ reason: reason && reason !== "<one sentence>" ? reason : void 0
49731
+ };
49732
+ }
49733
+ combineStepAndReviewOutput(stepOutput, reviewOutput) {
49734
+ const primary = stepOutput.trimEnd();
49735
+ const review = reviewOutput.trim();
49736
+ if (!review)
49737
+ return primary;
49738
+ if (!primary)
49739
+ return `REVIEW_OUTPUT
49740
+ ${review}
49741
+ `;
49742
+ return `${primary}
49743
+
49744
+ ---
49745
+ REVIEW_OUTPUT
49746
+ ${review}
49747
+ `;
49748
+ }
49213
49749
  /**
49214
49750
  * Build the CLI command and arguments for a non-interactive agent execution.
49215
49751
  * Each CLI has a specific flag for one-shot prompt mode.
@@ -49403,17 +49939,18 @@ DO NOT:
49403
49939
  this.unregisterWorker(agentName);
49404
49940
  }
49405
49941
  }
49406
- async spawnAndWait(agentDef, step, timeoutMs) {
49942
+ async spawnAndWait(agentDef, step, timeoutMs, options = {}) {
49407
49943
  if (agentDef.interactive === false) {
49408
49944
  return this.execNonInteractive(agentDef, step, timeoutMs);
49409
49945
  }
49410
49946
  if (!this.relay) {
49411
49947
  throw new Error("AgentRelay not initialized");
49412
49948
  }
49413
- let agentName = `${step.name}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49949
+ const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ""}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
49950
+ let agentName = requestedName;
49414
49951
  const role = agentDef.role?.toLowerCase() ?? "";
49415
49952
  const nameLC = agentDef.name.toLowerCase();
49416
- const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r));
49953
+ const isHub = _WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
49417
49954
  const pattern = this.currentConfig?.swarm.pattern;
49418
49955
  const isHubPattern = pattern && _WorkflowRunner.HUB_PATTERNS.has(pattern);
49419
49956
  const delegationGuidance = isHub || !isHubPattern ? this.buildDelegationGuidance(agentDef.cli, timeoutMs) : "";
@@ -49427,6 +49964,7 @@ DO NOT:
49427
49964
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49428
49965
  this.ptyOutputBuffers.get(agentName)?.push(stripped);
49429
49966
  logStream.write(chunk);
49967
+ options.onChunk?.({ agentName, chunk });
49430
49968
  });
49431
49969
  const agentChannels = this.channel ? [this.channel] : agentDef.channels;
49432
49970
  let agent;
@@ -49469,10 +50007,12 @@ DO NOT:
49469
50007
  const stripped = _WorkflowRunner.stripAnsi(chunk);
49470
50008
  this.ptyOutputBuffers.get(agent.name)?.push(stripped);
49471
50009
  newLogStream.write(chunk);
50010
+ options.onChunk?.({ agentName: agent.name, chunk });
49472
50011
  });
49473
50012
  }
49474
50013
  agentName = agent.name;
49475
50014
  }
50015
+ await options.onSpawned?.({ requestedName, actualName: agent.name, agent });
49476
50016
  let workerPid;
49477
50017
  try {
49478
50018
  const rawAgents = await this.relay.listAgentsRaw();
@@ -49525,6 +50065,7 @@ DO NOT:
49525
50065
  this.ptyLogStreams.delete(agentName);
49526
50066
  }
49527
50067
  this.unregisterWorker(agentName);
50068
+ this.supervisedRuntimeAgents.delete(agentName);
49528
50069
  }
49529
50070
  let output;
49530
50071
  if (ptyChunks.length > 0) {
@@ -49650,7 +50191,7 @@ DO NOT:
49650
50191
  continue;
49651
50192
  const role = agentDef.role?.toLowerCase() ?? "";
49652
50193
  const nameLC = agentDef.name.toLowerCase();
49653
- if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => role.includes(r))) {
50194
+ if (_WorkflowRunner.HUB_ROLES.has(nameLC) || [..._WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role))) {
49654
50195
  const handle = this.activeAgentHandles.get(agentDef.name);
49655
50196
  if (handle)
49656
50197
  return handle;