agent-relay 3.2.22 → 4.0.1
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 +5 -5
- 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 +6564 -2100
- package/dist/src/cli/bootstrap.d.ts.map +1 -1
- package/dist/src/cli/bootstrap.js +2 -0
- package/dist/src/cli/bootstrap.js.map +1 -1
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +14 -4
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts +2 -6
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +31 -12
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +10 -3
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/commands/monitoring.d.ts +2 -2
- package/dist/src/cli/commands/monitoring.d.ts.map +1 -1
- package/dist/src/cli/commands/monitoring.js +15 -6
- package/dist/src/cli/commands/monitoring.js.map +1 -1
- package/dist/src/cli/commands/on/dotfiles.d.ts +35 -0
- package/dist/src/cli/commands/on/dotfiles.d.ts.map +1 -0
- package/dist/src/cli/commands/on/dotfiles.js +157 -0
- package/dist/src/cli/commands/on/dotfiles.js.map +1 -0
- package/dist/src/cli/commands/on/prereqs.d.ts +15 -0
- package/dist/src/cli/commands/on/prereqs.d.ts.map +1 -0
- package/dist/src/cli/commands/on/prereqs.js +103 -0
- package/dist/src/cli/commands/on/prereqs.js.map +1 -0
- package/dist/src/cli/commands/on/provision.d.ts +22 -0
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -0
- package/dist/src/cli/commands/on/provision.js +157 -0
- package/dist/src/cli/commands/on/provision.js.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
- package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
- package/dist/src/cli/commands/on/scan.d.ts +8 -0
- package/dist/src/cli/commands/on/scan.d.ts.map +1 -0
- package/dist/src/cli/commands/on/scan.js +59 -0
- package/dist/src/cli/commands/on/scan.js.map +1 -0
- package/dist/src/cli/commands/on/services.d.ts +17 -0
- package/dist/src/cli/commands/on/services.d.ts.map +1 -0
- package/dist/src/cli/commands/on/services.js +328 -0
- package/dist/src/cli/commands/on/services.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts +61 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -0
- package/dist/src/cli/commands/on/start.js +1107 -0
- package/dist/src/cli/commands/on/start.js.map +1 -0
- package/dist/src/cli/commands/on/stop.d.ts +4 -0
- package/dist/src/cli/commands/on/stop.d.ts.map +1 -0
- package/dist/src/cli/commands/on/stop.js +11 -0
- package/dist/src/cli/commands/on/stop.js.map +1 -0
- package/dist/src/cli/commands/on/token.d.ts +8 -0
- package/dist/src/cli/commands/on/token.d.ts.map +1 -0
- package/dist/src/cli/commands/on/token.js +26 -0
- package/dist/src/cli/commands/on/token.js.map +1 -0
- package/dist/src/cli/commands/on/workspace.d.ts +4 -0
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -0
- package/dist/src/cli/commands/on/workspace.js +245 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts +10 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -0
- package/dist/src/cli/commands/on.js +52 -0
- package/dist/src/cli/commands/on.js.map +1 -0
- package/dist/src/cli/commands/setup.d.ts.map +1 -1
- package/dist/src/cli/commands/setup.js +10 -21
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/lib/bridge.js +1 -1
- package/dist/src/cli/lib/bridge.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts +14 -4
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +82 -120
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +4 -4
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js +14 -11
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
- package/dist/src/cli/lib/core-maintenance.js +11 -22
- package/dist/src/cli/lib/core-maintenance.js.map +1 -1
- package/dist/src/cost/pricing.d.ts +18 -0
- package/dist/src/cost/pricing.d.ts.map +1 -0
- package/dist/src/cost/pricing.js +111 -0
- package/dist/src/cost/pricing.js.map +1 -0
- package/dist/src/cost/tracker.d.ts +13 -0
- package/dist/src/cost/tracker.d.ts.map +1 -0
- package/dist/src/cost/tracker.js +152 -0
- package/dist/src/cost/tracker.js.map +1 -0
- package/dist/src/cost/types.d.ts +23 -0
- package/dist/src/cost/types.d.ts.map +1 -0
- package/dist/src/cost/types.js +2 -0
- package/dist/src/cost/types.js.map +1 -0
- package/package.json +15 -12
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +3 -3
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/README.md +10 -3
- package/packages/sdk/dist/broker-path.d.ts +3 -2
- package/packages/sdk/dist/broker-path.d.ts.map +1 -1
- package/packages/sdk/dist/broker-path.js +119 -32
- package/packages/sdk/dist/broker-path.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +119 -197
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +354 -823
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/examples/example.js +2 -5
- package/packages/sdk/dist/examples/example.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +3 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +3 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay-adapter.d.ts +9 -26
- package/packages/sdk/dist/relay-adapter.d.ts.map +1 -1
- package/packages/sdk/dist/relay-adapter.js +75 -47
- package/packages/sdk/dist/relay-adapter.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +26 -6
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +213 -43
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/transport.d.ts +58 -0
- package/packages/sdk/dist/transport.d.ts.map +1 -0
- package/packages/sdk/dist/transport.js +184 -0
- package/packages/sdk/dist/transport.js.map +1 -0
- package/packages/sdk/dist/types.d.ts +69 -0
- package/packages/sdk/dist/types.d.ts.map +1 -0
- package/packages/sdk/dist/types.js +5 -0
- package/packages/sdk/dist/types.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +3 -2
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -3
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
- package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
- package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +7 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +7 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
- package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/process-spawner.js +141 -0
- package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
- package/packages/sdk/dist/workflows/run.d.ts +2 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -6
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +443 -719
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
- package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/step-executor.js +393 -0
- package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.js +144 -0
- package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
- package/packages/sdk/dist/workflows/validator.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/validator.js +17 -2
- package/packages/sdk/dist/workflows/validator.js.map +1 -1
- package/packages/sdk/dist/workflows/verification.d.ts +33 -0
- package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/verification.js +122 -0
- package/packages/sdk/dist/workflows/verification.js.map +1 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/unit.test.ts +100 -1
- package/packages/sdk/src/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +453 -1069
- package/packages/sdk/src/examples/example.ts +2 -5
- package/packages/sdk/src/index.ts +9 -1
- package/packages/sdk/src/relay-adapter.ts +75 -55
- package/packages/sdk/src/relay.ts +262 -55
- package/packages/sdk/src/transport.ts +216 -0
- package/packages/sdk/src/types.ts +75 -0
- package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
- package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
- package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
- package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
- package/packages/sdk/src/workflows/builder.ts +6 -6
- package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
- package/packages/sdk/src/workflows/index.ts +12 -0
- package/packages/sdk/src/workflows/process-spawner.ts +201 -0
- package/packages/sdk/src/workflows/run.ts +2 -1
- package/packages/sdk/src/workflows/runner.ts +636 -951
- package/packages/sdk/src/workflows/step-executor.ts +579 -0
- package/packages/sdk/src/workflows/template-resolver.ts +180 -0
- package/packages/sdk/src/workflows/validator.ts +20 -2
- package/packages/sdk/src/workflows/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +0 -8
- package/packages/sdk-py/src/agent_relay/client.py +329 -522
- package/packages/sdk-py/src/agent_relay/protocol.py +2 -96
- package/packages/sdk-py/src/agent_relay/relay.py +1 -4
- package/packages/sdk-py/tests/test_wait_for_api_url.py +92 -0
- package/packages/sdk-py/uv.lock +5388 -0
- package/packages/telemetry/dist/client.d.ts.map +1 -1
- package/packages/telemetry/dist/client.js +1 -1
- package/packages/telemetry/dist/client.js.map +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/telemetry/src/client.ts +3 -10
- 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 +121 -1
|
@@ -1,354 +1,369 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AgentRelayClient — single client for communicating with an agent-relay broker
|
|
3
|
+
* over HTTP/WS. Works identically for local and remote brokers.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* // Remote broker (Daytona sandbox, cloud, etc.)
|
|
7
|
+
* const client = new AgentRelayClient({ baseUrl, apiKey });
|
|
8
|
+
*
|
|
9
|
+
* // Local broker (spawn and connect)
|
|
10
|
+
* const client = await AgentRelayClient.spawn({ cwd: '/my/project' });
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
14
|
+
import { randomBytes } from 'node:crypto';
|
|
15
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
6
16
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type BrokerStats,
|
|
17
|
-
type BrokerStatus,
|
|
18
|
-
type CrashInsightsResponse,
|
|
19
|
-
type HeadlessProvider,
|
|
20
|
-
type ProtocolEnvelope,
|
|
21
|
-
type ProtocolError,
|
|
22
|
-
type RestartPolicy,
|
|
23
|
-
type MessageInjectionMode,
|
|
17
|
+
import { BrokerTransport, AgentRelayProtocolError } from './transport.js';
|
|
18
|
+
import { getBrokerBinaryPath } from './broker-path.js';
|
|
19
|
+
import type {
|
|
20
|
+
AgentRuntime,
|
|
21
|
+
BrokerEvent,
|
|
22
|
+
BrokerStats,
|
|
23
|
+
BrokerStatus,
|
|
24
|
+
CrashInsightsResponse,
|
|
25
|
+
HeadlessProvider,
|
|
24
26
|
} from './protocol.js';
|
|
27
|
+
import type {
|
|
28
|
+
AgentTransport,
|
|
29
|
+
SpawnPtyInput,
|
|
30
|
+
SpawnProviderInput,
|
|
31
|
+
SendMessageInput,
|
|
32
|
+
ListAgent,
|
|
33
|
+
} from './types.js';
|
|
34
|
+
|
|
35
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
25
36
|
|
|
26
37
|
export interface AgentRelayClientOptions {
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
apiKey?: string;
|
|
40
|
+
/** Timeout in ms for HTTP requests. Default: 30000. */
|
|
41
|
+
requestTimeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AgentRelayBrokerInitArgs {
|
|
45
|
+
/** Optional HTTP API port for dashboard proxy (0 = disabled). */
|
|
46
|
+
apiPort?: number;
|
|
47
|
+
/** Bind address for the HTTP API. Defaults to 127.0.0.1 in the broker. */
|
|
48
|
+
apiBind?: string;
|
|
49
|
+
/** Enable persistence for broker state under the working directory. */
|
|
50
|
+
persist?: boolean;
|
|
51
|
+
/** Override the directory used for broker state files. */
|
|
52
|
+
stateDir?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AgentRelaySpawnOptions {
|
|
56
|
+
/** Path to the agent-relay-broker binary. Auto-resolved if omitted. */
|
|
27
57
|
binaryPath?: string;
|
|
28
|
-
|
|
58
|
+
/** Structured options mapped to the broker's Rust `init` CLI flags. */
|
|
59
|
+
binaryArgs?: AgentRelayBrokerInitArgs;
|
|
60
|
+
/** Broker name. Defaults to cwd basename. */
|
|
29
61
|
brokerName?: string;
|
|
62
|
+
/** Default channels for spawned agents. */
|
|
30
63
|
channels?: string[];
|
|
64
|
+
/** Working directory for the broker process. */
|
|
31
65
|
cwd?: string;
|
|
66
|
+
/** Environment variables for the broker process. */
|
|
32
67
|
env?: NodeJS.ProcessEnv;
|
|
68
|
+
/** Forward broker stderr to this callback. */
|
|
69
|
+
onStderr?: (line: string) => void;
|
|
70
|
+
/** Timeout in ms to wait for broker to become ready. Default: 15000. */
|
|
71
|
+
startupTimeoutMs?: number;
|
|
72
|
+
/** Timeout in ms for HTTP requests to the broker. Default: 30000. */
|
|
33
73
|
requestTimeoutMs?: number;
|
|
34
|
-
shutdownTimeoutMs?: number;
|
|
35
|
-
clientName?: string;
|
|
36
|
-
clientVersion?: string;
|
|
37
74
|
}
|
|
38
75
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cwd?: string;
|
|
47
|
-
team?: string;
|
|
48
|
-
shadowOf?: string;
|
|
49
|
-
shadowMode?: string;
|
|
50
|
-
/** Silence duration in seconds before emitting agent_idle (0 = disabled, default: 30). */
|
|
51
|
-
idleThresholdSecs?: number;
|
|
52
|
-
/** Auto-restart policy for crashed agents. */
|
|
53
|
-
restartPolicy?: RestartPolicy;
|
|
54
|
-
/** Name of a previously released agent whose continuity context should be injected. */
|
|
55
|
-
continueFrom?: string;
|
|
56
|
-
/** When true, skip injecting the relay MCP configuration and protocol prompt into the spawned agent.
|
|
57
|
-
* Useful for minor tasks where relay messaging is not needed, saving tokens. */
|
|
58
|
-
skipRelayPrompt?: boolean;
|
|
76
|
+
export interface SessionInfo {
|
|
77
|
+
broker_version: string;
|
|
78
|
+
protocol_version: number;
|
|
79
|
+
workspace_key?: string;
|
|
80
|
+
default_workspace_id?: string;
|
|
81
|
+
mode: string;
|
|
82
|
+
uptime_secs: number;
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
provider: HeadlessProvider;
|
|
64
|
-
args?: string[];
|
|
65
|
-
channels?: string[];
|
|
66
|
-
task?: string;
|
|
67
|
-
/** When true, skip injecting the relay MCP configuration and protocol prompt into the spawned agent.
|
|
68
|
-
* Useful for minor tasks where relay messaging is not needed, saving tokens. */
|
|
69
|
-
skipRelayPrompt?: boolean;
|
|
85
|
+
function isHeadlessProvider(value: string): value is HeadlessProvider {
|
|
86
|
+
return value === 'claude' || value === 'opencode';
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
export interface SpawnProviderInput {
|
|
75
|
-
name: string;
|
|
76
|
-
provider: string;
|
|
77
|
-
transport?: AgentTransport;
|
|
78
|
-
args?: string[];
|
|
79
|
-
channels?: string[];
|
|
80
|
-
task?: string;
|
|
81
|
-
model?: string;
|
|
82
|
-
cwd?: string;
|
|
83
|
-
team?: string;
|
|
84
|
-
shadowOf?: string;
|
|
85
|
-
shadowMode?: string;
|
|
86
|
-
idleThresholdSecs?: number;
|
|
87
|
-
restartPolicy?: RestartPolicy;
|
|
88
|
-
continueFrom?: string;
|
|
89
|
-
/** When true, skip injecting the relay MCP configuration and protocol prompt into the spawned agent.
|
|
90
|
-
* Useful for minor tasks where relay messaging is not needed, saving tokens. */
|
|
91
|
-
skipRelayPrompt?: boolean;
|
|
89
|
+
function resolveSpawnTransport(input: SpawnProviderInput): AgentTransport {
|
|
90
|
+
return input.transport ?? (input.provider === 'opencode' ? 'headless' : 'pty');
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
threadId?: string;
|
|
99
|
-
workspaceId?: string;
|
|
100
|
-
workspaceAlias?: string;
|
|
101
|
-
priority?: number;
|
|
102
|
-
data?: Record<string, unknown>;
|
|
103
|
-
mode?: MessageInjectionMode;
|
|
104
|
-
}
|
|
93
|
+
function isProcessRunning(pid: number): boolean {
|
|
94
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
team?: string;
|
|
113
|
-
channels: string[];
|
|
114
|
-
parent?: string;
|
|
115
|
-
pid?: number;
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, 0);
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
103
|
+
}
|
|
116
104
|
}
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
123
|
-
}
|
|
106
|
+
function buildBrokerInitArgs(args?: AgentRelayBrokerInitArgs): string[] {
|
|
107
|
+
if (!args) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
124
110
|
|
|
125
|
-
|
|
126
|
-
v: number;
|
|
127
|
-
type: string;
|
|
128
|
-
request_id?: string;
|
|
129
|
-
payload: unknown;
|
|
130
|
-
}
|
|
111
|
+
const cliArgs: string[] = [];
|
|
131
112
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
retryable: boolean;
|
|
135
|
-
data?: unknown;
|
|
136
|
-
|
|
137
|
-
constructor(payload: ProtocolError) {
|
|
138
|
-
super(payload.message);
|
|
139
|
-
this.name = 'AgentRelayProtocolError';
|
|
140
|
-
this.code = payload.code;
|
|
141
|
-
this.retryable = payload.retryable;
|
|
142
|
-
this.data = payload.data;
|
|
113
|
+
if (args.persist) {
|
|
114
|
+
cliArgs.push('--persist');
|
|
143
115
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
116
|
+
if (args.apiPort !== undefined) {
|
|
117
|
+
cliArgs.push('--api-port', String(args.apiPort));
|
|
118
|
+
}
|
|
119
|
+
if (args.apiBind !== undefined) {
|
|
120
|
+
cliArgs.push('--api-bind', args.apiBind);
|
|
121
|
+
}
|
|
122
|
+
if (args.stateDir !== undefined) {
|
|
123
|
+
cliArgs.push('--state-dir', args.stateDir);
|
|
150
124
|
}
|
|
151
|
-
}
|
|
152
125
|
|
|
153
|
-
|
|
154
|
-
return value === 'claude' || value === 'opencode';
|
|
126
|
+
return cliArgs;
|
|
155
127
|
}
|
|
156
128
|
|
|
129
|
+
// ── Client ─────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
157
131
|
export class AgentRelayClient {
|
|
158
|
-
private readonly
|
|
159
|
-
private child?: ChildProcessWithoutNullStreams;
|
|
160
|
-
private stdoutRl?: ReadlineInterface;
|
|
161
|
-
private stderrRl?: ReadlineInterface;
|
|
162
|
-
private lastStderrLine?: string;
|
|
163
|
-
private requestSeq = 0;
|
|
164
|
-
private pending = new Map<string, PendingRequest>();
|
|
165
|
-
private startingPromise?: Promise<void>;
|
|
166
|
-
private eventListeners = new Set<(event: BrokerEvent) => void>();
|
|
167
|
-
private stderrListeners = new Set<(line: string) => void>();
|
|
168
|
-
private eventBuffer: BrokerEvent[] = [];
|
|
169
|
-
private maxBufferSize = 1000;
|
|
170
|
-
private exitPromise?: Promise<void>;
|
|
171
|
-
/** The workspace key returned by the broker in its hello_ack response. */
|
|
172
|
-
workspaceKey?: string;
|
|
132
|
+
private readonly transport: BrokerTransport;
|
|
173
133
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
brokerName: options.brokerName ?? (path.basename(options.cwd ?? process.cwd()) || 'project'),
|
|
179
|
-
channels: options.channels ?? ['general'],
|
|
180
|
-
cwd: options.cwd ?? process.cwd(),
|
|
181
|
-
env: options.env ?? process.env,
|
|
182
|
-
requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
|
|
183
|
-
shutdownTimeoutMs: options.shutdownTimeoutMs ?? 3_000,
|
|
184
|
-
clientName: options.clientName ?? '@agent-relay/sdk',
|
|
185
|
-
clientVersion: options.clientVersion ?? '0.1.0',
|
|
186
|
-
};
|
|
187
|
-
}
|
|
134
|
+
/** Set after spawn() — the managed child process. */
|
|
135
|
+
private child: ChildProcess | null = null;
|
|
136
|
+
/** Lease renewal timer (only for spawned brokers). */
|
|
137
|
+
private leaseTimer: ReturnType<typeof setInterval> | null = null;
|
|
188
138
|
|
|
189
|
-
|
|
190
|
-
const client = new AgentRelayClient(options);
|
|
191
|
-
await client.start();
|
|
192
|
-
return client;
|
|
193
|
-
}
|
|
139
|
+
workspaceKey?: string;
|
|
194
140
|
|
|
195
|
-
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
141
|
+
constructor(options: AgentRelayClientOptions) {
|
|
142
|
+
this.transport = new BrokerTransport({
|
|
143
|
+
baseUrl: options.baseUrl,
|
|
144
|
+
apiKey: options.apiKey,
|
|
145
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
146
|
+
});
|
|
200
147
|
}
|
|
201
148
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Connect to an already-running broker by reading its connection file.
|
|
151
|
+
*
|
|
152
|
+
* The broker writes `connection.json` to its data directory ({cwd}/.agent-relay/
|
|
153
|
+
* in persist mode). This method reads that file to get the URL and API key.
|
|
154
|
+
*
|
|
155
|
+
* @param cwd — project directory (default: process.cwd())
|
|
156
|
+
* @param connectionPath — explicit path to connection.json (overrides cwd)
|
|
157
|
+
*/
|
|
158
|
+
static connect(options?: { cwd?: string; connectionPath?: string }): AgentRelayClient {
|
|
159
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
160
|
+
const stateDir = process.env.AGENT_RELAY_STATE_DIR;
|
|
161
|
+
const connPath =
|
|
162
|
+
options?.connectionPath ?? path.join(stateDir ?? path.join(cwd, '.agent-relay'), 'connection.json');
|
|
163
|
+
|
|
164
|
+
if (!existsSync(connPath)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`No running broker found (${connPath} does not exist). Start one with 'agent-relay up' or use AgentRelayClient.spawn().`
|
|
167
|
+
);
|
|
206
168
|
}
|
|
207
|
-
|
|
208
|
-
|
|
169
|
+
|
|
170
|
+
const raw = readFileSync(connPath, 'utf-8');
|
|
171
|
+
let conn: { url?: string; api_key?: string; port?: number; pid?: number };
|
|
172
|
+
try {
|
|
173
|
+
conn = JSON.parse(raw);
|
|
174
|
+
} catch {
|
|
175
|
+
throw new Error(`Corrupt broker connection file (${connPath}). Remove it and start the broker again.`);
|
|
209
176
|
}
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
177
|
+
|
|
178
|
+
if (typeof conn.url !== 'string' || typeof conn.api_key !== 'string' || typeof conn.pid !== 'number') {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Invalid broker connection metadata in ${connPath}. Remove it and start the broker again.`
|
|
214
181
|
);
|
|
215
182
|
}
|
|
216
|
-
const limit = filter?.limit;
|
|
217
|
-
if (limit !== undefined) {
|
|
218
|
-
events = events.slice(-limit);
|
|
219
|
-
}
|
|
220
|
-
return events;
|
|
221
|
-
}
|
|
222
183
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return event;
|
|
228
|
-
}
|
|
184
|
+
if (!isProcessRunning(conn.pid)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Stale broker connection file (${connPath}) points to dead pid ${conn.pid}. Start the broker with 'agent-relay up' or use AgentRelayClient.spawn().`
|
|
187
|
+
);
|
|
229
188
|
}
|
|
230
|
-
|
|
189
|
+
|
|
190
|
+
return new AgentRelayClient({ baseUrl: conn.url, apiKey: conn.api_key });
|
|
231
191
|
}
|
|
232
192
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Spawn a local broker process and return a connected client.
|
|
195
|
+
*
|
|
196
|
+
* 1. Generates a random API key
|
|
197
|
+
* 2. Spawns the broker binary (attached)
|
|
198
|
+
* 3. Parses the API port from stdout
|
|
199
|
+
* 4. Connects HTTP/WS transport
|
|
200
|
+
* 5. Fetches session metadata
|
|
201
|
+
* 6. Starts event stream + lease renewal
|
|
202
|
+
*/
|
|
203
|
+
static async spawn(options?: AgentRelaySpawnOptions): Promise<AgentRelayClient> {
|
|
204
|
+
const binaryPath = options?.binaryPath ?? getBrokerBinaryPath() ?? 'agent-relay-broker';
|
|
205
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
206
|
+
const brokerName = options?.brokerName ?? (path.basename(cwd) || 'project');
|
|
207
|
+
const channels = options?.channels ?? ['general'];
|
|
208
|
+
const timeoutMs = options?.startupTimeoutMs ?? 15_000;
|
|
209
|
+
const userArgs = buildBrokerInitArgs(options?.binaryArgs);
|
|
210
|
+
|
|
211
|
+
const apiKey = `br_${randomBytes(16).toString('hex')}`;
|
|
212
|
+
|
|
213
|
+
const env = {
|
|
214
|
+
...process.env,
|
|
215
|
+
...options?.env,
|
|
216
|
+
RELAY_BROKER_API_KEY: apiKey,
|
|
237
217
|
};
|
|
218
|
+
|
|
219
|
+
const args = ['init', '--name', brokerName, '--channels', channels.join(','), ...userArgs];
|
|
220
|
+
|
|
221
|
+
const child = spawn(binaryPath, args, {
|
|
222
|
+
cwd,
|
|
223
|
+
env,
|
|
224
|
+
stdio: ['ignore', 'pipe', options?.onStderr ? 'pipe' : 'ignore'],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Forward stderr if requested
|
|
228
|
+
if (options?.onStderr && child.stderr) {
|
|
229
|
+
const { createInterface } = await import('node:readline');
|
|
230
|
+
const rl = createInterface({ input: child.stderr });
|
|
231
|
+
rl.on('line', (line) => options.onStderr!(line));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse the API URL from stdout (the broker prints it after binding)
|
|
235
|
+
const baseUrl = await waitForApiUrl(child, timeoutMs);
|
|
236
|
+
|
|
237
|
+
const client = new AgentRelayClient({
|
|
238
|
+
baseUrl,
|
|
239
|
+
apiKey,
|
|
240
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
241
|
+
});
|
|
242
|
+
client.child = child;
|
|
243
|
+
|
|
244
|
+
await client.getSession();
|
|
245
|
+
client.connectEvents();
|
|
246
|
+
|
|
247
|
+
// Renew the owner lease so the broker doesn't auto-shutdown
|
|
248
|
+
client.leaseTimer = setInterval(() => {
|
|
249
|
+
client.renewLease().catch(() => {});
|
|
250
|
+
}, 60_000);
|
|
251
|
+
|
|
252
|
+
child.on('exit', () => {
|
|
253
|
+
client.disconnectEvents();
|
|
254
|
+
if (client.leaseTimer) {
|
|
255
|
+
clearInterval(client.leaseTimer);
|
|
256
|
+
client.leaseTimer = null;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return client;
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
/** PID of the managed broker process, if spawned locally. */
|
|
240
264
|
get brokerPid(): number | undefined {
|
|
241
265
|
return this.child?.pid;
|
|
242
266
|
}
|
|
243
267
|
|
|
244
|
-
|
|
245
|
-
if (this.child) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (this.startingPromise) {
|
|
249
|
-
return this.startingPromise;
|
|
250
|
-
}
|
|
268
|
+
// ── Session ────────────────────────────────────────────────────────
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.startingPromise = undefined;
|
|
257
|
-
}
|
|
270
|
+
async getSession(): Promise<SessionInfo> {
|
|
271
|
+
const session = await this.transport.request<SessionInfo>('/api/session');
|
|
272
|
+
this.workspaceKey = session.workspace_key;
|
|
273
|
+
return session;
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
* The broker warms its token cache in parallel; subsequent spawn_agent calls
|
|
263
|
-
* hit the cache rather than waiting on individual HTTP registrations.
|
|
264
|
-
* Fire-and-forget from the caller's perspective — broker responds immediately
|
|
265
|
-
* and registers in the background.
|
|
266
|
-
*/
|
|
267
|
-
async preflightAgents(agents: Array<{ name: string; cli: string | AgentRuntime }>): Promise<void> {
|
|
268
|
-
if (agents.length === 0) return;
|
|
269
|
-
await this.start();
|
|
270
|
-
await this.requestOk<void>('preflight_agents', { agents });
|
|
276
|
+
async healthCheck(): Promise<{ service: string }> {
|
|
277
|
+
return this.transport.request<{ service: string }>('/health');
|
|
271
278
|
}
|
|
272
279
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
name: input.name,
|
|
278
|
-
runtime: 'pty',
|
|
279
|
-
cli: input.cli,
|
|
280
|
-
args,
|
|
281
|
-
channels: input.channels ?? [],
|
|
282
|
-
model: input.model,
|
|
283
|
-
cwd: input.cwd ?? this.options.cwd,
|
|
284
|
-
team: input.team,
|
|
285
|
-
shadow_of: input.shadowOf,
|
|
286
|
-
shadow_mode: input.shadowMode,
|
|
287
|
-
restart_policy: input.restartPolicy,
|
|
288
|
-
};
|
|
289
|
-
const result = await this.requestOk<{ name: string; runtime: AgentRuntime }>('spawn_agent', {
|
|
290
|
-
agent,
|
|
291
|
-
...(input.task != null ? { initial_task: input.task } : {}),
|
|
292
|
-
...(input.idleThresholdSecs != null ? { idle_threshold_secs: input.idleThresholdSecs } : {}),
|
|
293
|
-
...(input.continueFrom != null ? { continue_from: input.continueFrom } : {}),
|
|
294
|
-
...(input.skipRelayPrompt != null ? { skip_relay_prompt: input.skipRelayPrompt } : {}),
|
|
295
|
-
});
|
|
296
|
-
return result;
|
|
280
|
+
// ── Events ─────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
connectEvents(sinceSeq?: number): void {
|
|
283
|
+
this.transport.connect(sinceSeq);
|
|
297
284
|
}
|
|
298
285
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const agent: AgentSpec = {
|
|
302
|
-
name: input.name,
|
|
303
|
-
runtime: 'headless',
|
|
304
|
-
provider: input.provider,
|
|
305
|
-
args: input.args ?? [],
|
|
306
|
-
channels: input.channels ?? [],
|
|
307
|
-
};
|
|
308
|
-
const result = await this.requestOk<{ name: string; runtime: AgentRuntime }>('spawn_agent', {
|
|
309
|
-
agent,
|
|
310
|
-
...(input.task != null ? { initial_task: input.task } : {}),
|
|
311
|
-
...(input.skipRelayPrompt != null ? { skip_relay_prompt: input.skipRelayPrompt } : {}),
|
|
312
|
-
});
|
|
313
|
-
return result;
|
|
286
|
+
disconnectEvents(): void {
|
|
287
|
+
this.transport.disconnect();
|
|
314
288
|
}
|
|
315
289
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
290
|
+
onEvent(listener: (event: BrokerEvent) => void): () => void {
|
|
291
|
+
return this.transport.onEvent(listener);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
queryEvents(filter?: { kind?: string; name?: string; since?: number; limit?: number }): BrokerEvent[] {
|
|
295
|
+
return this.transport.queryEvents(filter);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getLastEvent(kind: string, name?: string): BrokerEvent | undefined {
|
|
299
|
+
return this.transport.getLastEvent(kind, name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Agent lifecycle ────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
async spawnPty(input: SpawnPtyInput): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
305
|
+
return this.transport.request('/api/spawn', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
body: JSON.stringify({
|
|
325
308
|
name: input.name,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
309
|
+
cli: input.cli,
|
|
310
|
+
model: input.model,
|
|
311
|
+
args: input.args ?? [],
|
|
329
312
|
task: input.task,
|
|
313
|
+
channels: input.channels ?? [],
|
|
314
|
+
cwd: input.cwd,
|
|
315
|
+
team: input.team,
|
|
316
|
+
shadowOf: input.shadowOf,
|
|
317
|
+
shadowMode: input.shadowMode,
|
|
318
|
+
continueFrom: input.continueFrom,
|
|
319
|
+
idleThresholdSecs: input.idleThresholdSecs,
|
|
320
|
+
restartPolicy: input.restartPolicy,
|
|
330
321
|
skipRelayPrompt: input.skipRelayPrompt,
|
|
331
|
-
})
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async spawnProvider(input: SpawnProviderInput): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
327
|
+
const transport = resolveSpawnTransport(input);
|
|
328
|
+
if (transport === 'headless' && !isHeadlessProvider(input.provider)) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`provider '${input.provider}' does not support headless transport (supported: claude, opencode)`
|
|
331
|
+
);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
return this.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
334
|
+
return this.transport.request('/api/spawn', {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
name: input.name,
|
|
338
|
+
cli: input.provider,
|
|
339
|
+
model: input.model,
|
|
340
|
+
args: input.args ?? [],
|
|
341
|
+
task: input.task,
|
|
342
|
+
channels: input.channels ?? [],
|
|
343
|
+
cwd: input.cwd,
|
|
344
|
+
team: input.team,
|
|
345
|
+
shadowOf: input.shadowOf,
|
|
346
|
+
shadowMode: input.shadowMode,
|
|
347
|
+
continueFrom: input.continueFrom,
|
|
348
|
+
idleThresholdSecs: input.idleThresholdSecs,
|
|
349
|
+
restartPolicy: input.restartPolicy,
|
|
350
|
+
skipRelayPrompt: input.skipRelayPrompt,
|
|
351
|
+
transport,
|
|
352
|
+
}),
|
|
349
353
|
});
|
|
350
354
|
}
|
|
351
355
|
|
|
356
|
+
async spawnHeadless(input: {
|
|
357
|
+
name: string;
|
|
358
|
+
provider: HeadlessProvider;
|
|
359
|
+
args?: string[];
|
|
360
|
+
channels?: string[];
|
|
361
|
+
task?: string;
|
|
362
|
+
skipRelayPrompt?: boolean;
|
|
363
|
+
}): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
364
|
+
return this.spawnProvider({ ...input, transport: 'headless' });
|
|
365
|
+
}
|
|
366
|
+
|
|
352
367
|
async spawnClaude(
|
|
353
368
|
input: Omit<SpawnProviderInput, 'provider'>
|
|
354
369
|
): Promise<{ name: string; runtime: AgentRuntime }> {
|
|
@@ -362,23 +377,24 @@ export class AgentRelayClient {
|
|
|
362
377
|
}
|
|
363
378
|
|
|
364
379
|
async release(name: string, reason?: string): Promise<{ name: string }> {
|
|
365
|
-
|
|
366
|
-
|
|
380
|
+
return this.transport.request(`/api/spawned/${encodeURIComponent(name)}`, {
|
|
381
|
+
method: 'DELETE',
|
|
382
|
+
...(reason ? { body: JSON.stringify({ reason }) } : {}),
|
|
383
|
+
});
|
|
367
384
|
}
|
|
368
385
|
|
|
369
|
-
async
|
|
370
|
-
await this.
|
|
371
|
-
return
|
|
386
|
+
async listAgents(): Promise<ListAgent[]> {
|
|
387
|
+
const result = await this.transport.request<{ agents: ListAgent[] }>('/api/spawned');
|
|
388
|
+
return result.agents;
|
|
372
389
|
}
|
|
373
390
|
|
|
374
|
-
|
|
375
|
-
await this.start();
|
|
376
|
-
await this.requestOk<void>('subscribe_channels', { name, channels });
|
|
377
|
-
}
|
|
391
|
+
// ── PTY control ────────────────────────────────────────────────────
|
|
378
392
|
|
|
379
|
-
async
|
|
380
|
-
|
|
381
|
-
|
|
393
|
+
async sendInput(name: string, data: string): Promise<{ name: string; bytes_written: number }> {
|
|
394
|
+
return this.transport.request(`/api/input/${encodeURIComponent(name)}`, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
body: JSON.stringify({ data }),
|
|
397
|
+
});
|
|
382
398
|
}
|
|
383
399
|
|
|
384
400
|
async resizePty(
|
|
@@ -386,56 +402,29 @@ export class AgentRelayClient {
|
|
|
386
402
|
rows: number,
|
|
387
403
|
cols: number
|
|
388
404
|
): Promise<{ name: string; rows: number; cols: number }> {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
rows,
|
|
393
|
-
cols,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async setModel(
|
|
398
|
-
name: string,
|
|
399
|
-
model: string,
|
|
400
|
-
opts?: { timeoutMs?: number }
|
|
401
|
-
): Promise<{ name: string; model: string; success: boolean }> {
|
|
402
|
-
await this.start();
|
|
403
|
-
return this.requestOk<{ name: string; model: string; success: boolean }>('set_model', {
|
|
404
|
-
name,
|
|
405
|
-
model,
|
|
406
|
-
timeout_ms: opts?.timeoutMs,
|
|
405
|
+
return this.transport.request(`/api/resize/${encodeURIComponent(name)}`, {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
body: JSON.stringify({ rows, cols }),
|
|
407
408
|
});
|
|
408
409
|
}
|
|
409
410
|
|
|
410
|
-
|
|
411
|
-
agents: Array<{ name: string; pid: number; memory_bytes: number; uptime_secs: number }>;
|
|
412
|
-
broker?: BrokerStats;
|
|
413
|
-
}> {
|
|
414
|
-
await this.start();
|
|
415
|
-
return this.requestOk<{
|
|
416
|
-
agents: Array<{ name: string; pid: number; memory_bytes: number; uptime_secs: number }>;
|
|
417
|
-
broker?: BrokerStats;
|
|
418
|
-
}>('get_metrics', { agent });
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async getCrashInsights(): Promise<CrashInsightsResponse> {
|
|
422
|
-
await this.start();
|
|
423
|
-
return this.requestOk<CrashInsightsResponse>('get_crash_insights', {});
|
|
424
|
-
}
|
|
411
|
+
// ── Messaging ──────────────────────────────────────────────────────
|
|
425
412
|
|
|
426
413
|
async sendMessage(input: SendMessageInput): Promise<{ event_id: string; targets: string[] }> {
|
|
427
|
-
await this.start();
|
|
428
414
|
try {
|
|
429
|
-
return await this.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
415
|
+
return await this.transport.request('/api/send', {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
to: input.to,
|
|
419
|
+
text: input.text,
|
|
420
|
+
from: input.from,
|
|
421
|
+
threadId: input.threadId,
|
|
422
|
+
workspaceId: input.workspaceId,
|
|
423
|
+
workspaceAlias: input.workspaceAlias,
|
|
424
|
+
priority: input.priority,
|
|
425
|
+
data: input.data,
|
|
426
|
+
mode: input.mode,
|
|
427
|
+
}),
|
|
439
428
|
});
|
|
440
429
|
} catch (error) {
|
|
441
430
|
if (error instanceof AgentRelayProtocolError && error.code === 'unsupported_operation') {
|
|
@@ -445,786 +434,181 @@ export class AgentRelayClient {
|
|
|
445
434
|
}
|
|
446
435
|
}
|
|
447
436
|
|
|
448
|
-
|
|
449
|
-
await this.start();
|
|
450
|
-
const result = await this.requestOk<{ agents: ListAgent[] }>('list_agents', {});
|
|
451
|
-
return result.agents;
|
|
452
|
-
}
|
|
437
|
+
// ── Model control ──────────────────────────────────────────────────
|
|
453
438
|
|
|
454
|
-
async
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
void this.requestOk('shutdown', {}).catch(() => {
|
|
465
|
-
// Continue shutdown path if broker is already unhealthy or exits before replying.
|
|
439
|
+
async setModel(
|
|
440
|
+
name: string,
|
|
441
|
+
model: string,
|
|
442
|
+
opts?: { timeoutMs?: number }
|
|
443
|
+
): Promise<{ name: string; model: string; success: boolean }> {
|
|
444
|
+
return this.transport.request(`/api/spawned/${encodeURIComponent(name)}/model`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
body: JSON.stringify({ model, timeout_ms: opts?.timeoutMs }),
|
|
466
447
|
});
|
|
467
|
-
|
|
468
|
-
const child = this.child;
|
|
469
|
-
const wait = this.exitPromise ?? Promise.resolve();
|
|
470
|
-
const waitForExit = async (timeoutMs: number): Promise<boolean> => {
|
|
471
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
472
|
-
const result = await Promise.race([
|
|
473
|
-
wait.then(() => true),
|
|
474
|
-
new Promise<boolean>((resolve) => {
|
|
475
|
-
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
476
|
-
}),
|
|
477
|
-
]);
|
|
478
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
479
|
-
return result;
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
if (await waitForExit(this.options.shutdownTimeoutMs)) {
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
487
|
-
child.kill('SIGTERM');
|
|
488
|
-
}
|
|
489
|
-
if (await waitForExit(1_000)) {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
494
|
-
child.kill('SIGKILL');
|
|
495
|
-
}
|
|
496
|
-
await waitForExit(1_000);
|
|
497
448
|
}
|
|
498
449
|
|
|
499
|
-
|
|
500
|
-
if (!this.child) {
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
await this.exitPromise;
|
|
504
|
-
}
|
|
450
|
+
// ── Channels ───────────────────────────────────────────────────────
|
|
505
451
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
this.lastStderrLine = undefined;
|
|
512
|
-
|
|
513
|
-
const args = [
|
|
514
|
-
'init',
|
|
515
|
-
'--name',
|
|
516
|
-
this.options.brokerName,
|
|
517
|
-
...(this.options.channels.length > 0 ? ['--channels', this.options.channels.join(',')] : []),
|
|
518
|
-
...this.options.binaryArgs,
|
|
519
|
-
];
|
|
520
|
-
|
|
521
|
-
// Ensure the SDK bin directory (containing agent-relay-broker) is on
|
|
522
|
-
// PATH so spawned workers can find it without any user setup.
|
|
523
|
-
const env = { ...this.options.env };
|
|
524
|
-
if (isExplicitPath(this.options.binaryPath)) {
|
|
525
|
-
const binDir = path.dirname(path.resolve(resolvedBinary));
|
|
526
|
-
const currentPath = env.PATH ?? env.Path ?? '';
|
|
527
|
-
if (!currentPath.split(path.delimiter).includes(binDir)) {
|
|
528
|
-
env.PATH = `${binDir}${path.delimiter}${currentPath}`;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
console.error(`[broker] Starting: ${resolvedBinary} ${args.join(' ')}`);
|
|
533
|
-
const child = spawn(resolvedBinary, args, {
|
|
534
|
-
cwd: this.options.cwd,
|
|
535
|
-
env,
|
|
536
|
-
stdio: 'pipe',
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
this.child = child;
|
|
540
|
-
this.stdoutRl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
541
|
-
this.stderrRl = createInterface({ input: child.stderr, crlfDelay: Infinity });
|
|
542
|
-
|
|
543
|
-
this.stdoutRl.on('line', (line) => {
|
|
544
|
-
this.handleStdoutLine(line);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
this.stderrRl.on('line', (line) => {
|
|
548
|
-
const trimmed = line.trim();
|
|
549
|
-
if (trimmed) {
|
|
550
|
-
this.lastStderrLine = trimmed;
|
|
551
|
-
}
|
|
552
|
-
for (const listener of this.stderrListeners) {
|
|
553
|
-
listener(line);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
this.exitPromise = new Promise<void>((resolve) => {
|
|
558
|
-
// Use 'close' instead of 'exit' so that all buffered stderr/stdout
|
|
559
|
-
// data has been consumed before we build the error message. The
|
|
560
|
-
// 'exit' event fires when the process terminates, but stdio streams
|
|
561
|
-
// may still have unread data; 'close' fires after both the process
|
|
562
|
-
// exits AND all stdio streams have ended.
|
|
563
|
-
child.once('close', (code, signal) => {
|
|
564
|
-
const detail = this.lastStderrLine ? `: ${this.lastStderrLine}` : '';
|
|
565
|
-
const error = new AgentRelayProcessError(
|
|
566
|
-
`broker exited (code=${code ?? 'null'}, signal=${signal ?? 'null'})${detail}`
|
|
567
|
-
);
|
|
568
|
-
this.failAllPending(error);
|
|
569
|
-
this.disposeProcessHandles();
|
|
570
|
-
resolve();
|
|
571
|
-
});
|
|
572
|
-
child.once('error', (error) => {
|
|
573
|
-
this.failAllPending(error);
|
|
574
|
-
this.disposeProcessHandles();
|
|
575
|
-
resolve();
|
|
576
|
-
});
|
|
452
|
+
async subscribeChannels(name: string, channels: string[]): Promise<void> {
|
|
453
|
+
await this.transport.request(`/api/spawned/${encodeURIComponent(name)}/subscribe`, {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
body: JSON.stringify({ channels }),
|
|
577
456
|
});
|
|
578
|
-
|
|
579
|
-
const helloAck = await this.requestHello();
|
|
580
|
-
console.error('[broker] Broker ready (hello handshake complete)');
|
|
581
|
-
if (helloAck.workspace_key) {
|
|
582
|
-
this.workspaceKey = helloAck.workspace_key;
|
|
583
|
-
}
|
|
584
457
|
}
|
|
585
458
|
|
|
586
|
-
|
|
587
|
-
this.
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
this.lastStderrLine = undefined;
|
|
592
|
-
this.child = undefined;
|
|
593
|
-
this.exitPromise = undefined;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
private failAllPending(error: Error): void {
|
|
597
|
-
for (const pending of this.pending.values()) {
|
|
598
|
-
clearTimeout(pending.timeout);
|
|
599
|
-
pending.reject(error);
|
|
600
|
-
}
|
|
601
|
-
this.pending.clear();
|
|
459
|
+
async unsubscribeChannels(name: string, channels: string[]): Promise<void> {
|
|
460
|
+
await this.transport.request(`/api/spawned/${encodeURIComponent(name)}/unsubscribe`, {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
body: JSON.stringify({ channels }),
|
|
463
|
+
});
|
|
602
464
|
}
|
|
603
465
|
|
|
604
|
-
|
|
605
|
-
let parsed: ParsedEnvelope;
|
|
606
|
-
try {
|
|
607
|
-
parsed = JSON.parse(line) as ParsedEnvelope;
|
|
608
|
-
} catch {
|
|
609
|
-
// Non-protocol output should not crash the SDK.
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== 'string') {
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const envelope: ProtocolEnvelope<unknown> = {
|
|
621
|
-
v: parsed.v,
|
|
622
|
-
type: parsed.type,
|
|
623
|
-
request_id: parsed.request_id,
|
|
624
|
-
payload: parsed.payload,
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
if (envelope.type === 'event') {
|
|
628
|
-
const payload = envelope.payload as BrokerEvent;
|
|
629
|
-
this.eventBuffer.push(payload);
|
|
630
|
-
if (this.eventBuffer.length > this.maxBufferSize) {
|
|
631
|
-
this.eventBuffer.shift();
|
|
632
|
-
}
|
|
633
|
-
for (const listener of this.eventListeners) {
|
|
634
|
-
listener(payload);
|
|
635
|
-
}
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (!envelope.request_id) {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const pending = this.pending.get(envelope.request_id);
|
|
644
|
-
if (!pending) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (envelope.type === 'error') {
|
|
649
|
-
clearTimeout(pending.timeout);
|
|
650
|
-
this.pending.delete(envelope.request_id);
|
|
651
|
-
pending.reject(new AgentRelayProtocolError(envelope.payload as ProtocolError));
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
466
|
+
// ── Observability ──────────────────────────────────────────────────
|
|
654
467
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
pending.reject(
|
|
659
|
-
new AgentRelayProcessError(
|
|
660
|
-
`unexpected response type '${envelope.type}' for request '${envelope.request_id}' (expected '${pending.expectedType}')`
|
|
661
|
-
)
|
|
662
|
-
);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
clearTimeout(pending.timeout);
|
|
667
|
-
this.pending.delete(envelope.request_id);
|
|
668
|
-
pending.resolve(envelope);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
private async requestHello(): Promise<{
|
|
672
|
-
broker_version: string;
|
|
673
|
-
protocol_version: number;
|
|
674
|
-
workspace_key?: string;
|
|
468
|
+
async getMetrics(agent?: string): Promise<{
|
|
469
|
+
agents: Array<{ name: string; pid: number; memory_bytes: number; uptime_secs: number }>;
|
|
470
|
+
broker?: BrokerStats;
|
|
675
471
|
}> {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
client_version: this.options.clientVersion,
|
|
679
|
-
};
|
|
680
|
-
const frame = await this.sendRequest('hello', payload, 'hello_ack');
|
|
681
|
-
return frame.payload as { broker_version: string; protocol_version: number; workspace_key?: string };
|
|
472
|
+
const query = agent ? `?agent=${encodeURIComponent(agent)}` : '';
|
|
473
|
+
return this.transport.request(`/api/metrics${query}`);
|
|
682
474
|
}
|
|
683
475
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const result = frame.payload as { result: T };
|
|
687
|
-
return result.result;
|
|
476
|
+
async getStatus(): Promise<BrokerStatus> {
|
|
477
|
+
return this.transport.request<BrokerStatus>('/api/status');
|
|
688
478
|
}
|
|
689
479
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
expectedType: 'ok' | 'hello_ack'
|
|
694
|
-
): Promise<ProtocolEnvelope<unknown>> {
|
|
695
|
-
if (!this.child) {
|
|
696
|
-
throw new AgentRelayProcessError('broker is not running');
|
|
697
|
-
}
|
|
480
|
+
async getCrashInsights(): Promise<CrashInsightsResponse> {
|
|
481
|
+
return this.transport.request('/api/crash-insights');
|
|
482
|
+
}
|
|
698
483
|
|
|
699
|
-
|
|
700
|
-
const message: ProtocolEnvelope<unknown> = {
|
|
701
|
-
v: PROTOCOL_VERSION,
|
|
702
|
-
type,
|
|
703
|
-
request_id: requestId,
|
|
704
|
-
payload,
|
|
705
|
-
};
|
|
484
|
+
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
706
485
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
new AgentRelayProcessError(
|
|
712
|
-
`request timed out after ${this.options.requestTimeoutMs}ms (type='${type}', request_id='${requestId}')`
|
|
713
|
-
)
|
|
714
|
-
);
|
|
715
|
-
}, this.options.requestTimeoutMs);
|
|
716
|
-
|
|
717
|
-
this.pending.set(requestId, {
|
|
718
|
-
expectedType,
|
|
719
|
-
resolve,
|
|
720
|
-
reject,
|
|
721
|
-
timeout,
|
|
722
|
-
});
|
|
486
|
+
async preflight(agents: Array<{ name: string; cli: string }>): Promise<{ queued: number }> {
|
|
487
|
+
return this.transport.request('/api/preflight', {
|
|
488
|
+
method: 'POST',
|
|
489
|
+
body: JSON.stringify({ agents }),
|
|
723
490
|
});
|
|
724
|
-
|
|
725
|
-
const line = `${JSON.stringify(message)}\n`;
|
|
726
|
-
if (!this.child.stdin.write(line)) {
|
|
727
|
-
await once(this.child.stdin, 'drain');
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
return responsePromise;
|
|
731
491
|
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const CLI_MODEL_FLAG_CLIS = new Set(['claude', 'codex', 'gemini', 'goose', 'aider']);
|
|
735
|
-
|
|
736
|
-
const CLI_DEFAULT_ARGS: Record<string, string[]> = {
|
|
737
|
-
codex: ['-c', 'check_for_update_on_startup=false'],
|
|
738
|
-
};
|
|
739
492
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const defaultArgs = CLI_DEFAULT_ARGS[cliName] ?? [];
|
|
743
|
-
const baseArgs = [...defaultArgs, ...args];
|
|
744
|
-
if (!model) {
|
|
745
|
-
return baseArgs;
|
|
493
|
+
async renewLease(): Promise<{ renewed: boolean; expires_in_secs: number }> {
|
|
494
|
+
return this.transport.request('/api/session/renew', { method: 'POST' });
|
|
746
495
|
}
|
|
747
|
-
if (!CLI_MODEL_FLAG_CLIS.has(cliName)) {
|
|
748
|
-
return baseArgs;
|
|
749
|
-
}
|
|
750
|
-
if (hasModelArg(baseArgs)) {
|
|
751
|
-
return baseArgs;
|
|
752
|
-
}
|
|
753
|
-
return ['--model', model, ...baseArgs];
|
|
754
|
-
}
|
|
755
496
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
497
|
+
/**
|
|
498
|
+
* Shut down and clean up.
|
|
499
|
+
* - For spawned brokers (via .spawn()): sends POST /api/shutdown to kill the broker, waits for exit.
|
|
500
|
+
* - For connected brokers (via .connect() or constructor): just disconnects the transport.
|
|
501
|
+
* Does NOT kill the broker — the caller doesn't own it.
|
|
502
|
+
*/
|
|
503
|
+
async shutdown(): Promise<void> {
|
|
504
|
+
if (this.leaseTimer) {
|
|
505
|
+
clearInterval(this.leaseTimer);
|
|
506
|
+
this.leaseTimer = null;
|
|
764
507
|
}
|
|
765
|
-
}
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function expandTilde(p: string): string {
|
|
770
|
-
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
|
|
771
|
-
const home = os.homedir();
|
|
772
|
-
return path.join(home, p.slice(2));
|
|
773
|
-
}
|
|
774
|
-
return p;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function isExplicitPath(binaryPath: string): boolean {
|
|
778
|
-
return (
|
|
779
|
-
binaryPath.includes('/') ||
|
|
780
|
-
binaryPath.includes('\\') ||
|
|
781
|
-
binaryPath.startsWith('.') ||
|
|
782
|
-
binaryPath.startsWith('~')
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function detectPlatformSuffix(): string | null {
|
|
787
|
-
const platformMap: Record<string, Record<string, string>> = {
|
|
788
|
-
darwin: { arm64: 'darwin-arm64', x64: 'darwin-x64' },
|
|
789
|
-
linux: { arm64: 'linux-arm64', x64: 'linux-x64' },
|
|
790
|
-
win32: { x64: 'win32-x64' },
|
|
791
|
-
};
|
|
792
|
-
return platformMap[process.platform]?.[process.arch] ?? null;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function getLatestVersionSync(): string | null {
|
|
796
|
-
try {
|
|
797
|
-
const result = execSync('curl -fsSL https://api.github.com/repos/AgentWorkforce/relay/releases/latest', {
|
|
798
|
-
timeout: 15_000,
|
|
799
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
800
|
-
}).toString();
|
|
801
|
-
const match = result.match(/"tag_name"\s*:\s*"([^"]+)"/);
|
|
802
|
-
if (!match?.[1]) return null;
|
|
803
|
-
// Strip tag prefixes: "openclaw-v3.1.18" -> "3.1.18", "v3.1.18" -> "3.1.18"
|
|
804
|
-
return match[1].replace(/^openclaw-/, '').replace(/^v/, '');
|
|
805
|
-
} catch {
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
508
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (!suffix) {
|
|
813
|
-
throw new AgentRelayProcessError(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
817
|
-
const installDir = path.join(homeDir, '.agent-relay', 'bin');
|
|
818
|
-
const brokerExe = process.platform === 'win32' ? 'agent-relay-broker.exe' : 'agent-relay-broker';
|
|
819
|
-
const targetPath = path.join(installDir, brokerExe);
|
|
820
|
-
|
|
821
|
-
console.log(`[agent-relay] Broker binary not found, installing for ${suffix}...`);
|
|
822
|
-
|
|
823
|
-
const version = getLatestVersionSync();
|
|
824
|
-
if (!version) {
|
|
825
|
-
throw new AgentRelayProcessError(
|
|
826
|
-
'Failed to fetch latest agent-relay version from GitHub.\n' +
|
|
827
|
-
'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash'
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const binaryName = `agent-relay-broker-${suffix}`;
|
|
832
|
-
const downloadUrl = `https://github.com/AgentWorkforce/relay/releases/download/v${version}/${binaryName}`;
|
|
833
|
-
|
|
834
|
-
console.log(`[agent-relay] Downloading v${version} from ${downloadUrl}`);
|
|
835
|
-
|
|
836
|
-
try {
|
|
837
|
-
fs.mkdirSync(installDir, { recursive: true });
|
|
838
|
-
execSync(`curl -fsSL "${downloadUrl}" -o "${targetPath}"`, {
|
|
839
|
-
timeout: 60_000,
|
|
840
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
841
|
-
});
|
|
842
|
-
fs.chmodSync(targetPath, 0o755);
|
|
843
|
-
|
|
844
|
-
// macOS: strip quarantine attribute and re-sign to avoid Gatekeeper issues
|
|
845
|
-
if (process.platform === 'darwin') {
|
|
846
|
-
try {
|
|
847
|
-
execSync(`xattr -d com.apple.quarantine "${targetPath}" 2>/dev/null || true`, {
|
|
848
|
-
timeout: 10_000,
|
|
849
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
850
|
-
});
|
|
851
|
-
} catch {
|
|
852
|
-
// Non-fatal
|
|
853
|
-
}
|
|
509
|
+
// Only send the shutdown command if we own the broker process
|
|
510
|
+
if (this.child) {
|
|
854
511
|
try {
|
|
855
|
-
|
|
856
|
-
timeout: 10_000,
|
|
857
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
858
|
-
});
|
|
512
|
+
await this.transport.request('/api/shutdown', { method: 'POST' });
|
|
859
513
|
} catch {
|
|
860
|
-
//
|
|
514
|
+
// Broker may already be dead
|
|
861
515
|
}
|
|
862
516
|
}
|
|
863
517
|
|
|
864
|
-
|
|
865
|
-
execSync(`"${targetPath}" --help`, { timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
866
|
-
} catch (err) {
|
|
867
|
-
try {
|
|
868
|
-
fs.unlinkSync(targetPath);
|
|
869
|
-
} catch {
|
|
870
|
-
/* ignore */
|
|
871
|
-
}
|
|
872
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
873
|
-
throw new AgentRelayProcessError(
|
|
874
|
-
`Failed to install broker binary: ${message}\n` +
|
|
875
|
-
'Install manually: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash'
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
console.log(`[agent-relay] Broker installed to ${targetPath}`);
|
|
880
|
-
return targetPath;
|
|
881
|
-
}
|
|
518
|
+
this.transport.disconnect();
|
|
882
519
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// 1. In a source checkout, prefer Cargo's release binary to avoid stale bundled
|
|
888
|
-
// copies when local dev rebuilds happen while broker processes are running.
|
|
889
|
-
const workspaceRelease = path.resolve(moduleDir, '..', '..', '..', 'target', 'release', brokerExe);
|
|
890
|
-
if (fs.existsSync(workspaceRelease)) {
|
|
891
|
-
return workspaceRelease;
|
|
520
|
+
if (this.child) {
|
|
521
|
+
await waitForExit(this.child, 5000);
|
|
522
|
+
this.child = null;
|
|
523
|
+
}
|
|
892
524
|
}
|
|
893
525
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (suffix) {
|
|
900
|
-
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
901
|
-
const platformBinary = path.join(binDir, `agent-relay-broker-${suffix}${ext}`);
|
|
902
|
-
if (fs.existsSync(platformBinary)) {
|
|
903
|
-
return platformBinary;
|
|
526
|
+
/** Disconnect without shutting down the broker. Alias for cases where the intent is clear. */
|
|
527
|
+
disconnect(): void {
|
|
528
|
+
if (this.leaseTimer) {
|
|
529
|
+
clearInterval(this.leaseTimer);
|
|
530
|
+
this.leaseTimer = null;
|
|
904
531
|
}
|
|
532
|
+
this.transport.disconnect();
|
|
905
533
|
}
|
|
906
534
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const standaloneBroker = path.join(homeDir, '.agent-relay', 'bin', brokerExe);
|
|
910
|
-
if (fs.existsSync(standaloneBroker)) {
|
|
911
|
-
return standaloneBroker;
|
|
535
|
+
async getConfig(): Promise<{ workspaceKey?: string }> {
|
|
536
|
+
return this.transport.request('/api/config');
|
|
912
537
|
}
|
|
913
|
-
|
|
914
|
-
// 4. Auto-install from GitHub releases
|
|
915
|
-
return installBrokerBinary();
|
|
916
538
|
}
|
|
917
539
|
|
|
918
|
-
//
|
|
919
|
-
// HTTP transport client — connects to an already-running broker's HTTP API
|
|
920
|
-
// ---------------------------------------------------------------------------
|
|
540
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
921
541
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Parse the API URL from the broker's stdout. The broker prints:
|
|
544
|
+
* [agent-relay] API listening on http://{bind}:{port}
|
|
545
|
+
* Returns the full URL (e.g. "http://127.0.0.1:3889").
|
|
546
|
+
*/
|
|
547
|
+
async function waitForApiUrl(child: ChildProcess, timeoutMs: number): Promise<string> {
|
|
548
|
+
const { createInterface } = await import('node:readline');
|
|
926
549
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
autoStart?: boolean;
|
|
932
|
-
/**
|
|
933
|
-
* Path to the broker binary for auto-start.
|
|
934
|
-
* If not provided, the SDK resolves it automatically via standard install locations
|
|
935
|
-
* (~/.agent-relay/bin, bundled platform binary, or Cargo release build).
|
|
936
|
-
* Only used when `autoStart: true`.
|
|
937
|
-
*/
|
|
938
|
-
brokerBinaryPath?: string;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const DEFAULT_DASHBOARD_PORT = (() => {
|
|
942
|
-
const envPort = typeof process !== 'undefined' ? process.env.AGENT_RELAY_DASHBOARD_PORT : undefined;
|
|
943
|
-
if (envPort) {
|
|
944
|
-
const parsed = Number.parseInt(envPort, 10);
|
|
945
|
-
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
946
|
-
}
|
|
947
|
-
return 3888;
|
|
948
|
-
})();
|
|
949
|
-
const HTTP_MAX_PORT_SCAN = 25;
|
|
950
|
-
const HTTP_AUTOSTART_TIMEOUT_MS = 10_000;
|
|
951
|
-
const HTTP_AUTOSTART_POLL_MS = 250;
|
|
952
|
-
|
|
953
|
-
function sanitizeBrokerName(name: string): string {
|
|
954
|
-
return name.replace(/[^\p{L}\p{N}-]/gu, '-');
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
function brokerPidFilename(projectRoot: string): string {
|
|
958
|
-
const brokerName = path.basename(projectRoot) || 'project';
|
|
959
|
-
return `broker-${sanitizeBrokerName(brokerName)}.pid`;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
export class HttpAgentRelayClient {
|
|
963
|
-
private readonly port: number;
|
|
964
|
-
private readonly apiKey?: string;
|
|
965
|
-
|
|
966
|
-
constructor(options: HttpAgentRelayClientOptions) {
|
|
967
|
-
this.port = options.port;
|
|
968
|
-
this.apiKey = options.apiKey;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Connect to an already-running broker on the given port.
|
|
973
|
-
*/
|
|
974
|
-
static async connectHttp(
|
|
975
|
-
port: number,
|
|
976
|
-
options?: { apiKey?: string }
|
|
977
|
-
): Promise<HttpAgentRelayClient> {
|
|
978
|
-
const client = new HttpAgentRelayClient({ port, apiKey: options?.apiKey });
|
|
979
|
-
// Verify connectivity
|
|
980
|
-
await client.healthCheck();
|
|
981
|
-
return client;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
/**
|
|
985
|
-
* Discover a running broker for the current project and connect to it.
|
|
986
|
-
* Reads the broker PID file, verifies the process is alive, scans ports
|
|
987
|
-
* for the HTTP API, and returns a connected client.
|
|
988
|
-
*/
|
|
989
|
-
static async discoverAndConnect(
|
|
990
|
-
options?: DiscoverAndConnectOptions
|
|
991
|
-
): Promise<HttpAgentRelayClient> {
|
|
992
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
993
|
-
const apiKey = options?.apiKey ?? process.env.RELAY_BROKER_API_KEY?.trim();
|
|
994
|
-
const autoStart = options?.autoStart ?? false;
|
|
995
|
-
const paths = getProjectPaths(cwd);
|
|
996
|
-
const preferredApiPort = DEFAULT_DASHBOARD_PORT + 1;
|
|
997
|
-
|
|
998
|
-
// Try to find a running broker via PID file
|
|
999
|
-
const pidFilePath = path.join(paths.dataDir, brokerPidFilename(paths.projectRoot));
|
|
1000
|
-
const legacyPidPath = path.join(paths.dataDir, 'broker.pid');
|
|
1001
|
-
let brokerRunning = false;
|
|
1002
|
-
|
|
1003
|
-
for (const pidPath of [pidFilePath, legacyPidPath]) {
|
|
1004
|
-
if (fs.existsSync(pidPath)) {
|
|
1005
|
-
const pidStr = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
1006
|
-
const pid = Number.parseInt(pidStr, 10);
|
|
1007
|
-
if (Number.isFinite(pid) && pid > 0) {
|
|
1008
|
-
try {
|
|
1009
|
-
process.kill(pid, 0);
|
|
1010
|
-
brokerRunning = true;
|
|
1011
|
-
break;
|
|
1012
|
-
} catch {
|
|
1013
|
-
// Process not running
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (brokerRunning) {
|
|
1020
|
-
const port = await HttpAgentRelayClient.scanForBrokerPort(preferredApiPort);
|
|
1021
|
-
if (port !== null) {
|
|
1022
|
-
return new HttpAgentRelayClient({ port, apiKey });
|
|
1023
|
-
}
|
|
1024
|
-
throw new AgentRelayProcessError(
|
|
1025
|
-
'broker is running for this project, but its local API is unavailable'
|
|
1026
|
-
);
|
|
550
|
+
return new Promise<string>((resolve, reject) => {
|
|
551
|
+
if (!child.stdout) {
|
|
552
|
+
reject(new Error('Broker stdout not available'));
|
|
553
|
+
return;
|
|
1027
554
|
}
|
|
1028
555
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
556
|
+
let resolved = false;
|
|
557
|
+
const rl = createInterface({ input: child.stdout });
|
|
1032
558
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const child = spawn(
|
|
1040
|
-
brokerBinary,
|
|
1041
|
-
['init', '--persist', '--api-port', String(preferredApiPort)],
|
|
1042
|
-
{
|
|
1043
|
-
cwd: paths.projectRoot,
|
|
1044
|
-
env: process.env,
|
|
1045
|
-
detached: true,
|
|
1046
|
-
stdio: 'ignore',
|
|
559
|
+
const timer = setTimeout(() => {
|
|
560
|
+
if (!resolved) {
|
|
561
|
+
resolved = true;
|
|
562
|
+
rl.close();
|
|
563
|
+
reject(new Error(`Broker did not report API port within ${timeoutMs}ms`));
|
|
1047
564
|
}
|
|
1048
|
-
);
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
565
|
+
}, timeoutMs);
|
|
566
|
+
|
|
567
|
+
child.on('exit', (code) => {
|
|
568
|
+
if (!resolved) {
|
|
569
|
+
resolved = true;
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
rl.close();
|
|
572
|
+
reject(new Error(`Broker process exited with code ${code} before becoming ready`));
|
|
1056
573
|
}
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
throw new AgentRelayProcessError(
|
|
1061
|
-
`broker did not become ready within ${HTTP_AUTOSTART_TIMEOUT_MS}ms`
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
574
|
+
});
|
|
1064
575
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const payload = (await res.json().catch(() => null)) as { service?: string } | null;
|
|
1072
|
-
if (payload?.service === 'agent-relay-listen') {
|
|
1073
|
-
return port;
|
|
1074
|
-
}
|
|
1075
|
-
} catch {
|
|
1076
|
-
// Keep scanning
|
|
576
|
+
child.on('error', (err) => {
|
|
577
|
+
if (!resolved) {
|
|
578
|
+
resolved = true;
|
|
579
|
+
clearTimeout(timer);
|
|
580
|
+
rl.close();
|
|
581
|
+
reject(new Error(`Failed to start broker: ${err.message}`));
|
|
1077
582
|
}
|
|
1078
|
-
}
|
|
1079
|
-
return null;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
private async request<T = unknown>(pathname: string, init?: RequestInit): Promise<T> {
|
|
1083
|
-
const headers = new Headers(init?.headers);
|
|
1084
|
-
if (this.apiKey && !headers.has('x-api-key') && !headers.has('authorization')) {
|
|
1085
|
-
headers.set('x-api-key', this.apiKey);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const response = await fetch(`http://127.0.0.1:${this.port}${pathname}`, {
|
|
1089
|
-
...init,
|
|
1090
|
-
headers,
|
|
1091
583
|
});
|
|
1092
584
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
try {
|
|
1096
|
-
payload = text ? JSON.parse(text) : undefined;
|
|
1097
|
-
} catch {
|
|
1098
|
-
payload = text;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
if (!response.ok) {
|
|
1102
|
-
const msg = HttpAgentRelayClient.extractErrorMessage(response, payload);
|
|
1103
|
-
throw new AgentRelayProcessError(msg);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return payload as T;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
private static extractErrorMessage(response: Response, payload: unknown): string {
|
|
1110
|
-
if (typeof payload === 'string' && payload.trim()) return payload.trim();
|
|
1111
|
-
const p = payload as Record<string, unknown> | undefined;
|
|
1112
|
-
if (typeof p?.error === 'string') return p.error;
|
|
1113
|
-
if (typeof (p?.error as Record<string, unknown>)?.message === 'string')
|
|
1114
|
-
return (p!.error as Record<string, unknown>).message as string;
|
|
1115
|
-
if (typeof p?.message === 'string' && (p.message as string).trim())
|
|
1116
|
-
return (p.message as string).trim();
|
|
1117
|
-
return `${response.status} ${response.statusText}`.trim();
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
async healthCheck(): Promise<{ service: string }> {
|
|
1121
|
-
return this.request<{ service: string }>('/health');
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/** No-op — broker is already running. */
|
|
1125
|
-
async start(): Promise<void> {}
|
|
1126
|
-
|
|
1127
|
-
/** No-op — don't kill an externally-managed broker. */
|
|
1128
|
-
async shutdown(): Promise<void> {}
|
|
585
|
+
rl.on('line', (line) => {
|
|
586
|
+
if (resolved) return;
|
|
1129
587
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
model: input.model,
|
|
1138
|
-
args: input.args ?? [],
|
|
1139
|
-
task: input.task,
|
|
1140
|
-
channels: input.channels ?? [],
|
|
1141
|
-
cwd: input.cwd,
|
|
1142
|
-
team: input.team,
|
|
1143
|
-
shadowOf: input.shadowOf,
|
|
1144
|
-
shadowMode: input.shadowMode,
|
|
1145
|
-
continueFrom: input.continueFrom,
|
|
1146
|
-
idleThresholdSecs: input.idleThresholdSecs,
|
|
1147
|
-
restartPolicy: input.restartPolicy,
|
|
1148
|
-
skipRelayPrompt: input.skipRelayPrompt,
|
|
1149
|
-
}),
|
|
588
|
+
const match = line.match(/API listening on (https?:\/\/[^\s]+)/);
|
|
589
|
+
if (match) {
|
|
590
|
+
resolved = true;
|
|
591
|
+
clearTimeout(timer);
|
|
592
|
+
rl.close();
|
|
593
|
+
resolve(match[1]);
|
|
594
|
+
}
|
|
1150
595
|
});
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
runtime: 'pty',
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
1156
598
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
mode: input.mode,
|
|
1171
|
-
}),
|
|
599
|
+
function waitForExit(child: ChildProcess, timeoutMs: number): Promise<void> {
|
|
600
|
+
return new Promise((resolve) => {
|
|
601
|
+
if (child.exitCode !== null) {
|
|
602
|
+
resolve();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const timer = setTimeout(() => {
|
|
606
|
+
child.kill('SIGKILL');
|
|
607
|
+
resolve();
|
|
608
|
+
}, timeoutMs);
|
|
609
|
+
child.on('exit', () => {
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
resolve();
|
|
1172
612
|
});
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
async listAgents(): Promise<ListAgent[]> {
|
|
1176
|
-
const payload = await this.request<{ agents?: ListAgent[] }>('/api/spawned', { method: 'GET' });
|
|
1177
|
-
return Array.isArray(payload?.agents) ? payload.agents : [];
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
async release(name: string, reason?: string): Promise<{ name: string }> {
|
|
1181
|
-
const payload = await this.request<{ name?: string }>(
|
|
1182
|
-
`/api/spawned/${encodeURIComponent(name)}`,
|
|
1183
|
-
{
|
|
1184
|
-
method: 'DELETE',
|
|
1185
|
-
...(reason
|
|
1186
|
-
? { headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason }) }
|
|
1187
|
-
: {}),
|
|
1188
|
-
}
|
|
1189
|
-
);
|
|
1190
|
-
return { name: typeof payload?.name === 'string' ? payload.name : name };
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
async subscribeChannels(_name: string, _channels: string[]): Promise<void> {
|
|
1194
|
-
throw new Error(
|
|
1195
|
-
'subscribeChannels is only available via the broker protocol (BrokerAgentRelayClient). ' +
|
|
1196
|
-
'The HTTP API does not support dynamic channel subscription.'
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
async unsubscribeChannels(_name: string, _channels: string[]): Promise<void> {
|
|
1201
|
-
throw new Error(
|
|
1202
|
-
'unsubscribeChannels is only available via the broker protocol (BrokerAgentRelayClient). ' +
|
|
1203
|
-
'The HTTP API does not support dynamic channel unsubscription.'
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
async setModel(
|
|
1208
|
-
name: string,
|
|
1209
|
-
model: string,
|
|
1210
|
-
opts?: { timeoutMs?: number }
|
|
1211
|
-
): Promise<{ name: string; model: string; success: boolean }> {
|
|
1212
|
-
const payload = await this.request<{ success?: boolean; model?: string }>(
|
|
1213
|
-
`/api/spawned/${encodeURIComponent(name)}/model`,
|
|
1214
|
-
{
|
|
1215
|
-
method: 'POST',
|
|
1216
|
-
headers: { 'content-type': 'application/json' },
|
|
1217
|
-
body: JSON.stringify({ model, timeoutMs: opts?.timeoutMs }),
|
|
1218
|
-
}
|
|
1219
|
-
);
|
|
1220
|
-
return {
|
|
1221
|
-
name,
|
|
1222
|
-
model: typeof payload?.model === 'string' ? payload.model : model,
|
|
1223
|
-
success: payload?.success !== false,
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
async getConfig(): Promise<{ workspace_key?: string }> {
|
|
1228
|
-
return this.request<{ workspace_key?: string }>('/api/config');
|
|
1229
|
-
}
|
|
613
|
+
});
|
|
1230
614
|
}
|