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.
- package/README.md +8 -0
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +273 -56
- package/dist/src/cli/commands/core.d.ts +2 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +9 -2
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +87 -28
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/package.json +8 -8
- package/packages/acp-bridge/README.md +50 -67
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +169 -64
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +76 -9
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/integration.test.js +5 -4
- package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +34 -3
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +120 -10
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/protocol.d.ts +7 -1
- package/packages/sdk/dist/protocol.d.ts.map +1 -1
- package/packages/sdk/dist/relay.d.ts +47 -11
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +114 -23
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +71 -36
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +1 -1
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/contract-fixtures.test.ts +88 -9
- package/packages/sdk/src/__tests__/error-scenarios.test.ts +1 -1
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
- package/packages/sdk/src/__tests__/integration.test.ts +5 -4
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +277 -13
- package/packages/sdk/src/__tests__/swarm-coordinator.test.ts +1 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +67 -7
- package/packages/sdk/src/__tests__/workflow-trajectory.test.ts +4 -5
- package/packages/sdk/src/client.ts +171 -14
- package/packages/sdk/src/examples/workflows/runner-idle-refactor.yaml +306 -0
- package/packages/sdk/src/protocol.ts +7 -2
- package/packages/sdk/src/relay.ts +196 -34
- package/packages/sdk/src/workflows/runner.ts +73 -42
- package/packages/sdk/src/workflows/schema.json +1 -1
- package/packages/sdk/src/workflows/types.ts +1 -1
- package/packages/sdk/vitest.config.ts +1 -0
- package/packages/sdk-py/README.md +89 -102
- package/packages/sdk-py/agent_relay/__init__.py +16 -19
- package/packages/sdk-py/pyproject.toml +5 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +35 -1
- package/packages/sdk-py/src/agent_relay/client.py +776 -0
- package/packages/sdk-py/src/agent_relay/models.py +27 -0
- package/packages/sdk-py/src/agent_relay/protocol.py +114 -0
- package/packages/sdk-py/src/agent_relay/relay.py +860 -0
- package/packages/sdk-py/tests/test_relay_lifecycle_hooks.py +250 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/packages/sdk/.trajectories/active/traj_1771875803391_84ca57b2.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891934534_06504121.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891957929_211afc4e.json +0 -50
- package/packages/sdk/.trajectories/active/traj_1771891982509_38c84638.json +0 -50
- package/packages/sdk/.trajectories/completed/traj_1771875803188_cd6d181c.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803204_f2aeb8c8.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803210_d65f3f1a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803218_e454a25d.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803223_d7a64815.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803227_7e56da5b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803235_4fbf93b4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803243_47931c71.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803258_3816f3fe.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803268_8061140e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875803326_ae6f9c78.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771875808396_cbde0a6c.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875812026_aa2442bb.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875815431_c2c656c5.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771875818645_3a4dbf02.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891934403_24923c03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934421_dca16e24.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934430_057706f7.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934442_faf97382.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934454_5542ecd5.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934464_12202a08.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934487_94378275.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934503_ca728c13.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934519_100af69a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934536_62ad39d9.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891934553_d6798a52.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891939537_541c8096.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891942985_36ab9a4d.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891946453_e8a6e05f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891949838_5de0de84.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891957807_0ecfb4f4.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957827_c4539239.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957836_91168b48.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957848_8c5cad0b.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957857_0986b293.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957872_8a3113af.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957884_0bb85208.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957892_86c75e2e.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957907_98ca0e6f.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957918_d9091231.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891957931_dcaf77ed.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891962931_eb1fdee2.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891966262_9061a93f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891969915_1adaba19.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891973588_f08b79e9.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891982421_f1985bce.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982432_e7a84163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982447_369b842a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982469_5fc45199.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982495_454c7cb3.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982514_08098e03.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982526_b351d778.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982533_fa542d83.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982540_18ab24dc.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982544_5b4fa163.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891982548_c13f089a.json +0 -80
- package/packages/sdk/.trajectories/completed/traj_1771891987510_23f6da1f.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891991466_912c2e04.json +0 -91
- package/packages/sdk/.trajectories/completed/traj_1771891994891_60604be2.json +0 -91
- 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
|
|
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(
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1127
|
-
|
|
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(`
|
|
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
|
-
|
|
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
|
-
//
|
|
2632
|
-
|
|
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'
|
|
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 {
|