@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.
- package/README.md +169 -64
- package/bin/{agent-relay-broker → 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/__tests__/contract-fixtures.test.js +76 -9
- package/dist/__tests__/contract-fixtures.test.js.map +1 -1
- package/dist/__tests__/facade.test.js +48 -0
- package/dist/__tests__/facade.test.js.map +1 -1
- package/dist/__tests__/integration.test.js +11 -5
- package/dist/__tests__/integration.test.js.map +1 -1
- package/dist/__tests__/unit.test.js +36 -0
- package/dist/__tests__/unit.test.js.map +1 -1
- package/dist/client.d.ts +36 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +142 -9
- package/dist/client.js.map +1 -1
- package/dist/protocol.d.ts +7 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/relay.d.ts +74 -11
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +175 -27
- package/dist/relay.js.map +1 -1
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +71 -36
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/types.d.ts +1 -1
- package/dist/workflows/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/workflows/runner.js
CHANGED
|
@@ -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(`
|
|
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
|
-
//
|
|
2141
|
-
|
|
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'
|
|
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))
|