agent-relay 3.0.2 → 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 (134) hide show
  1. package/README.md +8 -0
  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 +273 -56
  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 +8 -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__/integration.test.js +5 -4
  25. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  26. package/packages/sdk/dist/client.d.ts +34 -3
  27. package/packages/sdk/dist/client.d.ts.map +1 -1
  28. package/packages/sdk/dist/client.js +120 -10
  29. package/packages/sdk/dist/client.js.map +1 -1
  30. package/packages/sdk/dist/protocol.d.ts +7 -1
  31. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  32. package/packages/sdk/dist/relay.d.ts +47 -11
  33. package/packages/sdk/dist/relay.d.ts.map +1 -1
  34. package/packages/sdk/dist/relay.js +114 -23
  35. package/packages/sdk/dist/relay.js.map +1 -1
  36. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  37. package/packages/sdk/dist/workflows/runner.js +71 -36
  38. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  39. package/packages/sdk/dist/workflows/types.d.ts +1 -1
  40. package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
  41. package/packages/sdk/package.json +2 -2
  42. package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
  43. package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
  44. package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
  45. package/packages/sdk/src/__tests__/integration.test.ts +5 -4
  46. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
  47. package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
  48. package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
  49. package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
  50. package/packages/sdk/src/client.ts +171 -14
  51. package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
  52. package/packages/sdk/src/protocol.ts +7 -2
  53. package/packages/sdk/src/relay.ts +196 -34
  54. package/packages/sdk/src/workflows/runner.ts +73 -42
  55. package/packages/sdk/src/workflows/schema.json +1 -1
  56. package/packages/sdk/src/workflows/types.ts +1 -1
  57. package/packages/sdk/vitest.config.ts +1 -0
  58. package/packages/sdk-py/README.md +89 -102
  59. package/packages/sdk-py/agent_relay/__init__.py +16 -19
  60. package/packages/sdk-py/pyproject.toml +5 -1
  61. package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
  62. package/packages/sdk-py/src/agent_relay/client.py +776 -0
  63. package/packages/sdk-py/src/agent_relay/models.py +27 -0
  64. package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
  65. package/packages/sdk-py/src/agent_relay/relay.py +860 -0
  66. package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
  67. package/packages/telemetry/package.json +1 -1
  68. package/packages/trajectory/package.json +2 -2
  69. package/packages/user-directory/package.json +2 -2
  70. package/packages/utils/package.json +2 -2
  71. package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
  72. package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
  73. package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
  74. package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
  75. package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
  76. package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
  77. package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
  78. package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
  79. package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
  80. package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
  81. package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
  82. package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
  83. package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
  84. package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
  85. package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
  86. package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
  87. package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
  88. package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
  89. package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
  90. package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
  91. package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
  92. package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
  93. package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
  94. package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
  95. package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
  96. package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
  97. package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
  98. package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
  99. package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
  100. package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
  101. package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
  102. package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
  103. package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
  104. package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
  105. package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
  106. package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
  107. package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
  108. package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
  109. package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
  110. package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
  111. package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
  112. package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
  113. package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
  114. package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
  115. package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
  116. package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
  117. package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
  118. package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
  119. package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
  120. package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
  121. package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
  122. package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
  123. package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
  124. package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
  125. package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
  126. package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
  127. package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
  128. package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
  129. package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
  130. package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
  131. package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
  132. package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
  133. package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
  134. package/packages/sdk/.trajectories/completed/traj_1771891998370_cfaf9b8b.json +0 -91
@@ -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;
@@ -250,13 +276,12 @@ export class AgentRelayClient {
250
276
  return result;
251
277
  }
252
278
 
253
- async spawnHeadlessClaude(
254
- input: SpawnHeadlessClaudeInput
255
- ): Promise<{ name: string; runtime: AgentRuntime }> {
279
+ async spawnHeadless(input: SpawnHeadlessInput): Promise<{ name: string; runtime: AgentRuntime }> {
256
280
  await this.start();
257
281
  const agent: AgentSpec = {
258
282
  name: input.name,
259
- runtime: 'headless_claude',
283
+ runtime: 'headless',
284
+ provider: input.provider,
260
285
  args: input.args ?? [],
261
286
  channels: input.channels ?? [],
262
287
  };
@@ -267,6 +292,52 @@ export class AgentRelayClient {
267
292
  return result;
268
293
  }
269
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
+
270
341
  async release(name: string, reason?: string): Promise<{ name: string }> {
271
342
  await this.start();
272
343
  return this.requestOk<{ name: string }>('release_agent', { name, reason });
@@ -533,7 +604,11 @@ export class AgentRelayClient {
533
604
  pending.resolve(envelope);
534
605
  }
535
606
 
536
- private async requestHello(): Promise<{ broker_version: string; protocol_version: number; workspace_key?: string }> {
607
+ private async requestHello(): Promise<{
608
+ broker_version: string;
609
+ protocol_version: number;
610
+ workspace_key?: string;
611
+ }> {
537
612
  const payload = {
538
613
  client_name: this.options.clientName,
539
614
  client_version: this.options.clientVersion,
@@ -644,6 +719,92 @@ function isExplicitPath(binaryPath: string): boolean {
644
719
  );
645
720
  }
646
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
+
647
808
  function resolveDefaultBinaryPath(): string {
648
809
  const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
649
810
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -659,11 +820,7 @@ function resolveDefaultBinaryPath(): string {
659
820
  // Try platform-specific name first (CI publishes per-platform binaries),
660
821
  // then fall back to the generic name (local dev / postinstall copy).
661
822
  const binDir = path.resolve(moduleDir, '..', 'bin');
662
- const platformMap: Record<string, Record<string, string>> = {
663
- darwin: { arm64: 'darwin-arm64', x64: 'darwin-x64' },
664
- linux: { arm64: 'linux-arm64', x64: 'linux-x64' },
665
- };
666
- const suffix = platformMap[process.platform]?.[process.arch];
823
+ const suffix = detectPlatformSuffix();
667
824
  if (suffix) {
668
825
  const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}`);
669
826
  if (fs.existsSync(platformBinary)) {
@@ -682,6 +839,6 @@ function resolveDefaultBinaryPath(): string {
682
839
  return standaloneBroker;
683
840
  }
684
841
 
685
- // 4. Fall back to agent-relay on PATH (may be Node CLI — will fail for broker ops)
686
- return 'agent-relay';
842
+ // 4. Auto-install from GitHub releases
843
+ return installBrokerBinary();
687
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';