agent-relay 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +37 -244
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/index.cjs +342 -60
  7. package/dist/src/cli/commands/core.d.ts +2 -0
  8. package/dist/src/cli/commands/core.d.ts.map +1 -1
  9. package/dist/src/cli/commands/core.js +9 -2
  10. package/dist/src/cli/commands/core.js.map +1 -1
  11. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  12. package/dist/src/cli/lib/broker-lifecycle.js +87 -28
  13. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  14. package/package.json +9 -8
  15. package/packages/acp-bridge/README.md +50 -67
  16. package/packages/acp-bridge/package.json +2 -2
  17. package/packages/config/package.json +1 -1
  18. package/packages/hooks/package.json +4 -4
  19. package/packages/memory/package.json +2 -2
  20. package/packages/policy/package.json +2 -2
  21. package/packages/sdk/README.md +169 -64
  22. package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
  23. package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
  24. package/packages/sdk/dist/__tests__/facade.test.js +48 -0
  25. package/packages/sdk/dist/__tests__/facade.test.js.map +1 -1
  26. package/packages/sdk/dist/__tests__/integration.test.js +11 -5
  27. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  28. package/packages/sdk/dist/__tests__/unit.test.js +36 -0
  29. package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
  30. package/packages/sdk/dist/client.d.ts +36 -3
  31. package/packages/sdk/dist/client.d.ts.map +1 -1
  32. package/packages/sdk/dist/client.js +142 -9
  33. package/packages/sdk/dist/client.js.map +1 -1
  34. package/packages/sdk/dist/protocol.d.ts +7 -1
  35. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  36. package/packages/sdk/dist/relay.d.ts +74 -11
  37. package/packages/sdk/dist/relay.d.ts.map +1 -1
  38. package/packages/sdk/dist/relay.js +175 -27
  39. package/packages/sdk/dist/relay.js.map +1 -1
  40. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  41. package/packages/sdk/dist/workflows/runner.js +71 -36
  42. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  43. package/packages/sdk/dist/workflows/types.d.ts +1 -1
  44. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  45. package/packages/sdk/package.json +2 -2
  46. package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
  47. package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
  48. package/packages/sdk/src/__tests__/facade.test.ts +68 -0
  49. package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
  50. package/packages/sdk/src/__tests__/integration.test.ts +11 -5
  51. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
  52. package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
  53. package/packages/sdk/src/__tests__/unit.test.ts +44 -0
  54. package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
  55. package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
  56. package/packages/sdk/src/client.ts +195 -14
  57. package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
  58. package/packages/sdk/src/protocol.ts +7 -2
  59. package/packages/sdk/src/relay.ts +271 -38
  60. package/packages/sdk/src/workflows/runner.ts +73 -42
  61. package/packages/sdk/src/workflows/schema.json +1 -1
  62. package/packages/sdk/src/workflows/types.ts +1 -1
  63. package/packages/sdk/vitest.config.ts +1 -0
  64. package/packages/sdk-py/README.md +89 -102
  65. package/packages/sdk-py/agent_relay/__init__.py +16 -19
  66. package/packages/sdk-py/pyproject.toml +6 -2
  67. package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
  68. package/packages/sdk-py/src/agent_relay/client.py +776 -0
  69. package/packages/sdk-py/src/agent_relay/models.py +27 -0
  70. package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
  71. package/packages/sdk-py/src/agent_relay/relay.py +860 -0
  72. package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
  73. package/packages/telemetry/package.json +1 -1
  74. package/packages/trajectory/package.json +2 -2
  75. package/packages/user-directory/package.json +2 -2
  76. package/packages/utils/package.json +2 -2
  77. package/scripts/postinstall.js +35 -162
  78. package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
  79. package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
  80. package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
  81. package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
  82. package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
  83. package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
  84. package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
  85. package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
  86. package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
  87. package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
  88. package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
  89. package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
  90. package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
  91. package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
  92. package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
  93. package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
  94. package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
  95. package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
  96. package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
  97. package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
  98. package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
  99. package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
  100. package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
  101. package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
  102. package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
  103. package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
  104. package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
  105. package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
  106. package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
  107. package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
  108. package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
  109. package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
  110. package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
  111. package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
  112. package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
  113. package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
  114. package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
  115. package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
  116. package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
  117. package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
  118. package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
  119. package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
  120. package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
  121. package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
  122. package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
  123. package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
  124. package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
  125. package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
  126. package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
  127. package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
  128. package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
  129. package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
  130. package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
  131. package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
  132. package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
  133. package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
  134. package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
  135. package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
  136. package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
  137. package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
  138. package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
  139. package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
  140. package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
  141. package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
  142. package/packages/sdk/bin/agent-relay-broker +0 -0
