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
@@ -33,7 +33,7 @@ import {
33
33
  type SendMessageInput,
34
34
  type SpawnPtyInput,
35
35
  } from './client.js';
36
- import type { AgentRuntime, BrokerEvent, BrokerStatus, RestartPolicy } from './protocol.js';
36
+ import type { AgentRuntime, BrokerEvent, BrokerStatus, HeadlessProvider, RestartPolicy } from './protocol.js';
37
37
  import {
38
38
  followLogs as followLogsFromFile,
39
39
  getLogs as getLogsFromFile,
@@ -87,7 +87,47 @@ export interface DeliveryState {
87
87
  updatedAt: number;
88
88
  }
89
89
 
90
- export interface SpawnOptions {
90
+ export interface SpawnLifecycleContext {
91
+ name: string;
92
+ cli: string;
93
+ channels: string[];
94
+ task?: string;
95
+ }
96
+
97
+ export interface SpawnLifecycleSuccessContext extends SpawnLifecycleContext {
98
+ runtime: AgentRuntime;
99
+ }
100
+
101
+ export interface SpawnLifecycleErrorContext extends SpawnLifecycleContext {
102
+ error: unknown;
103
+ }
104
+
105
+ export interface SpawnLifecycleHooks {
106
+ onStart?: (context: SpawnLifecycleContext) => void | Promise<void>;
107
+ onSuccess?: (context: SpawnLifecycleSuccessContext) => void | Promise<void>;
108
+ onError?: (context: SpawnLifecycleErrorContext) => void | Promise<void>;
109
+ }
110
+
111
+ export interface ReleaseLifecycleContext {
112
+ name: string;
113
+ reason?: string;
114
+ }
115
+
116
+ export interface ReleaseLifecycleErrorContext extends ReleaseLifecycleContext {
117
+ error: unknown;
118
+ }
119
+
120
+ export interface ReleaseLifecycleHooks {
121
+ onStart?: (context: ReleaseLifecycleContext) => void | Promise<void>;
122
+ onSuccess?: (context: ReleaseLifecycleContext) => void | Promise<void>;
123
+ onError?: (context: ReleaseLifecycleErrorContext) => void | Promise<void>;
124
+ }
125
+
126
+ export interface ReleaseOptions extends ReleaseLifecycleHooks {
127
+ reason?: string;
128
+ }
129
+
130
+ export interface SpawnOptions extends SpawnLifecycleHooks {
91
131
  args?: string[];
92
132
  channels?: string[];
93
133
  model?: string;
@@ -119,7 +159,7 @@ export interface Agent {
119
159
  exitSignal?: string;
120
160
  /** Set when the agent requests exit via /exit. Available after `onAgentExitRequested` fires. */
121
161
  exitReason?: string;
122
- release(reason?: string): Promise<void>;
162
+ release(reasonOrOptions?: string | ReleaseOptions): Promise<void>;
123
163
  waitForReady(timeoutMs?: number): Promise<void>;
124
164
  /** Wait for the agent process to exit on its own.
125
165
  * @param timeoutMs — optional timeout in ms. Resolves with `"timeout"` if exceeded,
@@ -152,14 +192,16 @@ export interface HumanHandle {
152
192
  }
153
193
 
154
194
  export interface AgentSpawner {
155
- spawn(options?: {
156
- name?: string;
157
- args?: string[];
158
- channels?: string[];
159
- task?: string;
160
- model?: string;
161
- cwd?: string;
162
- }): Promise<Agent>;
195
+ spawn(options?: SpawnerSpawnOptions): Promise<Agent>;
196
+ }
197
+
198
+ export interface SpawnerSpawnOptions extends SpawnLifecycleHooks {
199
+ name?: string;
200
+ args?: string[];
201
+ channels?: string[];
202
+ task?: string;
203
+ model?: string;
204
+ cwd?: string;
163
205
  }
164
206
 
165
207
  export type EventHook<T> = ((value: T) => void) | null;
@@ -296,7 +338,7 @@ export class AgentRelay {
296
338
 
297
339
  // ── Spawning ────────────────────────────────────────────────────────────
298
340
 
299
- async spawnPty(input: SpawnPtyInput): Promise<Agent> {
341
+ async spawnPty(input: SpawnPtyInput & SpawnLifecycleHooks): Promise<Agent> {
300
342
  const client = await this.ensureStarted();
301
343
  if (!input.channels || input.channels.length === 0) {
302
344
  console.warn(
@@ -305,26 +347,52 @@ export class AgentRelay {
305
347
  );
306
348
  }
307
349
  const channels = input.channels ?? ['general'];
308
- const result = await client.spawnPty({
350
+ const lifecycleContext: SpawnLifecycleContext = {
309
351
  name: input.name,
310
352
  cli: input.cli,
311
- args: input.args,
312
353
  channels,
313
354
  task: input.task,
314
- model: input.model,
315
- cwd: input.cwd,
316
- team: input.team,
317
- shadowOf: input.shadowOf,
318
- shadowMode: input.shadowMode,
319
- idleThresholdSecs: input.idleThresholdSecs,
320
- restartPolicy: input.restartPolicy,
321
- });
322
- this.readyAgents.delete(result.name);
323
- this.messageReadyAgents.delete(result.name);
324
- this.exitedAgents.delete(result.name);
325
- this.idleAgents.delete(result.name);
355
+ };
356
+ await this.invokeLifecycleHook(input.onStart, lifecycleContext, `spawnPty("${input.name}") onStart`);
357
+ let result: { name: string; runtime: AgentRuntime };
358
+ try {
359
+ result = await client.spawnPty({
360
+ name: input.name,
361
+ cli: input.cli,
362
+ args: input.args,
363
+ channels,
364
+ task: input.task,
365
+ model: input.model,
366
+ cwd: input.cwd,
367
+ team: input.team,
368
+ shadowOf: input.shadowOf,
369
+ shadowMode: input.shadowMode,
370
+ idleThresholdSecs: input.idleThresholdSecs,
371
+ restartPolicy: input.restartPolicy,
372
+ });
373
+ } catch (error) {
374
+ await this.invokeLifecycleHook(
375
+ input.onError,
376
+ {
377
+ ...lifecycleContext,
378
+ error,
379
+ },
380
+ `spawnPty("${input.name}") onError`
381
+ );
382
+ throw error;
383
+ }
384
+ this.resetAgentLifecycleState(result.name);
326
385
  const agent = this.makeAgent(result.name, result.runtime, channels);
327
386
  this.knownAgents.set(agent.name, agent);
387
+ await this.invokeLifecycleHook(
388
+ input.onSuccess,
389
+ {
390
+ ...lifecycleContext,
391
+ name: result.name,
392
+ runtime: result.runtime,
393
+ },
394
+ `spawnPty("${input.name}") onSuccess`
395
+ );
328
396
  return agent;
329
397
  }
330
398
 
@@ -342,6 +410,9 @@ export class AgentRelay {
342
410
  shadowMode: options?.shadowMode,
343
411
  idleThresholdSecs: options?.idleThresholdSecs,
344
412
  restartPolicy: options?.restartPolicy,
413
+ onStart: options?.onStart,
414
+ onSuccess: options?.onSuccess,
415
+ onError: options?.onError,
345
416
  });
346
417
  }
347
418
 
@@ -990,9 +1061,32 @@ export class AgentRelay {
990
1061
  },
991
1062
  exitCode: undefined,
992
1063
  exitSignal: undefined,
993
- async release(reason?: string) {
1064
+ async release(reasonOrOptions?: string | ReleaseOptions) {
1065
+ const releaseOptions = relay.normalizeReleaseOptions(reasonOrOptions);
1066
+ const releaseContext: ReleaseLifecycleContext = {
1067
+ name,
1068
+ reason: releaseOptions.reason,
1069
+ };
994
1070
  const client = await relay.ensureStarted();
995
- await client.release(name, reason);
1071
+ await relay.invokeLifecycleHook(releaseOptions.onStart, releaseContext, `release("${name}") onStart`);
1072
+ try {
1073
+ await client.release(name, releaseOptions.reason);
1074
+ await relay.invokeLifecycleHook(
1075
+ releaseOptions.onSuccess,
1076
+ releaseContext,
1077
+ `release("${name}") onSuccess`
1078
+ );
1079
+ } catch (error) {
1080
+ await relay.invokeLifecycleHook(
1081
+ releaseOptions.onError,
1082
+ {
1083
+ ...releaseContext,
1084
+ error,
1085
+ },
1086
+ `release("${name}") onError`
1087
+ );
1088
+ throw error;
1089
+ }
996
1090
  },
997
1091
  async waitForReady(timeoutMs = 60_000) {
998
1092
  await relay.waitForAgentReady(name, timeoutMs);
@@ -1117,17 +1211,13 @@ export class AgentRelay {
1117
1211
  private createSpawner(cli: string, defaultName: string, runtime: AgentRuntime): AgentSpawner {
1118
1212
  return {
1119
1213
  spawn: async (options?) => {
1120
- const client = await this.ensureStarted();
1121
1214
  const name = options?.name ?? defaultName;
1122
1215
  const channels = options?.channels ?? ['general'];
1123
1216
  const args = options?.args ?? [];
1124
1217
 
1125
1218
  const task = options?.task;
1126
- let result: { name: string; runtime: AgentRuntime };
1127
- if (runtime === 'headless_claude') {
1128
- result = await client.spawnHeadlessClaude({ name, args, channels, task });
1129
- } else {
1130
- result = await client.spawnPty({
1219
+ if (runtime === 'pty') {
1220
+ return this.spawnPty({
1131
1221
  name,
1132
1222
  cli,
1133
1223
  args,
@@ -1135,13 +1225,85 @@ export class AgentRelay {
1135
1225
  task,
1136
1226
  model: options?.model,
1137
1227
  cwd: options?.cwd,
1228
+ onStart: options?.onStart,
1229
+ onSuccess: options?.onSuccess,
1230
+ onError: options?.onError,
1231
+ });
1232
+ }
1233
+
1234
+ const client = await this.ensureStarted();
1235
+ const lifecycleContext: SpawnLifecycleContext = {
1236
+ name,
1237
+ cli,
1238
+ channels,
1239
+ task,
1240
+ };
1241
+ await this.invokeLifecycleHook(options?.onStart, lifecycleContext, `spawn("${name}") onStart`);
1242
+ let result: { name: string; runtime: AgentRuntime };
1243
+ try {
1244
+ result = await client.spawnProvider({
1245
+ name,
1246
+ provider: cli as HeadlessProvider,
1247
+ transport: 'headless',
1248
+ args,
1249
+ channels,
1250
+ task,
1138
1251
  });
1252
+ } catch (error) {
1253
+ await this.invokeLifecycleHook(
1254
+ options?.onError,
1255
+ {
1256
+ ...lifecycleContext,
1257
+ error,
1258
+ },
1259
+ `spawn("${name}") onError`
1260
+ );
1261
+ throw error;
1139
1262
  }
1140
1263
 
1264
+ this.resetAgentLifecycleState(result.name);
1141
1265
  const agent = this.makeAgent(result.name, result.runtime, channels);
1142
1266
  this.knownAgents.set(agent.name, agent);
1267
+ await this.invokeLifecycleHook(
1268
+ options?.onSuccess,
1269
+ {
1270
+ ...lifecycleContext,
1271
+ name: result.name,
1272
+ runtime: result.runtime,
1273
+ },
1274
+ `spawn("${name}") onSuccess`
1275
+ );
1143
1276
  return agent;
1144
1277
  },
1145
1278
  };
1146
1279
  }
1280
+
1281
+ private async invokeLifecycleHook<T>(
1282
+ hook: ((context: T) => void | Promise<void>) | undefined,
1283
+ context: T,
1284
+ label: string
1285
+ ): Promise<void> {
1286
+ if (!hook) {
1287
+ return;
1288
+ }
1289
+ try {
1290
+ await hook(context);
1291
+ } catch (error) {
1292
+ console.warn(`[AgentRelay] ${label} hook threw`, error);
1293
+ }
1294
+ }
1295
+
1296
+ private resetAgentLifecycleState(name: string): void {
1297
+ this.readyAgents.delete(name);
1298
+ this.messageReadyAgents.delete(name);
1299
+ this.exitedAgents.delete(name);
1300
+ this.idleAgents.delete(name);
1301
+ }
1302
+
1303
+ private normalizeReleaseOptions(reasonOrOptions?: string | ReleaseOptions): ReleaseOptions {
1304
+ if (typeof reasonOrOptions === 'string' || reasonOrOptions === undefined) {
1305
+ return { reason: reasonOrOptions };
1306
+ }
1307
+ return reasonOrOptions;
1308
+ }
1147
1309
  }
@@ -4,7 +4,7 @@
4
4
  * persists state to DB, and supports pause/resume/abort with retries.
5
5
  */
6
6
 
7
- import { spawn as cpSpawn } from 'node:child_process';
7
+ import { spawn as cpSpawn, execFileSync } from 'node:child_process';
8
8
  import { randomBytes } from 'node:crypto';
9
9
  import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
10
10
  import type { WriteStream } from 'node:fs';
@@ -103,6 +103,31 @@ interface StepState {
103
103
  agent?: Agent;
104
104
  }
105
105
 
106
+ // ── CLI resolution ───────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Resolve `cursor` to the concrete cursor agent binary available in PATH.
110
+ * Prefers `cursor-agent` over `agent`. Falls back to `agent` if neither
111
+ * `cursor-agent` nor a real cursor IDE CLI is found.
112
+ * Result is memoized after the first call to avoid repeated sync PATH lookups.
113
+ */
114
+ let _resolvedCursorCli: 'cursor-agent' | 'agent' | undefined;
115
+ function resolveCursorCli(): 'cursor-agent' | 'agent' {
116
+ if (_resolvedCursorCli !== undefined) return _resolvedCursorCli;
117
+ const candidates: Array<'cursor-agent' | 'agent'> = ['cursor-agent', 'agent'];
118
+ for (const candidate of candidates) {
119
+ try {
120
+ execFileSync('which', [candidate], { stdio: 'ignore' });
121
+ _resolvedCursorCli = candidate;
122
+ return candidate;
123
+ } catch {
124
+ // not in PATH, try next
125
+ }
126
+ }
127
+ _resolvedCursorCli = 'agent'; // last-resort default
128
+ return _resolvedCursorCli;
129
+ }
130
+
106
131
  // ── WorkflowRunner ──────────────────────────────────────────────────────────
107
132
 
108
133
  export class WorkflowRunner {
@@ -223,6 +248,21 @@ export class WorkflowRunner {
223
248
 
224
249
  this.relayApiKey = apiKey;
225
250
  this.relayApiKeyAutoCreated = true;
251
+
252
+ // Best-effort: push the key to a co-running dashboard (agent-relay up) so it
253
+ // can make Relaycast API calls without any file or manual env var setup.
254
+ const dashboardPort = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
255
+ fetch(`http://127.0.0.1:${dashboardPort}/api/relay-config`, {
256
+ method: 'POST',
257
+ headers: { 'content-type': 'application/json' },
258
+ body: JSON.stringify({ apiKey }),
259
+ }).then((res) => {
260
+ if (!res.ok) {
261
+ console.warn(`[WorkflowRunner] dashboard key push failed: HTTP ${res.status}`);
262
+ }
263
+ }).catch(() => {
264
+ // Dashboard not running — silently ignore.
265
+ });
226
266
  }
227
267
 
228
268
  private getRelayEnv(): NodeJS.ProcessEnv | undefined {
@@ -1102,8 +1142,7 @@ export class WorkflowRunner {
1102
1142
  this.log('API key resolved');
1103
1143
  if (this.relayApiKeyAutoCreated && this.relayApiKey) {
1104
1144
  this.log(`Workspace created — follow this run in Relaycast:`);
1105
- this.log(` RELAY_API_KEY=${this.relayApiKey}`);
1106
- this.log(` Observer: https://observer.relaycast.dev (paste key above)`);
1145
+ this.log(` Observer: https://observer.relaycast.dev/?key=${this.relayApiKey}`);
1107
1146
  this.log(` Channel: ${channel}`);
1108
1147
  }
1109
1148
 
@@ -1237,30 +1276,6 @@ export class WorkflowRunner {
1237
1276
  await this.runPreflightChecks(workflow.preflight, runId);
1238
1277
  }
1239
1278
 
1240
- // Pre-register all interactive agent steps with Relaycast before execution.
1241
- // This warms the broker's token cache so spawn_agent calls are instant cache
1242
- // hits rather than blocking on individual HTTP registrations per spawn.
1243
- // Agent names use the run ID prefix (deterministic) so we can predict them.
1244
- if (this.relay && !isResume) {
1245
- const agentPreflight = workflow.steps
1246
- .filter((s) => s.type !== 'deterministic' && s.type !== 'worktree' && s.agent)
1247
- .map((s) => {
1248
- const agentDef = agentMap.get(s.agent!);
1249
- return agentDef && agentDef.interactive !== false
1250
- ? { name: `${s.name}-${runId.slice(0, 8)}`, cli: agentDef.cli }
1251
- : null;
1252
- })
1253
- .filter((e): e is { name: string; cli: AgentCli } => e !== null);
1254
-
1255
- if (agentPreflight.length > 0) {
1256
- this.log(`Pre-registering ${agentPreflight.length} agents with Relaycast...`);
1257
- await this.relay.preflightAgents(agentPreflight).catch((err: Error) => {
1258
- this.log(`[preflight-agents] warning: ${err.message} — continuing without pre-registration`);
1259
- });
1260
- this.log('Agent pre-registration complete');
1261
- }
1262
- }
1263
-
1264
1279
  this.log(`Executing ${workflow.steps.length} steps (pattern: ${config.swarm.pattern})`);
1265
1280
  await this.executeSteps(workflow, stepStates, agentMap, config.errorHandling, runId);
1266
1281
 
@@ -1785,9 +1800,6 @@ export class WorkflowRunner {
1785
1800
  await this.persistStepOutput(runId, step.name, output);
1786
1801
 
1787
1802
  this.emit({ type: 'step:completed', runId, stepName: step.name, output });
1788
- this.postToChannel(
1789
- `**[${step.name}]** Completed (deterministic)\n${output.slice(0, 500)}${output.length > 500 ? '\n...(truncated)' : ''}`
1790
- );
1791
1803
  } catch (err) {
1792
1804
  const errorMsg = err instanceof Error ? err.message : String(err);
1793
1805
  this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
@@ -2081,9 +2093,6 @@ export class WorkflowRunner {
2081
2093
  await this.persistStepOutput(runId, step.name, output);
2082
2094
 
2083
2095
  this.emit({ type: 'step:completed', runId, stepName: step.name, output });
2084
- this.postToChannel(
2085
- `**[${step.name}]** Completed\n${output.slice(0, 500)}${output.length > 500 ? '\n...(truncated)' : ''}`
2086
- );
2087
2096
  await this.trajectory?.stepCompleted(step, output, attempt + 1);
2088
2097
  return;
2089
2098
  } catch (err) {
@@ -2136,6 +2145,13 @@ export class WorkflowRunner {
2136
2145
  return { cmd: 'aider', args: ['--message', task, '--yes-always', '--no-git', ...extraArgs] };
2137
2146
  case 'goose':
2138
2147
  return { cmd: 'goose', args: ['run', '--text', task, '--no-session', ...extraArgs] };
2148
+ case 'cursor-agent':
2149
+ case 'agent':
2150
+ return { cmd: cli, args: ['--force', '-p', task, ...extraArgs] };
2151
+ case 'cursor':
2152
+ // Should not reach here after resolveAgentDef resolves to agent/cursor-agent,
2153
+ // but handle as fallback.
2154
+ return { cmd: resolveCursorCli(), args: ['--force', '-p', task, ...extraArgs] };
2139
2155
  }
2140
2156
  }
2141
2157
 
@@ -2144,13 +2160,16 @@ export class WorkflowRunner {
2144
2160
  * Explicit fields on the definition always win over preset-inferred defaults.
2145
2161
  */
2146
2162
  private static resolveAgentDef(def: AgentDefinition): AgentDefinition {
2147
- if (!def.preset) return def;
2163
+ // Resolve "cursor" alias to whichever cursor agent binary is in PATH
2164
+ const resolvedCli: AgentCli = def.cli === 'cursor' ? resolveCursorCli() : def.cli;
2165
+
2166
+ if (!def.preset) return resolvedCli !== def.cli ? { ...def, cli: resolvedCli } : def;
2148
2167
  const nonInteractivePresets: AgentPreset[] = ['worker', 'reviewer', 'analyst'];
2149
2168
  const defaults: Partial<AgentDefinition> = nonInteractivePresets.includes(def.preset)
2150
2169
  ? { interactive: false }
2151
2170
  : {};
2152
2171
  // Explicit fields on the def always win
2153
- return { ...defaults, ...def } as AgentDefinition;
2172
+ return { ...defaults, ...def, cli: resolvedCli } as AgentDefinition;
2154
2173
  }
2155
2174
 
2156
2175
  /**
@@ -2386,10 +2405,6 @@ export class WorkflowRunner {
2386
2405
  }
2387
2406
 
2388
2407
  // Deterministic name: step name + first 8 chars of run ID.
2389
- // This matches the names pre-registered in preflightAgents(), so the broker
2390
- // hits its token cache instantly instead of making a fresh Relaycast HTTP call.
2391
- // On retry the broker may suffix a UUID (409 conflict) — that's fine, the agent
2392
- // still works, just without the cache benefit.
2393
2408
  let agentName = `${step.name}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
2394
2409
 
2395
2410
  // Only inject delegation guidance for lead/coordinator agents, not spokes/workers.
@@ -2628,8 +2643,19 @@ export class WorkflowRunner {
2628
2643
  ): Promise<'exited' | 'timeout' | 'released'> {
2629
2644
  const nudgeConfig = this.currentConfig?.swarm.idleNudge;
2630
2645
  if (!nudgeConfig) {
2631
- // No nudge config backward compatible simple wait
2632
- return agent.waitForExit(timeoutMs);
2646
+ // Idle = done: race exit against idle. Whichever fires first completes the step.
2647
+ const result = await Promise.race([
2648
+ agent.waitForExit(timeoutMs).then((r) => ({ kind: 'exit' as const, result: r })),
2649
+ agent.waitForIdle(timeoutMs).then((r) => ({ kind: 'idle' as const, result: r })),
2650
+ ]);
2651
+ if (result.kind === 'idle' && result.result === 'idle') {
2652
+ this.log(`[${step.name}] Agent "${agent.name}" went idle — treating as complete`);
2653
+ this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — treating as complete`);
2654
+ await agent.release();
2655
+ return 'released';
2656
+ }
2657
+ // Exit won the race, or idle returned 'exited'/'timeout' — pass through.
2658
+ return result.result as 'exited' | 'timeout' | 'released';
2633
2659
  }
2634
2660
 
2635
2661
  const nudgeAfterMs = nudgeConfig.nudgeAfterMs ?? 120_000;
@@ -3209,7 +3235,8 @@ export class WorkflowRunner {
3209
3235
  // Includes block-element chars (▗▖▘▝) used in the Claude Code header bar.
3210
3236
  const SPINNER =
3211
3237
  '\\u2756\\u2738\\u2739\\u273a\\u273b\\u273c\\u273d\\u2731\\u2732\\u2733\\u2734\\u2735\\u2736\\u2737\\u2743\\u2745\\u2746\\u25d6\\u25d7\\u25d8\\u25d9\\u2022\\u25cf\\u25cb\\u25a0\\u25a1\\u25b6\\u25c0\\u23f5\\u23f6\\u23f7\\u23f8\\u23f9\\u25e2\\u25e3\\u25e4\\u25e5\\u2597\\u2596\\u2598\\u259d\\u2bc8\\u2bc7\\u2bc5\\u2bc6\\u00b7' +
3212
- '\\u2590\\u258c\\u2588\\u2584\\u2580\\u259a\\u259e'; // additional block elements
3238
+ '\\u2590\\u258c\\u2588\\u2584\\u2580\\u259a\\u259e' + // additional block elements
3239
+ '\\u2b21\\u2b22'; // hex-hollow ⬡ and hex-filled ⬢ (Cursor "Generating" spinner)
3213
3240
  const spinnerRe = new RegExp(`[${SPINNER}]`, 'gu');
3214
3241
  const spinnerClassRe = new RegExp(`^[\\s${SPINNER}]*$`, 'u');
3215
3242
 
@@ -3227,6 +3254,9 @@ export class WorkflowRunner {
3227
3254
  // regardless of the specific word used (Thinking, Cascading, Flibbertigibbeting, etc.)
3228
3255
  const thinkingLineRe = new RegExp(`^[\\s${SPINNER}]*\\s*\\w[\\w\\s]*\\u2026\\s*$`, 'u');
3229
3256
  const cursorOnlyRe = /^[\s❯⎿›»◀▶←→↑↓⟨⟩⟪⟫·]+$/u;
3257
+ // Cursor Agent TUI lines: generating animations, pasted text indicators, UI chrome
3258
+ const cursorAgentRe =
3259
+ /^(?:Cursor Agent|[\s⬡⬢]*Generating[.\s]|\[Pasted text|Auto-run all|Add a follow-up|ctrl\+c to stop|shift\+tab|Auto$|\/\s*commands|@\s*files|!\s*shell|follow-ups?\s|The user ha)/iu;
3230
3260
  const slashCommandRe = /^\/\w+\s*$/u;
3231
3261
  const mcpJsonKvRe =
3232
3262
  /^\s*"(?:type|method|params|result|id|jsonrpc|tool|name|arguments|content|role|metadata)"\s*:/u;
@@ -3270,6 +3300,7 @@ export class WorkflowRunner {
3270
3300
  if (uiHintRe.test(trimmed)) continue;
3271
3301
  if (thinkingLineRe.test(trimmed)) continue;
3272
3302
  if (cursorOnlyRe.test(trimmed)) continue;
3303
+ if (cursorAgentRe.test(trimmed)) continue;
3273
3304
  if (slashCommandRe.test(trimmed)) continue;
3274
3305
  if (!meaningfulContentRe.test(trimmed)) continue;
3275
3306
 
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "AgentCli": {
178
178
  "type": "string",
179
- "enum": ["claude", "codex", "gemini", "aider", "goose", "opencode", "droid"]
179
+ "enum": ["claude", "codex", "gemini", "aider", "goose", "opencode", "droid", "cursor", "cursor-agent", "agent"]
180
180
  },
181
181
  "AgentConstraints": {
182
182
  "type": "object",
@@ -115,7 +115,7 @@ export interface AgentDefinition {
115
115
  preset?: AgentPreset;
116
116
  }
117
117
 
118
- export type AgentCli = 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'opencode' | 'droid';
118
+ export type AgentCli = 'claude' | 'codex' | 'gemini' | 'aider' | 'goose' | 'opencode' | 'droid' | 'cursor' | 'cursor-agent' | 'agent';
119
119
 
120
120
  /** Resource and behavioral constraints for an agent. */
121
121
  export interface AgentConstraints {
@@ -5,5 +5,6 @@ export default defineConfig({
5
5
  globals: true,
6
6
  environment: 'node',
7
7
  include: ['src/__tests__/**/*.test.ts'],
8
+ exclude: ['src/__tests__/unit.test.ts'],
8
9
  },
9
10
  });