@agent-relay/sdk 4.0.4 → 4.0.6

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.
@@ -103,6 +103,20 @@ function resolveCursorCli() {
103
103
  const resolved = resolveCliSync('cursor');
104
104
  return resolved?.binary ?? 'agent';
105
105
  }
106
+ function getWorkflowSdkSpawner(relay, cli) {
107
+ switch (cli) {
108
+ case 'claude':
109
+ return relay.claude;
110
+ case 'codex':
111
+ return relay.codex;
112
+ case 'gemini':
113
+ return relay.gemini;
114
+ case 'opencode':
115
+ return relay.opencode;
116
+ default:
117
+ return null;
118
+ }
119
+ }
106
120
  // ── WorkflowRunner ──────────────────────────────────────────────────────────
107
121
  export class WorkflowRunner {
108
122
  db;
@@ -1070,12 +1084,13 @@ export class WorkflowRunner {
1070
1084
  });
1071
1085
  }
1072
1086
  getRelayEnv() {
1073
- if (!this.relayApiKey) {
1074
- return this.relayOptions.env;
1087
+ if (!this.relayApiKey && !this.relayOptions.env) {
1088
+ return undefined;
1075
1089
  }
1076
1090
  return {
1077
- ...(this.relayOptions.env ?? filteredEnv()),
1078
- RELAY_API_KEY: this.relayApiKey,
1091
+ ...process.env,
1092
+ ...(this.relayOptions.env ?? {}),
1093
+ ...(this.relayApiKey ? { RELAY_API_KEY: this.relayApiKey } : {}),
1079
1094
  };
1080
1095
  }
1081
1096
  async provisionAgents(config) {
@@ -2048,7 +2063,7 @@ export class WorkflowRunner {
2048
2063
  this.log('API key resolved');
2049
2064
  if (this.relayApiKeyAutoCreated && this.relayApiKey) {
2050
2065
  this.log(`Workspace created — follow this run in Relaycast:`);
2051
- this.log(` Observer: https://agentrelay.dev/observer?key=${this.relayApiKey}`);
2066
+ this.log(` Observer: https://agentrelay.com/observer?key=${this.relayApiKey}`);
2052
2067
  this.log(` Channel: ${channel}`);
2053
2068
  }
2054
2069
  }
@@ -3115,7 +3130,7 @@ export class WorkflowRunner {
3115
3130
  let completionReason;
3116
3131
  let promptTaskText;
3117
3132
  if (usesDedicatedOwner) {
3118
- const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs);
3133
+ const result = await this.executeSupervisedAgentStep(step, { specialist: effectiveSpecialist, owner: effectiveOwner, reviewer: reviewDef }, resolvedTask, timeoutMs, attempt);
3119
3134
  specialistOutput = result.specialistOutput;
3120
3135
  ownerOutput = result.ownerOutput;
3121
3136
  ownerElapsed = result.ownerElapsed;
@@ -3133,6 +3148,7 @@ export class WorkflowRunner {
3133
3148
  const spawnResult = this.executor
3134
3149
  ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs)
3135
3150
  : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs, {
3151
+ retryAttempt: attempt,
3136
3152
  evidenceStepName: step.name,
3137
3153
  evidenceRole: usesOwnerFlow ? 'owner' : 'specialist',
3138
3154
  preserveOnIdle: !isHubPattern || !this.isLeadLikeAgent(effectiveOwner) ? false : undefined,
@@ -3361,6 +3377,34 @@ export class WorkflowRunner {
3361
3377
  `- Do not rely on terminal output alone for handoff; use the workflow group chat signal above.\n` +
3362
3378
  `- After posting your handoff signal, self-terminate with /exit unless the owner asks for follow-up.`);
3363
3379
  }
3380
+ buildWorkflowRuntimeAgentBaseName(stepName, options) {
3381
+ return `${stepName}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ''}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
3382
+ }
3383
+ async releaseStaleRetryAgents(baseRequestedName, stepName) {
3384
+ if (!this.relay) {
3385
+ return;
3386
+ }
3387
+ const staleAgents = (await this.relay.listAgents()).filter((agent) => agent.name === baseRequestedName || agent.name.startsWith(`${baseRequestedName}-r`));
3388
+ if (staleAgents.length === 0) {
3389
+ return;
3390
+ }
3391
+ const staleNames = [...new Set(staleAgents.map((agent) => agent.name))].sort();
3392
+ this.log(`[${stepName}] Releasing stale retry agent(s): ${staleNames.join(', ')}`);
3393
+ for (const agent of staleAgents) {
3394
+ await agent.release(`workflow retry cleanup for step "${stepName}"`);
3395
+ }
3396
+ const deadline = Date.now() + 5_000;
3397
+ while (Date.now() < deadline) {
3398
+ const remaining = (await this.relay.listAgentsRaw())
3399
+ .map((agent) => agent.name)
3400
+ .filter((name) => staleNames.includes(name));
3401
+ if (remaining.length === 0) {
3402
+ return;
3403
+ }
3404
+ await this.delay(100);
3405
+ }
3406
+ throw new Error(`Failed to clear stale retry agent(s) before respawn: ${staleNames.join(', ')}`);
3407
+ }
3364
3408
  buildSupervisorVerificationGuide(verification) {
3365
3409
  if (!verification)
3366
3410
  return '';
@@ -3377,7 +3421,7 @@ export class WorkflowRunner {
3377
3421
  return '';
3378
3422
  }
3379
3423
  }
3380
- async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs) {
3424
+ async executeSupervisedAgentStep(step, supervised, resolvedTask, timeoutMs, retryAttempt = 0) {
3381
3425
  if (this.executor) {
3382
3426
  const specialistTask = this.buildWorkerHandoffTask(step, resolvedTask, supervised);
3383
3427
  const supervisorTask = this.buildOwnerSupervisorTask(step, resolvedTask, supervised, supervised.specialist.name);
@@ -3425,6 +3469,7 @@ export class WorkflowRunner {
3425
3469
  this.log(`[${step.name}] Spawning specialist "${supervised.specialist.name}" (cli: ${supervised.specialist.cli})`);
3426
3470
  const workerPromise = this.spawnAndWait(supervised.specialist, specialistStep, timeoutMs, {
3427
3471
  agentNameSuffix: 'worker',
3472
+ retryAttempt,
3428
3473
  evidenceStepName: step.name,
3429
3474
  evidenceRole: 'worker',
3430
3475
  logicalName: supervised.specialist.name,
@@ -3488,6 +3533,7 @@ export class WorkflowRunner {
3488
3533
  try {
3489
3534
  const ownerResultObj = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
3490
3535
  agentNameSuffix: 'owner',
3536
+ retryAttempt,
3491
3537
  evidenceStepName: step.name,
3492
3538
  evidenceRole: 'owner',
3493
3539
  logicalName: supervised.owner.name,
@@ -4411,9 +4457,14 @@ export class WorkflowRunner {
4411
4457
  throw new Error('AgentRelay not initialized');
4412
4458
  }
4413
4459
  const evidenceStepName = options.evidenceStepName ?? step.name;
4414
- // Deterministic name: step name + optional role suffix + first 8 chars of run ID.
4415
- const requestedName = `${step.name}${options.agentNameSuffix ? `-${options.agentNameSuffix}` : ''}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
4460
+ const baseRequestedName = this.buildWorkflowRuntimeAgentBaseName(step.name, options);
4461
+ const requestedName = (options.retryAttempt ?? 0) > 0
4462
+ ? `${baseRequestedName}-r${(options.retryAttempt ?? 0) + 1}`
4463
+ : baseRequestedName;
4416
4464
  let agentName = requestedName;
4465
+ if ((options.retryAttempt ?? 0) > 0) {
4466
+ await this.releaseStaleRetryAgents(baseRequestedName, step.name);
4467
+ }
4417
4468
  // Only inject delegation guidance for lead/coordinator agents, not spokes/workers.
4418
4469
  // In non-hub patterns (pipeline, dag, etc.) every agent is autonomous so they all get it.
4419
4470
  const role = agentDef.role?.toLowerCase() ?? '';
@@ -4422,20 +4473,28 @@ export class WorkflowRunner {
4422
4473
  [...WorkflowRunner.HUB_ROLES].some((r) => new RegExp(`\\b${r}\\b`).test(role));
4423
4474
  const pattern = this.currentConfig?.swarm.pattern;
4424
4475
  const isHubPattern = pattern && WorkflowRunner.HUB_PATTERNS.has(pattern);
4425
- const delegationGuidance = isHub || !isHubPattern ? this.buildDelegationGuidance(agentDef.cli, timeoutMs) : '';
4476
+ const usesHeadlessWorkflowSpawner = agentDef.cli === 'opencode';
4477
+ const delegationGuidance = usesHeadlessWorkflowSpawner || (!isHub && isHubPattern)
4478
+ ? ''
4479
+ : this.buildDelegationGuidance(agentDef.cli, timeoutMs);
4426
4480
  // Non-claude CLIs (codex, gemini, etc.) don't auto-register with Relaycast
4427
4481
  // via the MCP system prompt the way claude does. Inject an explicit preamble
4428
4482
  // so they call register() before any other relay tool.
4429
- const relayRegistrationNote = this.buildRelayRegistrationNote(agentDef.cli, agentName);
4430
- const taskWithExit = step.task +
4431
- (relayRegistrationNote ? '\n\n' + relayRegistrationNote : '') +
4432
- (delegationGuidance ? '\n\n' + delegationGuidance + '\n' : '') +
4433
- '\n\n---\n' +
4434
- 'IMPORTANT: When you have fully completed this task, you MUST self-terminate by either: ' +
4435
- '(a) calling remove_agent(name: "<your-agent-name>", reason: "task completed") — preferred, or ' +
4436
- '(b) outputting the exact text "/exit" on its own line as a fallback. ' +
4437
- 'Do not wait for further input terminate immediately after finishing. ' +
4438
- 'Do NOT spawn sub-agents unless the task explicitly requires it.';
4483
+ const relayRegistrationNote = usesHeadlessWorkflowSpawner
4484
+ ? ''
4485
+ : this.buildRelayRegistrationNote(agentDef.cli, agentName);
4486
+ const interactiveTaskBase = step.task ?? '';
4487
+ const taskWithExit = usesHeadlessWorkflowSpawner
4488
+ ? interactiveTaskBase
4489
+ : interactiveTaskBase +
4490
+ (relayRegistrationNote ? '\n\n' + relayRegistrationNote : '') +
4491
+ (delegationGuidance ? '\n\n' + delegationGuidance + '\n' : '') +
4492
+ '\n\n---\n' +
4493
+ 'IMPORTANT: When you have fully completed this task, you MUST self-terminate by either: ' +
4494
+ '(a) calling remove_agent(name: "<your-agent-name>", reason: "task completed") — preferred, or ' +
4495
+ '(b) outputting the exact text "/exit" on its own line as a fallback. ' +
4496
+ 'Do not wait for further input — terminate immediately after finishing. ' +
4497
+ 'Do NOT spawn sub-agents unless the task explicitly requires it.';
4439
4498
  const preparedTask = this.prepareInteractiveSpawnTask(agentName, taskWithExit);
4440
4499
  // Register PTY output listener before spawning so we capture everything
4441
4500
  this.ptyOutputBuffers.set(agentName, []);
@@ -4464,9 +4523,8 @@ export class WorkflowRunner {
4464
4523
  RELAY_API_KEY: this.relayApiKey ?? 'workflow-runner',
4465
4524
  AGENT_CHANNELS: (agentChannels ?? []).join(','),
4466
4525
  });
4467
- agent = await this.relay.spawnPty({
4526
+ const spawnOptions = {
4468
4527
  name: agentName,
4469
- cli: agentDef.cli,
4470
4528
  model: agentDef.constraints?.model,
4471
4529
  args: interactiveSpawnPolicy.args,
4472
4530
  channels: agentChannels,
@@ -4474,7 +4532,19 @@ export class WorkflowRunner {
4474
4532
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
4475
4533
  cwd: agentCwd,
4476
4534
  agentToken: this.agentTokens.get(agentDef.name),
4477
- });
4535
+ };
4536
+ const sdkSpawner = getWorkflowSdkSpawner(this.relay, agentDef.cli);
4537
+ if (sdkSpawner) {
4538
+ this.log(`[${step.name}] Using SDK spawner for ${agentDef.cli} (requested runtime: ${agentDef.cli === 'opencode' ? 'headless' : 'pty'})`);
4539
+ agent = await sdkSpawner.spawn(spawnOptions);
4540
+ }
4541
+ else {
4542
+ this.log(`[${step.name}] Using PTY fallback for ${agentDef.cli}`);
4543
+ agent = await this.relay.spawnPty({
4544
+ ...spawnOptions,
4545
+ cli: agentDef.cli,
4546
+ });
4547
+ }
4478
4548
  // Re-key PTY maps if broker assigned a different name than requested
4479
4549
  if (agent.name !== agentName) {
4480
4550
  const oldName = agentName;
@@ -5351,6 +5421,10 @@ export class WorkflowRunner {
5351
5421
  }
5352
5422
  const maxMsg = 2000;
5353
5423
  const preview = scrubbed.length > maxMsg ? scrubbed.slice(-maxMsg) : scrubbed;
5424
+ // Surface the final output preview in the local workflow log immediately.
5425
+ // Some deterministic wrappers grep stdout/stderr for completion sentinels,
5426
+ // and fire-and-forget channel delivery can arrive too late for single-step runs.
5427
+ this.log(`[${stepName}] Output:\n\`\`\`\n${preview}\n\`\`\``);
5354
5428
  this.postToChannel(`**[${stepName}] Output:**\n\`\`\`\n${preview}\n\`\`\``, { stepName });
5355
5429
  }
5356
5430
  async persistAgentReport(runId, stepName, report) {