@@ -1,5 +1,5 @@
1
1
  import { once } from 'node:events';
2
- import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { execSync, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
3
3
  import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
@@ -14,6 +14,7 @@ import {
14
14
  type BrokerStats,
15
15
  type BrokerStatus,
16
16
  type CrashInsightsResponse,
17
+ type HeadlessProvider,
17
18
  type ProtocolEnvelope,
18
19
  type ProtocolError,
19
20
  type RestartPolicy,
@@ -51,13 +52,33 @@ export interface SpawnPtyInput {
51
52
  continueFrom?: string;
52
53
  }
53
54
 
54
- export interface SpawnHeadlessClaudeInput {
55
+ export interface SpawnHeadlessInput {
55
56
  name: string;
57
+ provider: HeadlessProvider;
56
58
  args?: string[];
57
59
  channels?: string[];
58
60
  task?: string;
59
61
  }
60
62
 
63
+ export type AgentTransport = 'pty' | 'headless';
64
+
65
+ export interface SpawnProviderInput {
66
+ name: string;
67
+ provider: string;
68
+ transport?: AgentTransport;
69
+ args?: string[];
70
+ channels?: string[];
71
+ task?: string;
72
+ model?: string;
73
+ cwd?: string;
74
+ team?: string;
75
+ shadowOf?: string;
76
+ shadowMode?: string;
77
+ idleThresholdSecs?: number;
78
+ restartPolicy?: RestartPolicy;
79
+ continueFrom?: string;
80
+ }
81
+
61
82
  export interface SendMessageInput {
62
83
  to: string;
63
84
  text: string;
@@ -70,6 +91,7 @@ export interface SendMessageInput {
70
91
  export interface ListAgent {
71
92
  name: string;
72
93
  runtime: AgentRuntime;
94
+ provider?: HeadlessProvider;
73
95
  cli?: string;
74
96
  model?: string;
75
97
  team?: string;
@@ -113,6 +135,10 @@ export class AgentRelayProcessError extends Error {
113
135
  }
114
136
  }
115
137
 
138
+ function isHeadlessProvider(value: string): value is HeadlessProvider {
139
+ return value === 'claude' || value === 'opencode';
140
+ }
141
+
116
142
  export class AgentRelayClient {
117
143
  private readonly options: Required<AgentRelayClientOptions>;
118
144
  private child?: ChildProcessWithoutNullStreams;
@@ -127,6 +153,8 @@ export class AgentRelayClient {
127
153
  private eventBuffer: BrokerEvent[] = [];
128
154
  private maxBufferSize = 1000;
129
155
  private exitPromise?: Promise<void>;
156
+ /** The workspace key returned by the broker in its hello_ack response. */
157
+ workspaceKey?: string;
130
158
 
131
159
  constructor(options: AgentRelayClientOptions = {}) {
132
160
  this.options = {
@@ -248,13 +276,12 @@ export class AgentRelayClient {
248
276
  return result;
249
277
  }
250
278
 
251
- async spawnHeadlessClaude(
252
- input: SpawnHeadlessClaudeInput
253
- ): Promise<{ name: string; runtime: AgentRuntime }> {
279
+ async spawnHeadless(input: SpawnHeadlessInput): Promise<{ name: string; runtime: AgentRuntime }> {
254
280
  await this.start();
255
281
  const agent: AgentSpec = {
256
282
  name: input.name,
257
- runtime: 'headless_claude',
283
+ runtime: 'headless',
284
+ provider: input.provider,
258
285
  args: input.args ?? [],
259
286
  channels: input.channels ?? [],
260
287
  };
@@ -265,6 +292,52 @@ export class AgentRelayClient {
265
292
  return result;
266
293
  }
267
294
 
295
+ async spawnProvider(input: SpawnProviderInput): Promise<{ name: string; runtime: AgentRuntime }> {
296
+ const transport = input.transport ?? (input.provider === 'opencode' ? 'headless' : 'pty');
297
+ if (transport === 'headless') {
298
+ if (!isHeadlessProvider(input.provider)) {
299
+ throw new AgentRelayProcessError(
300
+ `provider '${input.provider}' does not support headless transport (supported: claude, opencode)`
301
+ );
302
+ }
303
+ return this.spawnHeadless({
304
+ name: input.name,
305
+ provider: input.provider,
306
+ args: input.args,
307
+ channels: input.channels,
308
+ task: input.task,
309
+ });
310
+ }
311
+
312
+ return this.spawnPty({
313
+ name: input.name,
314
+ cli: input.provider,
315
+ args: input.args,
316
+ channels: input.channels,
317
+ task: input.task,
318
+ model: input.model,
319
+ cwd: input.cwd,
320
+ team: input.team,
321
+ shadowOf: input.shadowOf,
322
+ shadowMode: input.shadowMode,
323
+ idleThresholdSecs: input.idleThresholdSecs,
324
+ restartPolicy: input.restartPolicy,
325
+ continueFrom: input.continueFrom,
326
+ });
327
+ }
328
+
329
+ async spawnClaude(
330
+ input: Omit<SpawnProviderInput, 'provider'>
331
+ ): Promise<{ name: string; runtime: AgentRuntime }> {
332
+ return this.spawnProvider({ ...input, provider: 'claude' });
333
+ }
334
+
335
+ async spawnOpencode(
336
+ input: Omit<SpawnProviderInput, 'provider'>
337
+ ): Promise<{ name: string; runtime: AgentRuntime }> {
338
+ return this.spawnProvider({ ...input, provider: 'opencode' });
339
+ }
340
+
268
341
  async release(name: string, reason?: string): Promise<{ name: string }> {
269
342
  await this.start();
270
343
  return this.requestOk<{ name: string }>('release_agent', { name, reason });
@@ -439,8 +512,11 @@ export class AgentRelayClient {
439
512
  });
440
513
  });
441
514
 
442
- await this.requestHello();
515
+ const helloAck = await this.requestHello();
443
516
  console.log('[broker] Broker ready (hello handshake complete)');
517
+ if (helloAck.workspace_key) {
518
+ this.workspaceKey = helloAck.workspace_key;
519
+ }
444
520
  }
445
521
 
446
522
  private disposeProcessHandles(): void {
@@ -528,13 +604,17 @@ export class AgentRelayClient {
528
604
  pending.resolve(envelope);
529
605
  }
530
606
 
531
- private async requestHello(): Promise<{ broker_version: string; protocol_version: number }> {
607
+ private async requestHello(): Promise<{
608
+ broker_version: string;
609
+ protocol_version: number;
610
+ workspace_key?: string;
611
+ }> {
532
612
  const payload = {
533
613
  client_name: this.options.clientName,
534
614
  client_version: this.options.clientVersion,
535
615
  };
536
616
  const frame = await this.sendRequest('hello', payload, 'hello_ack');
537
- return frame.payload as { broker_version: string; protocol_version: number };
617
+ return frame.payload as { broker_version: string; protocol_version: number; workspace_key?: string };
538
618
  }
539
619
 
540
620
  private async requestOk<T = unknown>(type: string, payload: unknown): Promise<T> {
@@ -589,12 +669,17 @@ export class AgentRelayClient {
589
669
 
590
670
  const CLI_MODEL_FLAG_CLIS = new Set(['claude', 'codex', 'gemini', 'goose', 'aider']);
591
671
 
672
+ const CLI_DEFAULT_ARGS: Record<string, string[]> = {
673
+ codex: ['-c', 'check_for_update_on_startup=false'],
674
+ };
675
+
592
676
  function buildPtyArgsWithModel(cli: string, args: string[], model?: string): string[] {
593
- const baseArgs = [...args];
677
+ const cliName = cli.split(':')[0].trim().toLowerCase();
678
+ const defaultArgs = CLI_DEFAULT_ARGS[cliName] ?? [];
679
+ const baseArgs = [...defaultArgs, ...args];
594
680
  if (!model) {
595
681
  return baseArgs;
596
682
  }
597
- const cliName = cli.split(':')[0].trim().toLowerCase();
598
683
  if (!CLI_MODEL_FLAG_CLIS.has(cliName)) {
599
684
  return baseArgs;
600
685
  }
@@ -634,6 +719,92 @@ function isExplicitPath(binaryPath: string): boolean {
634
719
  );
635
720
  }
636
721
 
722
+ function detectPlatformSuffix(): string | null {
723
+ const platformMap: Record<string, Record<string, string>> = {
724
+ darwin: { arm64: 'darwin-arm64', x64: 'darwin-x64' },
725
+ linux: { arm64: 'linux-arm64', x64: 'linux-x64' },
726
+ };
727
+ return platformMap[process.platform]?.[process.arch] ?? null;
728
+ }
729
+
730
+ function getLatestVersionSync(): string | null {
731
+ try {
732
+ const result = execSync('curl -fsSL https://api.github.com/repos/AgentWorkforce/relay/releases/latest', {
733
+ timeout: 15_000,
734
+ stdio: ['pipe', 'pipe', 'pipe'],
735
+ }).toString();
736
+ const match = result.match(/"tag_name"\s*:\s*"v?([^"]+)"/);
737
+ return match?.[1] ?? null;
738
+ } catch {
739
+ return null;
740
+ }
741
+ }
742
+
743
+ function installBrokerBinary(): string {
744
+ const suffix = detectPlatformSuffix();
745
+ if (!suffix) {
746
+ throw new AgentRelayProcessError(`Unsupported platform: ${process.platform}-${process.arch}`);
747
+ }
748
+
749
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
750
+ const installDir = path.join(homeDir, '.agent-relay', 'bin');
751
+ const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
752
+ const targetPath = path.join(installDir, brokerExe);
753
+
754
+ console.log(`[agent-relay] Broker binary not found, installing for ${suffix}...`);
755
+
756
+ const version = getLatestVersionSync();
757
+ if (!version) {
758
+ throw new AgentRelayProcessError(
759
+ 'Failed to fetch latest agent-relay version from GitHub.\n' +
760
+ 'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash'
761
+ );
762
+ }
763
+
764
+ const binaryName = `agent-relay-broker-${suffix}`;
765
+ const downloadUrl = `https://github.com/AgentWorkforce/relay/releases/download/v${version}/${binaryName}`;
766
+
767
+ console.log(`[agent-relay] Downloading v${version} from ${downloadUrl}`);
768
+
769
+ try {
770
+ fs.mkdirSync(installDir, { recursive: true });
771
+ execSync(`curl -fsSL "${downloadUrl}" -o "${targetPath}"`, {
772
+ timeout: 60_000,
773
+ stdio: ['pipe', 'pipe', 'pipe'],
774
+ });
775
+ fs.chmodSync(targetPath, 0o755);
776
+
777
+ // macOS: re-sign to avoid Gatekeeper issues
778
+ if (process.platform === 'darwin') {
779
+ try {
780
+ execSync(`codesign --force --sign - "${targetPath}"`, {
781
+ timeout: 10_000,
782
+ stdio: ['pipe', 'pipe', 'pipe'],
783
+ });
784
+ } catch {
785
+ // Non-fatal
786
+ }
787
+ }
788
+
789
+ // Verify
790
+ execSync(`"${targetPath}" --help`, { timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
791
+ } catch (err) {
792
+ try {
793
+ fs.unlinkSync(targetPath);
794
+ } catch {
795
+ /* ignore */
796
+ }
797
+ const message = err instanceof Error ? err.message : String(err);
798
+ throw new AgentRelayProcessError(
799
+ `Failed to install broker binary: ${message}\n` +
800
+ 'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash'
801
+ );
802
+ }
803
+
804
+ console.log(`[agent-relay] Broker installed to ${targetPath}`);
805
+ return targetPath;
806
+ }
807
+
637
808
  function resolveDefaultBinaryPath(): string {
638
809
  const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
639
810
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -646,7 +817,17 @@ function resolveDefaultBinaryPath(): string {
646
817
  }
647
818
 
648
819
  // 2. Check for bundled broker binary in SDK package (npm install)
649
- const bundled = path.resolve(moduleDir, '..', 'bin', brokerExe);
820
+ // Try platform-specific name first (CI publishes per-platform binaries),
821
+ // then fall back to the generic name (local dev / postinstall copy).
822
+ const binDir = path.resolve(moduleDir, '..', 'bin');
823
+ const suffix = detectPlatformSuffix();
824
+ if (suffix) {
825
+ const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}`);
826
+ if (fs.existsSync(platformBinary)) {
827
+ return platformBinary;
828
+ }
829
+ }
830
+ const bundled = path.join(binDir, brokerExe);
650
831
  if (fs.existsSync(bundled)) {
651
832
  return bundled;
652
833
  }
@@ -658,6 +839,6 @@ function resolveDefaultBinaryPath(): string {
658
839
  return standaloneBroker;
659
840
  }
660
841
 
661
- // 4. Fall back to agent-relay on PATH (may be Node CLI — will fail for broker ops)
662
- return 'agent-relay';
842
+ // 4. Auto-install from GitHub releases
843
+ return installBrokerBinary();
663
844
  }
@@ -0,0 +1,306 @@
1
+ version: '1.0'
2
+ name: runner-idle-refactor
3
+ description: >
4
+ Refactors WorkflowRunner with two changes:
5
+ 1. Remove agent pre-registration (preflightAgents) — agents spawn only when their step runs.
6
+ 2. Idle = done — when no idleNudge config is set, race waitForExit vs waitForIdle;
7
+ if idle fires first the step completes immediately.
8
+
9
+ Workflow: read context → implement → update tests → type-check → run tests
10
+ → fix if broken → final test run → review.
11
+
12
+ swarm:
13
+ pattern: pipeline
14
+ maxConcurrency: 3
15
+ timeoutMs: 1800000 # 30 min
16
+ channel: wf-runner-refactor
17
+
18
+ # No idleNudge — each agent is spawned per step and exits when done.
19
+ # Non-interactive preset for all agents (pure code changes, no relay tools needed).
20
+
21
+ agents:
22
+ - name: implementer
23
+ cli: codex
24
+ preset: worker
25
+ role: 'Makes the two targeted edits to packages/sdk/src/workflows/runner.ts.'
26
+ constraints:
27
+ model: gpt-5.3-codex
28
+
29
+ - name: test-writer
30
+ cli: codex
31
+ preset: worker
32
+ role: 'Adds new test cases to idle-nudge.test.ts covering the idle=done behavior.'
33
+ constraints:
34
+ model: gpt-5.3-codex
35
+
36
+ - name: fixer
37
+ cli: codex
38
+ preset: worker
39
+ role: 'Fixes TypeScript errors or failing tests found in the test run.'
40
+ constraints:
41
+ model: gpt-5.3-codex
42
+
43
+ - name: reviewer
44
+ cli: claude
45
+ preset: reviewer
46
+ role: 'Reviews the diff for correctness, edge cases, and backwards compatibility.'
47
+ constraints:
48
+ model: sonnet
49
+
50
+ workflows:
51
+ - name: default
52
+ onError: continue
53
+
54
+ steps:
55
+ # ── Phase 1: Capture context for agents ───────────────────────────────
56
+
57
+ - name: read-prespawn-block
58
+ type: deterministic
59
+ command: >
60
+ grep -n "Pre-register all interactive\|preflightAgents\|Agent pre-registration"
61
+ packages/sdk/src/workflows/runner.ts | head -10 &&
62
+ echo "---" &&
63
+ awk '/Pre-register all interactive agent steps/,/Agent pre-registration complete/{print NR": "$0}'
64
+ packages/sdk/src/workflows/runner.ts
65
+ captureOutput: true
66
+ failOnError: false
67
+
68
+ - name: read-spawn-comment
69
+ type: deterministic
70
+ command: >
71
+ grep -n "cache.*hit\|preflightAgents\|token cache"
72
+ packages/sdk/src/workflows/runner.ts | head -10
73
+ captureOutput: true
74
+ failOnError: false
75
+
76
+ - name: read-idle-method
77
+ type: deterministic
78
+ command: >
79
+ awk '/private async waitForExitWithIdleNudging/,/^ \}$/{print NR": "$0}'
80
+ packages/sdk/src/workflows/runner.ts | head -80
81
+ captureOutput: true
82
+ failOnError: false
83
+
84
+ - name: read-test-file
85
+ type: deterministic
86
+ dependsOn: [read-prespawn-block]
87
+ command: cat packages/sdk/src/__tests__/idle-nudge.test.ts
88
+ captureOutput: true
89
+ failOnError: false
90
+
91
+ # ── Phase 2: Implement runner.ts changes ──────────────────────────────
92
+
93
+ - name: implement
94
+ type: agent
95
+ agent: implementer
96
+ dependsOn: [read-prespawn-block, read-spawn-comment, read-idle-method]
97
+ task: |
98
+ Make exactly two changes to `packages/sdk/src/workflows/runner.ts`:
99
+
100
+ ── Change 1: Remove agent pre-registration block ──
101
+
102
+ Find the block that starts with this comment and delete it entirely:
103
+ // Pre-register all interactive agent steps with Relaycast before execution.
104
+
105
+ The block ends with:
106
+ this.log('Agent pre-registration complete');
107
+ followed by the closing `}` of the if statement.
108
+
109
+ Here is what the block looks like (with line numbers for reference):
110
+ {{steps.read-prespawn-block.output}}
111
+
112
+ Also find and update the stale comment inside `spawnAndWait` that says something
113
+ about "token cache" / "cache hits" / "preflightAgents" — those references are now
114
+ stale. Keep just: "Deterministic name: step name + first 8 chars of run ID."
115
+
116
+ Stale comment location:
117
+ {{steps.read-spawn-comment.output}}
118
+
119
+ ── Change 2: Idle = done in waitForExitWithIdleNudging ──
120
+
121
+ Find the `waitForExitWithIdleNudging` method. In the branch where `nudgeConfig` is
122
+ absent, replace the simple `return agent.waitForExit(timeoutMs)` with a race between
123
+ `waitForExit` and `waitForIdle`. If idle wins, release the agent and return 'released'.
124
+
125
+ Current method (with line numbers):
126
+ {{steps.read-idle-method.output}}
127
+
128
+ Replace the no-nudge-config branch with:
129
+ ```typescript
130
+ if (!nudgeConfig) {
131
+ // Idle = done: race exit against idle. Whichever fires first completes the step.
132
+ const result = await Promise.race([
133
+ agent.waitForExit(timeoutMs).then((r) => ({ kind: 'exit' as const, result: r })),
134
+ agent.waitForIdle(timeoutMs).then((r) => ({ kind: 'idle' as const, result: r })),
135
+ ]);
136
+ if (result.kind === 'idle' && result.result === 'idle') {
137
+ this.log(`[${step.name}] Agent "${agent.name}" went idle — treating as complete`);
138
+ this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — treating as complete`);
139
+ await agent.release();
140
+ return 'released';
141
+ }
142
+ // Exit won the race, or idle returned 'exited'/'timeout' — pass through.
143
+ return result.result as 'exited' | 'timeout' | 'released';
144
+ }
145
+ ```
146
+
147
+ Only modify these two things. Do not change anything else.
148
+ verification:
149
+ type: exit_code
150
+
151
+ # ── Phase 3: Update tests ──────────────────────────────────────────────
152
+
153
+ - name: update-tests
154
+ type: agent
155
+ agent: test-writer
156
+ dependsOn: [implement, read-test-file]
157
+ task: |
158
+ Add new test cases to `packages/sdk/src/__tests__/idle-nudge.test.ts` for the
159
+ new idle=done behavior. Do NOT modify any existing tests — only append new ones.
160
+
161
+ Current test file:
162
+ {{steps.read-test-file.output}}
163
+
164
+ Background on the change:
165
+ - When `idleNudge` config is absent in the swarm config, `waitForExitWithIdleNudging`
166
+ now races `waitForExit` vs `waitForIdle`.
167
+ - If `waitForIdle` resolves with `'idle'` first → `agent.release()` is called and
168
+ the method returns `'released'`.
169
+ - If `waitForExit` resolves first → that result is returned as-is.
170
+ - If `waitForIdle` resolves with `'exited'` or `'timeout'` → exit result wins.
171
+
172
+ The mock infrastructure already has `waitForExitFn` and `waitForIdleFn` that you
173
+ can control. Add a new describe block: `'Idle = done (no idleNudge config)'` with:
174
+
175
+ Test 1 — idle fires first:
176
+ waitForIdleFn resolves 'idle' immediately
177
+ waitForExitFn never resolves (returns a never-settling promise or very long timeout)
178
+ → workflow run should complete (step succeeds, agent.release() is called)
179
+
180
+ Test 2 — exit fires first:
181
+ waitForExitFn resolves 'exited' immediately
182
+ waitForIdleFn resolves 'timeout' (or never fires before exit)
183
+ → workflow run should complete, agent.release() NOT called by idle logic
184
+
185
+ Test 3 — both timeout:
186
+ waitForExitFn resolves 'timeout'
187
+ waitForIdleFn resolves 'timeout'
188
+ → step should fail with a timeout error
189
+
190
+ Use the existing `makeConfig()` and `makeDb()` helpers. Use the existing
191
+ `WorkflowRunner` import pattern already in the file.
192
+ verification:
193
+ type: exit_code
194
+
195
+ # ── Phase 4: Type-check ────────────────────────────────────────────────
196
+
197
+ - name: type-check
198
+ type: deterministic
199
+ dependsOn: [implement, update-tests]
200
+ command: >
201
+ cd packages/sdk &&
202
+ npx tsc --noEmit 2>&1 | tail -30 &&
203
+ echo "TYPE_CHECK_PASSED" || echo "TYPE_CHECK_FAILED"
204
+ captureOutput: true
205
+ failOnError: false
206
+
207
+ # ── Phase 5: Run vitest ────────────────────────────────────────────────
208
+
209
+ - name: run-tests
210
+ type: deterministic
211
+ dependsOn: [type-check]
212
+ command: >-
213
+ cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -80
214
+ captureOutput: true
215
+ failOnError: false
216
+
217
+ # ── Phase 6: Fix failures ──────────────────────────────────────────────
218
+
219
+ - name: fix-if-broken
220
+ type: agent
221
+ agent: fixer
222
+ dependsOn: [run-tests, type-check]
223
+ task: |
224
+ Review the type-check and test results. Fix any failures.
225
+
226
+ Type-check:
227
+ {{steps.type-check.output}}
228
+
229
+ Test run:
230
+ {{steps.run-tests.output}}
231
+
232
+ If both show PASSED / ALL_TESTS_PASSED, output: FIX_DONE:none
233
+
234
+ Otherwise:
235
+ - For TypeScript errors: fix packages/sdk/src/workflows/runner.ts
236
+ - For failing tests: fix packages/sdk/src/__tests__/idle-nudge.test.ts
237
+ - Do NOT change the intended behavior — only fix syntax/type/mock issues
238
+
239
+ verification:
240
+ type: exit_code
241
+ maxIterations: 2
242
+
243
+ # ── Phase 7: Final test run ────────────────────────────────────────────
244
+
245
+ - name: final-tests
246
+ type: deterministic
247
+ dependsOn: [fix-if-broken]
248
+ command: >-
249
+ cd packages/sdk && { npx vitest run 2>&1; echo "EXIT:$?"; } | tail -60
250
+ captureOutput: true
251
+ failOnError: false
252
+
253
+ # ── Phase 8: Capture diff for review ──────────────────────────────────
254
+
255
+ - name: capture-diff
256
+ type: deterministic
257
+ dependsOn: [final-tests]
258
+ command: >
259
+ git diff packages/sdk/src/workflows/runner.ts
260
+ packages/sdk/src/__tests__/idle-nudge.test.ts
261
+ captureOutput: true
262
+ failOnError: false
263
+
264
+ # ── Phase 9: Code review ───────────────────────────────────────────────
265
+
266
+ - name: review
267
+ type: agent
268
+ agent: reviewer
269
+ dependsOn: [capture-diff, final-tests]
270
+ task: |
271
+ Review these changes to WorkflowRunner. Be precise and thorough.
272
+
273
+ Final test result:
274
+ {{steps.final-tests.output}}
275
+
276
+ Diff:
277
+ {{steps.capture-diff.output}}
278
+
279
+ Review checklist:
280
+ 1. Pre-registration block is fully gone — no leftover `preflightAgents` calls or
281
+ stale comments referencing "token cache" / "cache hits"
282
+ 2. Race logic in `waitForExitWithIdleNudging`: both promises created before awaiting?
283
+ No floating promise leak if exit wins before idle settles?
284
+ 3. Edge case: `waitForIdle` returns 'exited' (agent already gone) — does the cast
285
+ `result.result as 'exited' | 'timeout' | 'released'` hold? Check the union types.
286
+ 4. Edge case: `waitForIdle` returns 'timeout' and exit won — correct fallthrough?
287
+ 5. New tests: do they actually cover all three cases with proper mock wiring?
288
+ 6. No unintended changes to the nudge path (idleNudge config present) — that code
289
+ should be unchanged.
290
+
291
+ verification:
292
+ type: exit_code
293
+
294
+ errorHandling:
295
+ strategy: continue # best-effort — don't abort if an optional step fails
296
+ maxRetries: 1
297
+ retryDelayMs: 3000
298
+ notifyChannel: wf-runner-refactor
299
+
300
+ state:
301
+ backend: memory
302
+ ttlMs: 7200000 # 2 hours
303
+
304
+ trajectories:
305
+ enabled: true
306
+ autoDecisions: true
@@ -1,6 +1,7 @@
1
1
  export const PROTOCOL_VERSION = 1 as const;
2
2
 
3
- export type AgentRuntime = 'pty' | 'headless_claude';
3
+ export type AgentRuntime = 'pty' | 'headless';
4
+ export type HeadlessProvider = 'claude' | 'opencode';
4
5
 
5
6
  export interface RestartPolicy {
6
7
  enabled?: boolean;
@@ -12,6 +13,7 @@ export interface RestartPolicy {
12
13
  export interface AgentSpec {
13
14
  name: string;
14
15
  runtime: AgentRuntime;
16
+ provider?: HeadlessProvider;
15
17
  cli?: string;
16
18
  args?: string[];
17
19
  channels?: string[];
@@ -112,6 +114,7 @@ export interface BrokerStatus {
112
114
  agents: Array<{
113
115
  name: string;
114
116
  runtime: AgentRuntime;
117
+ provider?: HeadlessProvider;
115
118
  cli?: string;
116
119
  model?: string;
117
120
  team?: string;
@@ -180,6 +183,7 @@ export type BrokerEvent =
180
183
  kind: 'agent_spawned';
181
184
  name: string;
182
185
  runtime: AgentRuntime;
186
+ provider?: HeadlessProvider;
183
187
  cli?: string;
184
188
  model?: string;
185
189
  parent?: string;
@@ -271,6 +275,7 @@ export type BrokerEvent =
271
275
  kind: 'worker_ready';
272
276
  name: string;
273
277
  runtime: AgentRuntime;
278
+ provider?: HeadlessProvider;
274
279
  cli?: string;
275
280
  model?: string;
276
281
  }
@@ -361,7 +366,7 @@ export type BrokerToWorker =
361
366
  export type WorkerToBroker =
362
367
  | {
363
368
  type: 'worker_ready';
364
- payload: { name: string; runtime: AgentRuntime };
369
+ payload: { name: string; runtime: AgentRuntime; provider?: HeadlessProvider };
365
370
  }
366
371
  | {
367
372
  type: 'delivery_ack';