agent-relay 2.2.24 → 2.3.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/package.json +64 -21
- package/packages/acp-bridge/package.json +2 -2
- package/packages/api-types/package.json +1 -1
- package/packages/benchmark/package.json +5 -5
- package/packages/bridge/package.json +7 -7
- package/packages/cli-tester/package.json +1 -1
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +3 -3
- package/packages/sdk-ts/README.md +65 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.d.ts +2 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.d.ts.map +1 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.js +139 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.js.map +1 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts +2 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts.map +1 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.js +176 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.js.map +1 -0
- package/packages/sdk-ts/dist/browser.d.ts +16 -0
- package/packages/sdk-ts/dist/browser.d.ts.map +1 -0
- package/packages/sdk-ts/dist/browser.js +19 -0
- package/packages/sdk-ts/dist/browser.js.map +1 -0
- package/packages/sdk-ts/dist/client.d.ts +91 -0
- package/packages/sdk-ts/dist/client.d.ts.map +1 -0
- package/packages/sdk-ts/dist/client.js +360 -0
- package/packages/sdk-ts/dist/client.js.map +1 -0
- package/packages/sdk-ts/dist/consensus-helpers.d.ts +103 -0
- package/packages/sdk-ts/dist/consensus-helpers.d.ts.map +1 -0
- package/packages/sdk-ts/dist/consensus-helpers.js +147 -0
- package/packages/sdk-ts/dist/consensus-helpers.js.map +1 -0
- package/packages/sdk-ts/dist/consensus.d.ts +72 -0
- package/packages/sdk-ts/dist/consensus.d.ts.map +1 -0
- package/packages/sdk-ts/dist/consensus.js +378 -0
- package/packages/sdk-ts/dist/consensus.js.map +1 -0
- package/packages/sdk-ts/dist/examples/demo.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/demo.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/demo.js +63 -0
- package/packages/sdk-ts/dist/examples/demo.js.map +1 -0
- package/packages/sdk-ts/dist/examples/example.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/example.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/example.js +80 -0
- package/packages/sdk-ts/dist/examples/example.js.map +1 -0
- package/packages/sdk-ts/dist/examples/quickstart.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/quickstart.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/quickstart.js +56 -0
- package/packages/sdk-ts/dist/examples/quickstart.js.map +1 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.js +281 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.js.map +1 -0
- package/packages/sdk-ts/dist/index.d.ts +9 -0
- package/packages/sdk-ts/dist/index.d.ts.map +1 -0
- package/packages/sdk-ts/dist/index.js +9 -0
- package/packages/sdk-ts/dist/index.js.map +1 -0
- package/packages/sdk-ts/dist/logs.d.ts +47 -0
- package/packages/sdk-ts/dist/logs.d.ts.map +1 -0
- package/packages/sdk-ts/dist/logs.js +137 -0
- package/packages/sdk-ts/dist/logs.js.map +1 -0
- package/packages/sdk-ts/dist/protocol.d.ts +249 -0
- package/packages/sdk-ts/dist/protocol.d.ts.map +1 -0
- package/packages/sdk-ts/dist/protocol.js +2 -0
- package/packages/sdk-ts/dist/protocol.js.map +1 -0
- package/packages/sdk-ts/dist/pty.d.ts +8 -0
- package/packages/sdk-ts/dist/pty.d.ts.map +1 -0
- package/packages/sdk-ts/dist/pty.js +14 -0
- package/packages/sdk-ts/dist/pty.js.map +1 -0
- package/packages/sdk-ts/dist/relay.d.ts +118 -0
- package/packages/sdk-ts/dist/relay.d.ts.map +1 -0
- package/packages/sdk-ts/dist/relay.js +355 -0
- package/packages/sdk-ts/dist/relay.js.map +1 -0
- package/packages/sdk-ts/dist/relaycast.d.ts +57 -0
- package/packages/sdk-ts/dist/relaycast.d.ts.map +1 -0
- package/packages/sdk-ts/dist/relaycast.js +110 -0
- package/packages/sdk-ts/dist/relaycast.js.map +1 -0
- package/packages/sdk-ts/dist/shadow.d.ts +100 -0
- package/packages/sdk-ts/dist/shadow.d.ts.map +1 -0
- package/packages/sdk-ts/dist/shadow.js +174 -0
- package/packages/sdk-ts/dist/shadow.js.map +1 -0
- package/packages/sdk-ts/package.json +75 -0
- package/packages/sdk-ts/scripts/bundle-agent-relay.mjs +53 -0
- package/packages/sdk-ts/src/__tests__/integration.test.ts +170 -0
- package/packages/sdk-ts/src/__tests__/quickstart.test.ts +198 -0
- package/packages/sdk-ts/src/browser.ts +57 -0
- package/packages/sdk-ts/src/client.ts +491 -0
- package/packages/sdk-ts/src/consensus-helpers.ts +253 -0
- package/packages/sdk-ts/src/consensus.ts +506 -0
- package/packages/sdk-ts/src/examples/demo.ts +88 -0
- package/packages/sdk-ts/src/examples/example.ts +91 -0
- package/packages/sdk-ts/src/examples/quickstart.ts +72 -0
- package/packages/sdk-ts/src/examples/ralph-loop.ts +352 -0
- package/packages/sdk-ts/src/examples/sample-prd.json +37 -0
- package/packages/sdk-ts/src/index.ts +8 -0
- package/packages/sdk-ts/src/logs.ts +163 -0
- package/packages/sdk-ts/src/protocol.ts +266 -0
- package/packages/sdk-ts/src/pty.ts +16 -0
- package/packages/sdk-ts/src/relay.ts +454 -0
- package/packages/sdk-ts/src/relaycast.ts +143 -0
- package/packages/sdk-ts/src/shadow.ts +230 -0
- package/packages/sdk-ts/tsconfig.json +16 -0
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- 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 +3 -3
- package/packages/wrapper/package.json +6 -6
- package/packages/mcp/SPEC.md +0 -1922
- package/packages/mcp/STAFFING_PLAN.md +0 -294
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible entry point for the broker SDK.
|
|
3
|
+
*
|
|
4
|
+
* Exports only modules with zero Node.js dependencies.
|
|
5
|
+
* The ConsensusEngine class (needs node:crypto + node:events) is NOT
|
|
6
|
+
* exported here — only its types and pure helper functions.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { PROTOCOL_VERSION, type BrokerEvent } from "agent-relay/broker/browser";
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Protocol types — pure TypeScript, zero deps
|
|
15
|
+
export {
|
|
16
|
+
PROTOCOL_VERSION,
|
|
17
|
+
type AgentRuntime,
|
|
18
|
+
type AgentSpec,
|
|
19
|
+
type BrokerEvent,
|
|
20
|
+
type BrokerStatus,
|
|
21
|
+
type BrokerToSdk,
|
|
22
|
+
type BrokerToWorker,
|
|
23
|
+
type PendingDeliveryInfo,
|
|
24
|
+
type ProtocolEnvelope,
|
|
25
|
+
type ProtocolError,
|
|
26
|
+
type RelayDelivery,
|
|
27
|
+
type SdkToBroker,
|
|
28
|
+
type WorkerToBroker,
|
|
29
|
+
} from "./protocol.js";
|
|
30
|
+
|
|
31
|
+
// Consensus types + pure functions (from the Node-free helpers file)
|
|
32
|
+
export {
|
|
33
|
+
formatProposalMessage,
|
|
34
|
+
formatResultMessage,
|
|
35
|
+
parseVoteCommand,
|
|
36
|
+
parseProposalCommand,
|
|
37
|
+
isConsensusCommand,
|
|
38
|
+
type ConsensusType,
|
|
39
|
+
type VoteValue,
|
|
40
|
+
type ProposalStatus,
|
|
41
|
+
type AgentWeight,
|
|
42
|
+
type Vote,
|
|
43
|
+
type Proposal,
|
|
44
|
+
type ConsensusResult,
|
|
45
|
+
type ConsensusConfig,
|
|
46
|
+
type ConsensusEvents,
|
|
47
|
+
type ParsedProposalCommand,
|
|
48
|
+
} from "./consensus-helpers.js";
|
|
49
|
+
|
|
50
|
+
// Shadow manager — pure in-memory, no I/O deps
|
|
51
|
+
export {
|
|
52
|
+
ShadowManager,
|
|
53
|
+
type ShadowConfig,
|
|
54
|
+
type ShadowRelationship,
|
|
55
|
+
type ShadowCopy,
|
|
56
|
+
type SpeakOnTrigger,
|
|
57
|
+
} from "./shadow.js";
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
PROTOCOL_VERSION,
|
|
11
|
+
type AgentRuntime,
|
|
12
|
+
type AgentSpec,
|
|
13
|
+
type BrokerEvent,
|
|
14
|
+
type BrokerStatus,
|
|
15
|
+
type ProtocolEnvelope,
|
|
16
|
+
type ProtocolError,
|
|
17
|
+
} from "./protocol.js";
|
|
18
|
+
|
|
19
|
+
export interface AgentRelayClientOptions {
|
|
20
|
+
binaryPath?: string;
|
|
21
|
+
binaryArgs?: string[];
|
|
22
|
+
brokerName?: string;
|
|
23
|
+
channels?: string[];
|
|
24
|
+
cwd?: string;
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
requestTimeoutMs?: number;
|
|
27
|
+
shutdownTimeoutMs?: number;
|
|
28
|
+
clientName?: string;
|
|
29
|
+
clientVersion?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SpawnPtyInput {
|
|
33
|
+
name: string;
|
|
34
|
+
cli: string;
|
|
35
|
+
args?: string[];
|
|
36
|
+
channels?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SpawnHeadlessClaudeInput {
|
|
40
|
+
name: string;
|
|
41
|
+
args?: string[];
|
|
42
|
+
channels?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SendMessageInput {
|
|
46
|
+
to: string;
|
|
47
|
+
text: string;
|
|
48
|
+
from?: string;
|
|
49
|
+
threadId?: string;
|
|
50
|
+
priority?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ListAgent {
|
|
54
|
+
name: string;
|
|
55
|
+
runtime: AgentRuntime;
|
|
56
|
+
channels: string[];
|
|
57
|
+
parent?: string;
|
|
58
|
+
pid?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PendingRequest {
|
|
62
|
+
expectedType: "ok" | "hello_ack";
|
|
63
|
+
resolve: (value: ProtocolEnvelope<unknown>) => void;
|
|
64
|
+
reject: (error: Error) => void;
|
|
65
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ParsedEnvelope {
|
|
69
|
+
v: number;
|
|
70
|
+
type: string;
|
|
71
|
+
request_id?: string;
|
|
72
|
+
payload: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class AgentRelayProtocolError extends Error {
|
|
76
|
+
code: string;
|
|
77
|
+
retryable: boolean;
|
|
78
|
+
data?: unknown;
|
|
79
|
+
|
|
80
|
+
constructor(payload: ProtocolError) {
|
|
81
|
+
super(payload.message);
|
|
82
|
+
this.name = "AgentRelayProtocolError";
|
|
83
|
+
this.code = payload.code;
|
|
84
|
+
this.retryable = payload.retryable;
|
|
85
|
+
this.data = payload.data;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class AgentRelayProcessError extends Error {
|
|
90
|
+
constructor(message: string) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "AgentRelayProcessError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class AgentRelayClient {
|
|
97
|
+
private readonly options: Required<AgentRelayClientOptions>;
|
|
98
|
+
private child?: ChildProcessWithoutNullStreams;
|
|
99
|
+
private stdoutRl?: ReadlineInterface;
|
|
100
|
+
private stderrRl?: ReadlineInterface;
|
|
101
|
+
private requestSeq = 0;
|
|
102
|
+
private pending = new Map<string, PendingRequest>();
|
|
103
|
+
private startingPromise?: Promise<void>;
|
|
104
|
+
private eventListeners = new Set<(event: BrokerEvent) => void>();
|
|
105
|
+
private stderrListeners = new Set<(line: string) => void>();
|
|
106
|
+
private exitPromise?: Promise<void>;
|
|
107
|
+
|
|
108
|
+
constructor(options: AgentRelayClientOptions = {}) {
|
|
109
|
+
this.options = {
|
|
110
|
+
binaryPath: options.binaryPath ?? resolveDefaultBinaryPath(),
|
|
111
|
+
binaryArgs: options.binaryArgs ?? [],
|
|
112
|
+
brokerName: options.brokerName ?? "broker",
|
|
113
|
+
channels: options.channels ?? ["general"],
|
|
114
|
+
cwd: options.cwd ?? process.cwd(),
|
|
115
|
+
env: options.env ?? process.env,
|
|
116
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
|
|
117
|
+
shutdownTimeoutMs: options.shutdownTimeoutMs ?? 3_000,
|
|
118
|
+
clientName: options.clientName ?? "@agent-relay/sdk-ts",
|
|
119
|
+
clientVersion: options.clientVersion ?? "0.1.0",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static async start(options: AgentRelayClientOptions = {}): Promise<AgentRelayClient> {
|
|
124
|
+
const client = new AgentRelayClient(options);
|
|
125
|
+
await client.start();
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onEvent(listener: (event: BrokerEvent) => void): () => void {
|
|
130
|
+
this.eventListeners.add(listener);
|
|
131
|
+
return () => {
|
|
132
|
+
this.eventListeners.delete(listener);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onBrokerStderr(listener: (line: string) => void): () => void {
|
|
137
|
+
this.stderrListeners.add(listener);
|
|
138
|
+
return () => {
|
|
139
|
+
this.stderrListeners.delete(listener);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async start(): Promise<void> {
|
|
144
|
+
if (this.child) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (this.startingPromise) {
|
|
148
|
+
return this.startingPromise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.startingPromise = this.startInternal();
|
|
152
|
+
try {
|
|
153
|
+
await this.startingPromise;
|
|
154
|
+
} finally {
|
|
155
|
+
this.startingPromise = undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async spawnPty(input: SpawnPtyInput): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
160
|
+
await this.start();
|
|
161
|
+
const agent: AgentSpec = {
|
|
162
|
+
name: input.name,
|
|
163
|
+
runtime: "pty",
|
|
164
|
+
cli: input.cli,
|
|
165
|
+
args: input.args ?? [],
|
|
166
|
+
channels: input.channels ?? [],
|
|
167
|
+
};
|
|
168
|
+
const result = await this.requestOk<{ name: string; runtime: AgentRuntime }>("spawn_agent", {
|
|
169
|
+
agent,
|
|
170
|
+
});
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async spawnHeadlessClaude(
|
|
175
|
+
input: SpawnHeadlessClaudeInput,
|
|
176
|
+
): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
177
|
+
await this.start();
|
|
178
|
+
const agent: AgentSpec = {
|
|
179
|
+
name: input.name,
|
|
180
|
+
runtime: "headless_claude",
|
|
181
|
+
args: input.args ?? [],
|
|
182
|
+
channels: input.channels ?? [],
|
|
183
|
+
};
|
|
184
|
+
const result = await this.requestOk<{ name: string; runtime: AgentRuntime }>("spawn_agent", {
|
|
185
|
+
agent,
|
|
186
|
+
});
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async release(name: string): Promise<{ name: string }> {
|
|
191
|
+
await this.start();
|
|
192
|
+
return this.requestOk<{ name: string }>("release_agent", { name });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async sendMessage(
|
|
196
|
+
input: SendMessageInput,
|
|
197
|
+
): Promise<{ event_id: string; targets: string[] }> {
|
|
198
|
+
await this.start();
|
|
199
|
+
try {
|
|
200
|
+
return await this.requestOk<{ event_id: string; targets: string[] }>("send_message", {
|
|
201
|
+
to: input.to,
|
|
202
|
+
text: input.text,
|
|
203
|
+
from: input.from,
|
|
204
|
+
thread_id: input.threadId,
|
|
205
|
+
priority: input.priority,
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof AgentRelayProtocolError && error.code === "unsupported_operation") {
|
|
209
|
+
return { event_id: "unsupported_operation", targets: [] };
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async listAgents(): Promise<ListAgent[]> {
|
|
216
|
+
await this.start();
|
|
217
|
+
const result = await this.requestOk<{ agents: ListAgent[] }>("list_agents", {});
|
|
218
|
+
return result.agents;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getStatus(): Promise<BrokerStatus> {
|
|
222
|
+
await this.start();
|
|
223
|
+
return this.requestOk<BrokerStatus>("get_status", {});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async shutdown(): Promise<void> {
|
|
227
|
+
if (!this.child) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await this.requestOk("shutdown", {});
|
|
233
|
+
} catch {
|
|
234
|
+
// Continue shutdown path if broker is already unhealthy.
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = this.child;
|
|
238
|
+
const wait = this.exitPromise ?? Promise.resolve();
|
|
239
|
+
const timeout = setTimeout(() => {
|
|
240
|
+
if (!child.killed) {
|
|
241
|
+
child.kill("SIGTERM");
|
|
242
|
+
}
|
|
243
|
+
}, this.options.shutdownTimeoutMs);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await wait;
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timeout);
|
|
249
|
+
if (this.child) {
|
|
250
|
+
this.child.kill("SIGKILL");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async waitForExit(): Promise<void> {
|
|
256
|
+
if (!this.child) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await this.exitPromise;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async startInternal(): Promise<void> {
|
|
263
|
+
const resolvedBinary = expandTilde(this.options.binaryPath);
|
|
264
|
+
if (isExplicitPath(this.options.binaryPath) && !fs.existsSync(resolvedBinary)) {
|
|
265
|
+
throw new AgentRelayProcessError(`broker binary not found: ${this.options.binaryPath}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const args = [
|
|
269
|
+
...this.options.binaryArgs,
|
|
270
|
+
"init",
|
|
271
|
+
"--name",
|
|
272
|
+
this.options.brokerName,
|
|
273
|
+
"--channels",
|
|
274
|
+
this.options.channels.join(","),
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
// Ensure the SDK bin directory (containing agent-relay + relay_send) is on
|
|
278
|
+
// PATH so spawned workers can find relay_send without any user setup.
|
|
279
|
+
const env = { ...this.options.env };
|
|
280
|
+
if (isExplicitPath(this.options.binaryPath)) {
|
|
281
|
+
const binDir = path.dirname(path.resolve(resolvedBinary));
|
|
282
|
+
const currentPath = env.PATH ?? env.Path ?? "";
|
|
283
|
+
if (!currentPath.split(path.delimiter).includes(binDir)) {
|
|
284
|
+
env.PATH = `${binDir}${path.delimiter}${currentPath}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const child = spawn(resolvedBinary, args, {
|
|
289
|
+
cwd: this.options.cwd,
|
|
290
|
+
env,
|
|
291
|
+
stdio: "pipe",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
this.child = child;
|
|
295
|
+
this.stdoutRl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
296
|
+
this.stderrRl = createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
297
|
+
|
|
298
|
+
this.stdoutRl.on("line", (line) => {
|
|
299
|
+
this.handleStdoutLine(line);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
this.stderrRl.on("line", (line) => {
|
|
303
|
+
for (const listener of this.stderrListeners) {
|
|
304
|
+
listener(line);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
this.exitPromise = new Promise<void>((resolve) => {
|
|
309
|
+
child.once("exit", (code, signal) => {
|
|
310
|
+
const error = new AgentRelayProcessError(
|
|
311
|
+
`broker exited (code=${code ?? "null"}, signal=${signal ?? "null"})`,
|
|
312
|
+
);
|
|
313
|
+
this.failAllPending(error);
|
|
314
|
+
this.disposeProcessHandles();
|
|
315
|
+
resolve();
|
|
316
|
+
});
|
|
317
|
+
child.once("error", (error) => {
|
|
318
|
+
this.failAllPending(error);
|
|
319
|
+
this.disposeProcessHandles();
|
|
320
|
+
resolve();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await this.requestHello();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private disposeProcessHandles(): void {
|
|
328
|
+
this.stdoutRl?.close();
|
|
329
|
+
this.stderrRl?.close();
|
|
330
|
+
this.stdoutRl = undefined;
|
|
331
|
+
this.stderrRl = undefined;
|
|
332
|
+
this.child = undefined;
|
|
333
|
+
this.exitPromise = undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private failAllPending(error: Error): void {
|
|
337
|
+
for (const pending of this.pending.values()) {
|
|
338
|
+
clearTimeout(pending.timeout);
|
|
339
|
+
pending.reject(error);
|
|
340
|
+
}
|
|
341
|
+
this.pending.clear();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private handleStdoutLine(line: string): void {
|
|
345
|
+
let parsed: ParsedEnvelope;
|
|
346
|
+
try {
|
|
347
|
+
parsed = JSON.parse(line) as ParsedEnvelope;
|
|
348
|
+
} catch {
|
|
349
|
+
// Non-protocol output should not crash the SDK.
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!parsed || typeof parsed !== "object") {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== "string") {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const envelope: ProtocolEnvelope<unknown> = {
|
|
361
|
+
v: parsed.v,
|
|
362
|
+
type: parsed.type,
|
|
363
|
+
request_id: parsed.request_id,
|
|
364
|
+
payload: parsed.payload,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (envelope.type === "event") {
|
|
368
|
+
const payload = envelope.payload as BrokerEvent;
|
|
369
|
+
for (const listener of this.eventListeners) {
|
|
370
|
+
listener(payload);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!envelope.request_id) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const pending = this.pending.get(envelope.request_id);
|
|
380
|
+
if (!pending) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (envelope.type === "error") {
|
|
385
|
+
clearTimeout(pending.timeout);
|
|
386
|
+
this.pending.delete(envelope.request_id);
|
|
387
|
+
pending.reject(new AgentRelayProtocolError(envelope.payload as ProtocolError));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (envelope.type !== pending.expectedType) {
|
|
392
|
+
clearTimeout(pending.timeout);
|
|
393
|
+
this.pending.delete(envelope.request_id);
|
|
394
|
+
pending.reject(
|
|
395
|
+
new AgentRelayProcessError(
|
|
396
|
+
`unexpected response type '${envelope.type}' for request '${envelope.request_id}' (expected '${pending.expectedType}')`,
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
clearTimeout(pending.timeout);
|
|
403
|
+
this.pending.delete(envelope.request_id);
|
|
404
|
+
pending.resolve(envelope);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async requestHello(): Promise<{ broker_version: string; protocol_version: number }> {
|
|
408
|
+
const payload = {
|
|
409
|
+
client_name: this.options.clientName,
|
|
410
|
+
client_version: this.options.clientVersion,
|
|
411
|
+
};
|
|
412
|
+
const frame = await this.sendRequest("hello", payload, "hello_ack");
|
|
413
|
+
return frame.payload as { broker_version: string; protocol_version: number };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private async requestOk<T = unknown>(type: string, payload: unknown): Promise<T> {
|
|
417
|
+
const frame = await this.sendRequest(type, payload, "ok");
|
|
418
|
+
const result = frame.payload as { result: T };
|
|
419
|
+
return result.result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async sendRequest(
|
|
423
|
+
type: string,
|
|
424
|
+
payload: unknown,
|
|
425
|
+
expectedType: "ok" | "hello_ack",
|
|
426
|
+
): Promise<ProtocolEnvelope<unknown>> {
|
|
427
|
+
if (!this.child) {
|
|
428
|
+
throw new AgentRelayProcessError("broker is not running");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const request_id = `req_${++this.requestSeq}`;
|
|
432
|
+
const message: ProtocolEnvelope<unknown> = {
|
|
433
|
+
v: PROTOCOL_VERSION,
|
|
434
|
+
type,
|
|
435
|
+
request_id,
|
|
436
|
+
payload,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const responsePromise = new Promise<ProtocolEnvelope<unknown>>((resolve, reject) => {
|
|
440
|
+
const timeout = setTimeout(() => {
|
|
441
|
+
this.pending.delete(request_id);
|
|
442
|
+
reject(
|
|
443
|
+
new AgentRelayProcessError(
|
|
444
|
+
`request timed out after ${this.options.requestTimeoutMs}ms (type='${type}', request_id='${request_id}')`,
|
|
445
|
+
),
|
|
446
|
+
);
|
|
447
|
+
}, this.options.requestTimeoutMs);
|
|
448
|
+
|
|
449
|
+
this.pending.set(request_id, {
|
|
450
|
+
expectedType,
|
|
451
|
+
resolve,
|
|
452
|
+
reject,
|
|
453
|
+
timeout,
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
458
|
+
if (!this.child.stdin.write(line)) {
|
|
459
|
+
await once(this.child.stdin, "drain");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return responsePromise;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function expandTilde(p: string): string {
|
|
467
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
468
|
+
const home = os.homedir();
|
|
469
|
+
return path.join(home, p.slice(2));
|
|
470
|
+
}
|
|
471
|
+
return p;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isExplicitPath(binaryPath: string): boolean {
|
|
475
|
+
return (
|
|
476
|
+
binaryPath.includes("/") ||
|
|
477
|
+
binaryPath.includes("\\") ||
|
|
478
|
+
binaryPath.startsWith(".") ||
|
|
479
|
+
binaryPath.startsWith("~")
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function resolveDefaultBinaryPath(): string {
|
|
484
|
+
const exe = process.platform === "win32" ? "agent-relay.exe" : "agent-relay";
|
|
485
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
486
|
+
const bundled = path.resolve(moduleDir, "..", "bin", exe);
|
|
487
|
+
if (fs.existsSync(bundled)) {
|
|
488
|
+
return bundled;
|
|
489
|
+
}
|
|
490
|
+
return "agent-relay";
|
|
491
|
+
}
|