@agent-relay/sdk 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.
@@ -3,7 +3,7 @@
3
3
  * executes steps (sequential/parallel/DAG), runs verification checks,
4
4
  * persists state to DB, and supports pause/resume/abort with retries.
5
5
  */
6
- import { spawn as cpSpawn } from 'node:child_process';
6
+ import { spawn as cpSpawn, execFileSync } from 'node:child_process';
7
7
  import { randomBytes } from 'node:crypto';
8
8
  import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
9
9
  import { readFile, writeFile } from 'node:fs/promises';
@@ -17,6 +17,31 @@ import { WorkflowTrajectory } from './trajectory.js';
17
17
  // Import from sub-paths to avoid pulling in the full @relaycast/sdk dependency.
18
18
  import { AgentRelay } from '../relay.js';
19
19
  import { RelayCast, RelayError } from '@relaycast/sdk';
20
+ // ── CLI resolution ───────────────────────────────────────────────────────────
21
+ /**
22
+ * Resolve `cursor` to the concrete cursor agent binary available in PATH.
23
+ * Prefers `cursor-agent` over `agent`. Falls back to `agent` if neither
24
+ * `cursor-agent` nor a real cursor IDE CLI is found.
25
+ * Result is memoized after the first call to avoid repeated sync PATH lookups.
26
+ */
27
+ let _resolvedCursorCli;
28
+ function resolveCursorCli() {
29
+ if (_resolvedCursorCli !== undefined)
30
+ return _resolvedCursorCli;
31
+ const candidates = ['cursor-agent', 'agent'];
32
+ for (const candidate of candidates) {
33
+ try {
34
+ execFileSync('which', [candidate], { stdio: 'ignore' });
35
+ _resolvedCursorCli = candidate;
36
+ return candidate;
37
+ }
38
+ catch {
39
+ // not in PATH, try next
40
+ }
41
+ }
42
+ _resolvedCursorCli = 'agent'; // last-resort default
43
+ return _resolvedCursorCli;
44
+ }
20
45
  // ── WorkflowRunner ──────────────────────────────────────────────────────────
21
46
  export class WorkflowRunner {
22
47
  db;
@@ -118,6 +143,20 @@ export class WorkflowRunner {
118
143
  }
119
144
  this.relayApiKey = apiKey;
120
145
  this.relayApiKeyAutoCreated = true;
146
+ // Best-effort: push the key to a co-running dashboard (agent-relay up) so it
147
+ // can make Relaycast API calls without any file or manual env var setup.
148
+ const dashboardPort = process.env.AGENT_RELAY_DASHBOARD_PORT || '3888';
149
+ fetch(`http://127.0.0.1:${dashboardPort}/api/relay-config`, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json' },
152
+ body: JSON.stringify({ apiKey }),
153
+ }).then((res) => {
154
+ if (!res.ok) {
155
+ console.warn(`[WorkflowRunner] dashboard key push failed: HTTP ${res.status}`);
156
+ }
157
+ }).catch(() => {
158
+ // Dashboard not running — silently ignore.
159
+ });
121
160
  }
