agent-relay 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -244
- 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 +342 -60
- 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 +9 -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__/facade.test.js +48 -0
- package/packages/sdk/dist/__tests__/facade.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/integration.test.js +11 -5
- package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
- package/packages/sdk/dist/__tests__/unit.test.js +36 -0
- package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +36 -3
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +142 -9
- 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 +74 -11
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +175 -27
- 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__/facade.test.ts +68 -0
- package/packages/sdk/src/__tests__/idle-nudge.test.ts +205 -257
- package/packages/sdk/src/__tests__/integration.test.ts +11 -5
- 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__/unit.test.ts +44 -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 +195 -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 +271 -38
- 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 +6 -2
- 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/scripts/postinstall.js +35 -162
- 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
- package/packages/sdk/bin/agent-relay-broker +0 -0
|
@@ -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;
|
|
@@ -173,6 +215,17 @@ export interface AgentRelayOptions {
|
|
|
173
215
|
env?: NodeJS.ProcessEnv;
|
|
174
216
|
requestTimeoutMs?: number;
|
|
175
217
|
shutdownTimeoutMs?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Name for the auto-created Relaycast workspace.
|
|
220
|
+
* If omitted, a random name is generated.
|
|
221
|
+
* Ignored when RELAY_API_KEY is already set in env or process.env.
|
|
222
|
+
*/
|
|
223
|
+
workspaceName?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Base URL for the Relaycast API.
|
|
226
|
+
* Defaults to RELAYCAST_BASE_URL env var or https://api.relaycast.dev.
|
|
227
|
+
*/
|
|
228
|
+
relaycastBaseUrl?: string;
|
|
176
229
|
}
|
|
177
230
|
|
|
178
231
|
type OutputListener = {
|
|
@@ -195,6 +248,19 @@ export class AgentRelay {
|
|
|
195
248
|
onAgentExitRequested: EventHook<{ name: string; reason: string }> = null;
|
|
196
249
|
onAgentIdle: EventHook<{ name: string; idleSecs: number }> = null;
|
|
197
250
|
|
|
251
|
+
// ── Public accessors ────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/** The resolved Relaycast workspace API key (available after first spawn). */
|
|
254
|
+
get workspaceKey(): string | undefined {
|
|
255
|
+
return this.relayApiKey;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Observer URL for the auto-created workspace (available after first spawn). */
|
|
259
|
+
get observerUrl(): string | undefined {
|
|
260
|
+
if (!this.relayApiKey) return undefined;
|
|
261
|
+
return `https://observer.relaycast.dev/?key=${this.relayApiKey}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
198
264
|
// Shorthand spawners
|
|
199
265
|
readonly codex: AgentSpawner;
|
|
200
266
|
readonly claude: AgentSpawner;
|
|
@@ -202,6 +268,9 @@ export class AgentRelay {
|
|
|
202
268
|
|
|
203
269
|
private readonly clientOptions: AgentRelayClientOptions;
|
|
204
270
|
private readonly defaultChannels: string[];
|
|
271
|
+
private readonly workspaceName?: string;
|
|
272
|
+
private readonly relaycastBaseUrl?: string;
|
|
273
|
+
private relayApiKey?: string;
|
|
205
274
|
private client?: AgentRelayClient;
|
|
206
275
|
private startPromise?: Promise<AgentRelayClient>;
|
|
207
276
|
private unsubEvent?: () => void;
|
|
@@ -225,6 +294,8 @@ export class AgentRelay {
|
|
|
225
294
|
|
|
226
295
|
constructor(options: AgentRelayOptions = {}) {
|
|
227
296
|
this.defaultChannels = options.channels ?? ['general'];
|
|
297
|
+
this.workspaceName = options.workspaceName;
|
|
298
|
+
this.relaycastBaseUrl = options.relaycastBaseUrl;
|
|
228
299
|
this.clientOptions = {
|
|
229
300
|
binaryPath: options.binaryPath,
|
|
230
301
|
binaryArgs: options.binaryArgs,
|
|
@@ -267,7 +338,7 @@ export class AgentRelay {
|
|
|
267
338
|
|
|
268
339
|
// ── Spawning ────────────────────────────────────────────────────────────
|
|
269
340
|
|
|
270
|
-
async spawnPty(input: SpawnPtyInput): Promise<Agent> {
|
|
341
|
+
async spawnPty(input: SpawnPtyInput & SpawnLifecycleHooks): Promise<Agent> {
|
|
271
342
|
const client = await this.ensureStarted();
|
|
272
343
|
if (!input.channels || input.channels.length === 0) {
|
|
273
344
|
console.warn(
|
|
@@ -276,26 +347,52 @@ export class AgentRelay {
|
|
|
276
347
|
);
|
|
277
348
|
}
|
|
278
349
|
const channels = input.channels ?? ['general'];
|
|
279
|
-
const
|
|
350
|
+
const lifecycleContext: SpawnLifecycleContext = {
|
|
280
351
|
name: input.name,
|
|
281
352
|
cli: input.cli,
|
|
282
|
-
args: input.args,
|
|
283
353
|
channels,
|
|
284
354
|
task: input.task,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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);
|
|
297
385
|
const agent = this.makeAgent(result.name, result.runtime, channels);
|
|
298
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
|
+
);
|
|
299
396
|
return agent;
|
|
300
397
|
}
|
|
301
398
|
|
|
@@ -313,6 +410,9 @@ export class AgentRelay {
|
|
|
313
410
|
shadowMode: options?.shadowMode,
|
|
314
411
|
idleThresholdSecs: options?.idleThresholdSecs,
|
|
315
412
|
restartPolicy: options?.restartPolicy,
|
|
413
|
+
onStart: options?.onStart,
|
|
414
|
+
onSuccess: options?.onSuccess,
|
|
415
|
+
onError: options?.onError,
|
|
316
416
|
});
|
|
317
417
|
}
|
|
318
418
|
|
|
@@ -322,7 +422,7 @@ export class AgentRelay {
|
|
|
322
422
|
if (waitForMessage) {
|
|
323
423
|
return this.waitForAgentMessage(name, timeoutMs ?? 60_000);
|
|
324
424
|
}
|
|
325
|
-
return this.waitForAgentReady(name, timeoutMs ??
|
|
425
|
+
return this.waitForAgentReady(name, timeoutMs ?? 60_000);
|
|
326
426
|
}
|
|
327
427
|
|
|
328
428
|
// ── Human source ────────────────────────────────────────────────────────
|
|
@@ -551,7 +651,7 @@ export class AgentRelay {
|
|
|
551
651
|
* The agent's CLI may not yet be ready to receive messages.
|
|
552
652
|
* Use `waitForAgentMessage()` for full readiness.
|
|
553
653
|
*/
|
|
554
|
-
async waitForAgentReady(name: string, timeoutMs =
|
|
654
|
+
async waitForAgentReady(name: string, timeoutMs = 60_000): Promise<Agent> {
|
|
555
655
|
const client = await this.ensureStarted();
|
|
556
656
|
const existing = this.knownAgents.get(name);
|
|
557
657
|
if (existing && this.readyAgents.has(name)) {
|
|
@@ -750,14 +850,56 @@ export class AgentRelay {
|
|
|
750
850
|
}
|
|
751
851
|
}
|
|
752
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Ensure a Relaycast workspace API key is available.
|
|
855
|
+
* Resolution order:
|
|
856
|
+
* 1. Already resolved (cached from a previous call)
|
|
857
|
+
* 2. RELAY_API_KEY in options.env
|
|
858
|
+
* 3. RELAY_API_KEY in process.env
|
|
859
|
+
* 4. Auto-create a fresh workspace via the Relaycast REST API
|
|
860
|
+
*/
|
|
861
|
+
private async ensureRelaycastApiKey(): Promise<void> {
|
|
862
|
+
if (this.relayApiKey) return;
|
|
863
|
+
|
|
864
|
+
const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
|
|
865
|
+
if (envKey) {
|
|
866
|
+
this.relayApiKey = envKey;
|
|
867
|
+
// Ensure the broker subprocess inherits the full process env + the key.
|
|
868
|
+
// Without this, spawning with an explicit binaryPath but no env option
|
|
869
|
+
// would cause the broker to start with an empty environment (no PATH,
|
|
870
|
+
// no RELAY_API_KEY), making connect_relay() hang and triggering the
|
|
871
|
+
// hello-handshake timeout.
|
|
872
|
+
if (!this.clientOptions.env) {
|
|
873
|
+
this.clientOptions.env = { ...process.env, RELAY_API_KEY: envKey };
|
|
874
|
+
} else if (!this.clientOptions.env.RELAY_API_KEY) {
|
|
875
|
+
this.clientOptions.env.RELAY_API_KEY = envKey;
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// No API key in env — broker will create/select its own workspace.
|
|
881
|
+
// Ensure the broker process inherits the full environment (PATH, etc.)
|
|
882
|
+
// so it can connect to Relaycast. The actual workspace key will be
|
|
883
|
+
// read from the broker's hello_ack response in ensureStarted().
|
|
884
|
+
if (!this.clientOptions.env) {
|
|
885
|
+
this.clientOptions.env = { ...process.env };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
753
889
|
private async ensureStarted(): Promise<AgentRelayClient> {
|
|
754
890
|
if (this.client) return this.client;
|
|
755
891
|
if (this.startPromise) return this.startPromise;
|
|
756
892
|
|
|
757
|
-
this.startPromise =
|
|
893
|
+
this.startPromise = this.ensureRelaycastApiKey()
|
|
894
|
+
.then(() => AgentRelayClient.start(this.clientOptions))
|
|
758
895
|
.then((c) => {
|
|
759
896
|
this.client = c;
|
|
760
897
|
this.startPromise = undefined;
|
|
898
|
+
// Use the workspace key the broker actually connected with.
|
|
899
|
+
// This ensures SDK and workers are always on the same workspace.
|
|
900
|
+
if (c.workspaceKey) {
|
|
901
|
+
this.relayApiKey = c.workspaceKey;
|
|
902
|
+
}
|
|
761
903
|
this.wireEvents(c);
|
|
762
904
|
return c;
|
|
763
905
|
})
|
|
@@ -919,11 +1061,34 @@ export class AgentRelay {
|
|
|
919
1061
|
},
|
|
920
1062
|
exitCode: undefined,
|
|
921
1063
|
exitSignal: undefined,
|
|
922
|
-
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
|
+
};
|
|
923
1070
|
const client = await relay.ensureStarted();
|
|
924
|
-
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
|
+
}
|
|
925
1090
|
},
|
|
926
|
-
async waitForReady(timeoutMs =
|
|
1091
|
+
async waitForReady(timeoutMs = 60_000) {
|
|
927
1092
|
await relay.waitForAgentReady(name, timeoutMs);
|
|
928
1093
|
},
|
|
929
1094
|
waitForExit(timeoutMs?: number) {
|
|
@@ -1046,17 +1211,13 @@ export class AgentRelay {
|
|
|
1046
1211
|
private createSpawner(cli: string, defaultName: string, runtime: AgentRuntime): AgentSpawner {
|
|
1047
1212
|
return {
|
|
1048
1213
|
spawn: async (options?) => {
|
|
1049
|
-
const client = await this.ensureStarted();
|
|
1050
1214
|
const name = options?.name ?? defaultName;
|
|
1051
1215
|
const channels = options?.channels ?? ['general'];
|
|
1052
1216
|
const args = options?.args ?? [];
|
|
1053
1217
|
|
|
1054
1218
|
const task = options?.task;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
result = await client.spawnHeadlessClaude({ name, args, channels, task });
|
|
1058
|
-
} else {
|
|
1059
|
-
result = await client.spawnPty({
|
|
1219
|
+
if (runtime === 'pty') {
|
|
1220
|
+
return this.spawnPty({
|
|
1060
1221
|
name,
|
|
1061
1222
|
cli,
|
|
1062
1223
|
args,
|
|
@@ -1064,13 +1225,85 @@ export class AgentRelay {
|
|
|
1064
1225
|
task,
|
|
1065
1226
|
model: options?.model,
|
|
1066
1227
|
cwd: options?.cwd,
|
|
1228
|
+
onStart: options?.onStart,
|
|
1229
|
+
onSuccess: options?.onSuccess,
|
|
1230
|
+
onError: options?.onError,
|
|
1067
1231
|
});
|
|
1068
1232
|
}
|
|
1069
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,
|
|
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;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
this.resetAgentLifecycleState(result.name);
|
|
1070
1265
|
const agent = this.makeAgent(result.name, result.runtime, channels);
|
|
1071
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
|
+
);
|
|
1072
1276
|
return agent;
|
|
1073
1277
|
},
|
|
1074
1278
|
};
|
|
1075
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
|
+
}
|
|
1076
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 {
|