122
161
  getRelayEnv() {
123
162
  if (!this.relayApiKey) {
@@ -866,8 +905,7 @@ export class WorkflowRunner {
866
905
  this.log('API key resolved');
867
906
  if (this.relayApiKeyAutoCreated && this.relayApiKey) {
868
907
  this.log(`Workspace created — follow this run in Relaycast:`);
869
- this.log(` RELAY_API_KEY=${this.relayApiKey}`);
870
- this.log(` Observer: https://observer.relaycast.dev (paste key above)`);
908
+ this.log(` Observer: https://observer.relaycast.dev/?key=${this.relayApiKey}`);
871
909
  this.log(` Channel: ${channel}`);
872
910
  }
873
911
  this.log('Starting broker...');
@@ -995,28 +1033,6 @@ export class WorkflowRunner {
995
1033
  if (!isResume && workflow.preflight?.length) {
996
1034
  await this.runPreflightChecks(workflow.preflight, runId);
997
1035
  }
998
- // Pre-register all interactive agent steps with Relaycast before execution.
999
- // This warms the broker's token cache so spawn_agent calls are instant cache
1000
- // hits rather than blocking on individual HTTP registrations per spawn.
1001
- // Agent names use the run ID prefix (deterministic) so we can predict them.
1002
- if (this.relay && !isResume) {
1003
- const agentPreflight = workflow.steps
1004
- .filter((s) => s.type !== 'deterministic' && s.type !== 'worktree' && s.agent)
1005
- .map((s) => {
1006
- const agentDef = agentMap.get(s.agent);
1007
- return agentDef && agentDef.interactive !== false
1008
- ? { name: `${s.name}-${runId.slice(0, 8)}`, cli: agentDef.cli }
1009
- : null;
1010
- })
1011
- .filter((e) => e !== null);
1012
- if (agentPreflight.length > 0) {
1013
- this.log(`Pre-registering ${agentPreflight.length} agents with Relaycast...`);
1014
- await this.relay.preflightAgents(agentPreflight).catch((err) => {
1015
- this.log(`[preflight-agents] warning: ${err.message} — continuing without pre-registration`);
1016
- });
1017
- this.log('Agent pre-registration complete');
1018
- }
1019
- }
1020
1036
  this.log(`Executing ${workflow.steps.length} steps (pattern: ${config.swarm.pattern})`);
1021
1037
  await this.executeSteps(workflow, stepStates, agentMap, config.errorHandling, runId);
1022
1038
  const allCompleted = [...stepStates.values()].every((s) => s.row.status === 'completed' || s.row.status === 'skipped');
@@ -1444,7 +1460,6 @@ export class WorkflowRunner {
1444
1460
  // Persist step output
1445
1461
  await this.persistStepOutput(runId, step.name, output);
1446
1462
  this.emit({ type: 'step:completed', runId, stepName: step.name, output });
1447
- this.postToChannel(`**[${step.name}]** Completed (deterministic)\n${output.slice(0, 500)}${output.length > 500 ? '\n...(truncated)' : ''}`);
1448
1463
  }
1449
1464
  catch (err) {
1450
1465
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -1688,7 +1703,6 @@ export class WorkflowRunner {
1688
1703
  // Persist step output to disk so it survives restarts and is inspectable
1689
1704
  await this.persistStepOutput(runId, step.name, output);
1690
1705
  this.emit({ type: 'step:completed', runId, stepName: step.name, output });
1691
- this.postToChannel(`**[${step.name}]** Completed\n${output.slice(0, 500)}${output.length > 500 ? '\n...(truncated)' : ''}`);
1692
1706
  await this.trajectory?.stepCompleted(step, output, attempt + 1);
1693
1707
  return;
1694
1708
  }
@@ -1732,6 +1746,13 @@ export class WorkflowRunner {
1732
1746
  return { cmd: 'aider', args: ['--message', task, '--yes-always', '--no-git', ...extraArgs] };
1733
1747
  case 'goose':
1734
1748
  return { cmd: 'goose', args: ['run', '--text', task, '--no-session', ...extraArgs] };
1749
+ case 'cursor-agent':
1750
+ case 'agent':
1751
+ return { cmd: cli, args: ['--force', '-p', task, ...extraArgs] };
1752
+ case 'cursor':
1753
+ // Should not reach here after resolveAgentDef resolves to agent/cursor-agent,
1754
+ // but handle as fallback.
1755
+ return { cmd: resolveCursorCli(), args: ['--force', '-p', task, ...extraArgs] };
1735
1756
  }
1736
1757
  }
1737
1758
  /**
@@ -1739,14 +1760,16 @@ export class WorkflowRunner {
1739
1760
  * Explicit fields on the definition always win over preset-inferred defaults.
1740
1761
  */
1741
1762
  static resolveAgentDef(def) {
1763
+ // Resolve "cursor" alias to whichever cursor agent binary is in PATH
1764
+ const resolvedCli = def.cli === 'cursor' ? resolveCursorCli() : def.cli;
1742
1765
  if (!def.preset)
1743
- return def;
1766
+ return resolvedCli !== def.cli ? { ...def, cli: resolvedCli } : def;
1744
1767
  const nonInteractivePresets = ['worker', 'reviewer', 'analyst'];
1745
1768
  const defaults = nonInteractivePresets.includes(def.preset)
1746
1769
  ? { interactive: false }
1747
1770
  : {};
1748
1771
  // Explicit fields on the def always win
1749
- return { ...defaults, ...def };
1772
+ return { ...defaults, ...def, cli: resolvedCli };
1750
1773
  }
1751
1774
  /**
1752
1775
  * Returns a preset-specific prefix that is prepended to the non-interactive
@@ -1933,10 +1956,6 @@ export class WorkflowRunner {
1933
1956
  throw new Error('AgentRelay not initialized');
1934
1957
  }
1935
1958
  // Deterministic name: step name + first 8 chars of run ID.
1936
- // This matches the names pre-registered in preflightAgents(), so the broker
1937
- // hits its token cache instantly instead of making a fresh Relaycast HTTP call.
1938
- // On retry the broker may suffix a UUID (409 conflict) — that's fine, the agent
1939
- // still works, just without the cache benefit.
1940
1959
  let agentName = `${step.name}-${(this.currentRunId ?? this.generateShortId()).slice(0, 8)}`;
1941
1960
  // Only inject delegation guidance for lead/coordinator agents, not spokes/workers.
1942
1961
  // In non-hub patterns (pipeline, dag, etc.) every agent is autonomous so they all get it.
@@ -2137,8 +2156,19 @@ export class WorkflowRunner {
2137
2156
  async waitForExitWithIdleNudging(agent, agentDef, step, timeoutMs) {
2138
2157
  const nudgeConfig = this.currentConfig?.swarm.idleNudge;
2139
2158
  if (!nudgeConfig) {
2140
- // No nudge config backward compatible simple wait
2141
- return agent.waitForExit(timeoutMs);
2159
+ // Idle = done: race exit against idle. Whichever fires first completes the step.
2160
+ const result = await Promise.race([
2161
+ agent.waitForExit(timeoutMs).then((r) => ({ kind: 'exit', result: r })),
2162
+ agent.waitForIdle(timeoutMs).then((r) => ({ kind: 'idle', result: r })),
2163
+ ]);
2164
+ if (result.kind === 'idle' && result.result === 'idle') {
2165
+ this.log(`[${step.name}] Agent "${agent.name}" went idle — treating as complete`);
2166
+ this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — treating as complete`);
2167
+ await agent.release();
2168
+ return 'released';
2169
+ }
2170
+ // Exit won the race, or idle returned 'exited'/'timeout' — pass through.
2171
+ return result.result;
2142
2172
  }
2143
2173
  const nudgeAfterMs = nudgeConfig.nudgeAfterMs ?? 120_000;
2144
2174
  const escalateAfterMs = nudgeConfig.escalateAfterMs ?? 120_000;
@@ -2630,7 +2660,8 @@ export class WorkflowRunner {
2630
2660
  // Unicode spinner / ornament characters used by Claude TUI animations.
2631
2661
  // Includes block-element chars (▗▖▘▝) used in the Claude Code header bar.
2632
2662
  const SPINNER = '\\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' +
2633
- '\\u2590\\u258c\\u2588\\u2584\\u2580\\u259a\\u259e'; // additional block elements
2663
+ '\\u2590\\u258c\\u2588\\u2584\\u2580\\u259a\\u259e' + // additional block elements
2664
+ '\\u2b21\\u2b22'; // hex-hollow ⬡ and hex-filled ⬢ (Cursor "Generating" spinner)
2634
2665
  const spinnerRe = new RegExp(`[${SPINNER}]`, 'gu');
2635
2666
  const spinnerClassRe = new RegExp(`^[\\s${SPINNER}]*$`, 'u');
2636
2667
  // Line-level filters
@@ -2645,6 +2676,8 @@ export class WorkflowRunner {
2645
2676
  // regardless of the specific word used (Thinking, Cascading, Flibbertigibbeting, etc.)
2646
2677
  const thinkingLineRe = new RegExp(`^[\\s${SPINNER}]*\\s*\\w[\\w\\s]*\\u2026\\s*$`, 'u');
2647
2678
  const cursorOnlyRe = /^[\s❯⎿›»◀▶←→↑↓⟨⟩⟪⟫·]+$/u;
2679
+ // Cursor Agent TUI lines: generating animations, pasted text indicators, UI chrome
2680
+ const cursorAgentRe = /^(?: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;
2648
2681
  const slashCommandRe = /^\/\w+\s*$/u;
2649
2682
  const mcpJsonKvRe = /^\s*"(?:type|method|params|result|id|jsonrpc|tool|name|arguments|content|role|metadata)"\s*:/u;
2650
2683
  const meaningfulContentRe = /[a-zA-Z0-9]/u;
@@ -2693,6 +2726,8 @@ export class WorkflowRunner {
2693
2726
  continue;
2694
2727
  if (cursorOnlyRe.test(trimmed))
2695
2728
  continue;
2729
+ if (cursorAgentRe.test(trimmed))
2730
+ continue;
2696
2731
  if (slashCommandRe.test(trimmed))
2697
2732
  continue;
2698
2733
  if (!meaningfulContentRe.test(trimmed))