agent-relay-orchestrator 0.104.2 → 0.104.5
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 +3 -3
- package/src/shared-callmux.ts +93 -109
- package/vendor/callmux/bin/callmux.js +0 -47579
- package/vendor/callmux/package.json +0 -11
- package/vendor/callmux/schema.json +0 -868
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.104.
|
|
3
|
+
"version": "0.104.5",
|
|
4
4
|
"description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"src/**/*.ts",
|
|
11
11
|
"!src/**/*.test.ts",
|
|
12
|
-
"vendor/**",
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
@@ -18,7 +17,8 @@
|
|
|
18
17
|
},
|
|
19
18
|
"dependencies": {
|
|
20
19
|
"agent-relay-providers": "0.104.0",
|
|
21
|
-
"agent-relay-sdk": "0.2.
|
|
20
|
+
"agent-relay-sdk": "0.2.90",
|
|
21
|
+
"callmux": "0.23.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/bun": "latest",
|
package/src/shared-callmux.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname,
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
5
|
+
import { createListener, type CallmuxConfig, type CreateListenerOptions, type ListenerHealthSnapshot, type ProgrammaticListener } from "callmux";
|
|
5
6
|
import type { OrchestratorConfig } from "./config";
|
|
6
7
|
import { agentRelayHome } from "./config";
|
|
7
8
|
|
|
@@ -10,14 +11,13 @@ export const SHARED_CALLMUX_HOST_ENV = "AGENT_RELAY_SHARED_CALLMUX_HOST";
|
|
|
10
11
|
export const SHARED_CALLMUX_PORT_ENV = "AGENT_RELAY_SHARED_CALLMUX_PORT";
|
|
11
12
|
export const SHARED_CALLMUX_CONFIG_ENV = "AGENT_RELAY_SHARED_CALLMUX_CONFIG";
|
|
12
13
|
export const SHARED_CALLMUX_SOURCE_CONFIG_ENV = "AGENT_RELAY_SHARED_CALLMUX_SOURCE_CONFIG";
|
|
13
|
-
export const SHARED_CALLMUX_COMMAND_ENV = "AGENT_RELAY_SHARED_CALLMUX_COMMAND";
|
|
14
14
|
export const SHARED_CALLMUX_ENABLE_ENV = "AGENT_RELAY_SHARED_CALLMUX_ENABLE";
|
|
15
15
|
|
|
16
16
|
export const DEFAULT_SHARED_CALLMUX_HOST = "127.0.0.1";
|
|
17
17
|
export const DEFAULT_SHARED_CALLMUX_PORT = 4861;
|
|
18
18
|
export const SHARED_CALLMUX_SERVER_NAMES = ["tokenlean", "github"] as const;
|
|
19
19
|
|
|
20
|
-
const CONFIG_SCHEMA = "
|
|
20
|
+
const CONFIG_SCHEMA = "callmux/schema.json";
|
|
21
21
|
const GITHUB_TOOLS = [
|
|
22
22
|
"issue_read",
|
|
23
23
|
"issue_write",
|
|
@@ -30,7 +30,6 @@ const GITHUB_TOOLS = [
|
|
|
30
30
|
];
|
|
31
31
|
|
|
32
32
|
export interface SharedCallmuxOptions {
|
|
33
|
-
command: string;
|
|
34
33
|
host: string;
|
|
35
34
|
port: number;
|
|
36
35
|
url: string;
|
|
@@ -39,16 +38,8 @@ export interface SharedCallmuxOptions {
|
|
|
39
38
|
enabled: boolean;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
export interface SharedCallmuxProcess {
|
|
43
|
-
pid?: number;
|
|
44
|
-
exited: Promise<number | null>;
|
|
45
|
-
kill(signal?: NodeJS.Signals): void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
export interface SharedCallmuxSupervisorDeps {
|
|
49
|
-
|
|
50
|
-
spawn(command: string, args: string[], opts: { env: Record<string, string>; cwd: string }): SharedCallmuxProcess;
|
|
51
|
-
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
42
|
+
createListener(options: CreateListenerOptions): Promise<ProgrammaticListener>;
|
|
52
43
|
setInterval(fn: () => void, ms: number): Timer;
|
|
53
44
|
clearInterval(timer: Timer): void;
|
|
54
45
|
setTimeout(fn: () => void, ms: number): Timer;
|
|
@@ -58,11 +49,10 @@ export interface SharedCallmuxSupervisorDeps {
|
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
export interface SharedCallmuxHealthSnapshot {
|
|
61
|
-
state: "disabled" | "
|
|
52
|
+
state: "disabled" | "starting" | "running" | "unhealthy" | "restarting" | "stopped";
|
|
62
53
|
url: string;
|
|
63
|
-
command?: string;
|
|
64
54
|
reason?: string;
|
|
65
|
-
|
|
55
|
+
downstream?: ListenerHealthSnapshot["downstream"];
|
|
66
56
|
}
|
|
67
57
|
|
|
68
58
|
export function sharedCallmuxOptionsFromEnv(env: Record<string, string | undefined> = process.env): SharedCallmuxOptions {
|
|
@@ -72,7 +62,6 @@ export function sharedCallmuxOptionsFromEnv(env: Record<string, string | undefin
|
|
|
72
62
|
const port = numberEnv(env[SHARED_CALLMUX_PORT_ENV]) ?? parsed?.port ?? DEFAULT_SHARED_CALLMUX_PORT;
|
|
73
63
|
const url = explicitUrl ?? `http://${host}:${port}/mcp`;
|
|
74
64
|
return {
|
|
75
|
-
command: env[SHARED_CALLMUX_COMMAND_ENV] || bundledCallmuxCommand() || "callmux",
|
|
76
65
|
host,
|
|
77
66
|
port,
|
|
78
67
|
url,
|
|
@@ -86,30 +75,31 @@ export function sharedMcpListenerUrl(): string {
|
|
|
86
75
|
return sharedCallmuxOptionsFromEnv().url;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
export function writeSharedCallmuxConfig(opts: Pick<SharedCallmuxOptions, "configPath" | "sourceConfigPath">):
|
|
78
|
+
export function writeSharedCallmuxConfig(opts: Pick<SharedCallmuxOptions, "configPath" | "sourceConfigPath">): CallmuxConfig {
|
|
90
79
|
const source = readJsonObject(opts.sourceConfigPath);
|
|
91
80
|
const sourceServers = isRecord(source.servers) ? source.servers : {};
|
|
92
|
-
const servers = {
|
|
81
|
+
const servers: CallmuxConfig["servers"] = {
|
|
93
82
|
tokenlean: cloneServer(sourceServers.tokenlean) ?? defaultTokenleanServer(),
|
|
94
83
|
github: cloneServer(sourceServers.github) ?? defaultGithubServer(),
|
|
95
84
|
};
|
|
96
|
-
const generated:
|
|
97
|
-
$schema: CONFIG_SCHEMA,
|
|
85
|
+
const generated: CallmuxConfig = {
|
|
98
86
|
servers,
|
|
99
87
|
cacheTtlSeconds: numberFromRecord(source, "cacheTtlSeconds") ?? 10,
|
|
100
88
|
maxConcurrency: numberFromRecord(source, "maxConcurrency") ?? 20,
|
|
101
89
|
callTimeoutMs: numberFromRecord(source, "callTimeoutMs") ?? 180_000,
|
|
102
|
-
outputFormat:
|
|
90
|
+
outputFormat: outputFormatFromRecord(source, "outputFormat") ?? "auto",
|
|
103
91
|
// Relay workers consume only proxied tokenlean+github tools; suppress callmux meta-tools.
|
|
104
92
|
exposeMetaTools: false,
|
|
105
93
|
};
|
|
94
|
+
const persisted = { $schema: CONFIG_SCHEMA, ...generated };
|
|
106
95
|
mkdirSync(dirname(opts.configPath), { recursive: true });
|
|
107
|
-
writeFileSync(opts.configPath, JSON.stringify(
|
|
96
|
+
writeFileSync(opts.configPath, JSON.stringify(persisted, null, 2) + "\n", { mode: 0o600 });
|
|
108
97
|
return generated;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
100
|
export class SharedCallmuxSupervisor {
|
|
112
|
-
private
|
|
101
|
+
private listener: ProgrammaticListener | null = null;
|
|
102
|
+
private readonly onStatus = (snapshot: ListenerHealthSnapshot) => this.reportCallmuxHealth(snapshot);
|
|
113
103
|
private healthTimer: Timer | null = null;
|
|
114
104
|
private restartTimer: Timer | null = null;
|
|
115
105
|
private stopping = false;
|
|
@@ -131,13 +121,8 @@ export class SharedCallmuxSupervisor {
|
|
|
131
121
|
return;
|
|
132
122
|
}
|
|
133
123
|
this.deps.log(`[orchestrator] Shared callmux listener: ${this.opts.url}`);
|
|
134
|
-
if (!this.deps.which(this.opts.command)) {
|
|
135
|
-
this.deps.log("[orchestrator] shared callmux not found — shared listener dormant");
|
|
136
|
-
this.report("missing", "command not found");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
124
|
this.report("starting");
|
|
140
|
-
this.
|
|
125
|
+
void this.open();
|
|
141
126
|
this.healthTimer = this.deps.setInterval(() => {
|
|
142
127
|
void this.checkHealth();
|
|
143
128
|
}, this.timing.healthIntervalMs ?? 10_000);
|
|
@@ -149,86 +134,85 @@ export class SharedCallmuxSupervisor {
|
|
|
149
134
|
if (this.restartTimer) this.deps.clearTimeout(this.restartTimer);
|
|
150
135
|
this.healthTimer = null;
|
|
151
136
|
this.restartTimer = null;
|
|
152
|
-
if (this.
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
137
|
+
if (this.listener) {
|
|
138
|
+
const listener = this.listener;
|
|
139
|
+
this.listener = null;
|
|
140
|
+
listener.off("status", this.onStatus);
|
|
141
|
+
void listener.stop().catch((err) => this.deps.log(`[orchestrator] Shared callmux listener stop failed: ${errMessage(err)}`));
|
|
155
142
|
}
|
|
156
143
|
}
|
|
157
144
|
|
|
158
145
|
async checkHealth(): Promise<boolean> {
|
|
159
146
|
if (!this.opts.enabled) return true;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ok = response.ok;
|
|
165
|
-
} catch {
|
|
166
|
-
ok = false;
|
|
167
|
-
}
|
|
168
|
-
if (ok) {
|
|
147
|
+
const snapshot = this.listener?.health();
|
|
148
|
+
if (!snapshot) return false;
|
|
149
|
+
this.reportCallmuxHealth(snapshot);
|
|
150
|
+
if (snapshot.state === "running" || snapshot.state === "degraded" || snapshot.state === "reloading") {
|
|
169
151
|
this.backoffMs = this.timing.restartBaseMs ?? 1_000;
|
|
170
|
-
|
|
171
|
-
return true;
|
|
152
|
+
return snapshot.state === "running";
|
|
172
153
|
}
|
|
173
|
-
this.deps.log(`[orchestrator] Shared callmux
|
|
174
|
-
this.
|
|
175
|
-
this.restart("readiness failed");
|
|
154
|
+
this.deps.log(`[orchestrator] Shared callmux listener is ${snapshot.state}; restarting`);
|
|
155
|
+
this.restart(`listener ${snapshot.state}`);
|
|
176
156
|
return false;
|
|
177
157
|
}
|
|
178
158
|
|
|
179
|
-
private
|
|
180
|
-
if (this.stopping || this.
|
|
181
|
-
let proc: SharedCallmuxProcess;
|
|
159
|
+
private async open(): Promise<void> {
|
|
160
|
+
if (this.stopping || this.listener) return;
|
|
182
161
|
try {
|
|
183
|
-
writeSharedCallmuxConfig(this.opts);
|
|
184
|
-
const
|
|
185
|
-
const env = {
|
|
186
|
-
...process.env as Record<string, string>,
|
|
162
|
+
const persistedConfig = writeSharedCallmuxConfig(this.opts);
|
|
163
|
+
const config = withRuntimeEnv(persistedConfig, {
|
|
187
164
|
...this.config.env,
|
|
188
165
|
[SHARED_MCP_URL_ENV]: this.opts.url,
|
|
189
|
-
};
|
|
190
|
-
|
|
166
|
+
});
|
|
167
|
+
const listener = await this.deps.createListener({
|
|
168
|
+
host: this.opts.host,
|
|
169
|
+
port: this.opts.port,
|
|
170
|
+
config,
|
|
171
|
+
configPath: this.opts.configPath,
|
|
172
|
+
});
|
|
173
|
+
if (this.stopping) {
|
|
174
|
+
await listener.stop();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.listener = listener;
|
|
178
|
+
listener.on("status", this.onStatus);
|
|
179
|
+
this.deps.log(`[orchestrator] Started shared callmux listener at ${listener.mcpUrl}`);
|
|
180
|
+
this.reportCallmuxHealth(listener.health());
|
|
191
181
|
} catch (err) {
|
|
192
182
|
this.deps.log(`[orchestrator] Shared callmux listener failed to start: ${errMessage(err)}; scheduling restart`);
|
|
193
183
|
this.report("restarting", errMessage(err));
|
|
194
184
|
this.scheduleRestart();
|
|
195
|
-
return;
|
|
196
185
|
}
|
|
197
|
-
this.proc = proc;
|
|
198
|
-
this.deps.log(`[orchestrator] Started shared callmux listener pid=${proc.pid ?? "unknown"}`);
|
|
199
|
-
this.report("running", undefined, proc.pid);
|
|
200
|
-
proc.exited.then((code) => {
|
|
201
|
-
if (this.proc !== proc || this.stopping) return;
|
|
202
|
-
this.proc = null;
|
|
203
|
-
this.deps.log(`[orchestrator] Shared callmux listener exited (${code ?? "signal"}); scheduling restart`);
|
|
204
|
-
this.report("restarting", `exited ${code ?? "signal"}`);
|
|
205
|
-
this.scheduleRestart();
|
|
206
|
-
}).catch((err) => {
|
|
207
|
-
if (this.proc !== proc || this.stopping) return;
|
|
208
|
-
this.proc = null;
|
|
209
|
-
this.deps.log(`[orchestrator] Shared callmux listener exit watcher failed: ${err}`);
|
|
210
|
-
this.report("restarting", errMessage(err));
|
|
211
|
-
this.scheduleRestart();
|
|
212
|
-
});
|
|
213
186
|
}
|
|
214
187
|
|
|
215
188
|
private restart(reason: string): void {
|
|
216
|
-
if (this.
|
|
189
|
+
if (this.listener) {
|
|
190
|
+
const listener = this.listener;
|
|
217
191
|
this.deps.log(`[orchestrator] Stopping shared callmux listener: ${reason}`);
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
192
|
+
this.listener = null;
|
|
193
|
+
listener.off("status", this.onStatus);
|
|
194
|
+
void listener.stop().catch((err) => this.deps.log(`[orchestrator] Shared callmux listener stop failed: ${errMessage(err)}`));
|
|
220
195
|
}
|
|
221
196
|
this.report("restarting", reason);
|
|
222
197
|
this.scheduleRestart();
|
|
223
198
|
}
|
|
224
199
|
|
|
225
|
-
private report(state: SharedCallmuxHealthSnapshot["state"], reason?: string,
|
|
200
|
+
private report(state: SharedCallmuxHealthSnapshot["state"], reason?: string, downstream?: ListenerHealthSnapshot["downstream"]): void {
|
|
226
201
|
this.deps.report({
|
|
227
202
|
state,
|
|
228
203
|
url: this.opts.url,
|
|
229
|
-
command: this.opts.command,
|
|
230
204
|
...(reason ? { reason } : {}),
|
|
231
|
-
...(
|
|
205
|
+
...(downstream ? { downstream } : {}),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private reportCallmuxHealth(snapshot: ListenerHealthSnapshot): void {
|
|
210
|
+
const state = mapCallmuxState(snapshot);
|
|
211
|
+
this.deps.report({
|
|
212
|
+
state,
|
|
213
|
+
url: snapshot.mcpUrl,
|
|
214
|
+
...(snapshot.reason ? { reason: snapshot.reason } : state === "unhealthy" && snapshot.downstream.failed > 0 ? { reason: `${snapshot.downstream.failed} downstream server(s) failed` } : {}),
|
|
215
|
+
downstream: snapshot.downstream,
|
|
232
216
|
});
|
|
233
217
|
}
|
|
234
218
|
|
|
@@ -238,29 +222,14 @@ export class SharedCallmuxSupervisor {
|
|
|
238
222
|
this.backoffMs = Math.min(this.backoffMs * 2, this.timing.restartMaxMs ?? 30_000);
|
|
239
223
|
this.restartTimer = this.deps.setTimeout(() => {
|
|
240
224
|
this.restartTimer = null;
|
|
241
|
-
this.
|
|
225
|
+
void this.open();
|
|
242
226
|
}, delay);
|
|
243
227
|
}
|
|
244
228
|
}
|
|
245
229
|
|
|
246
230
|
function defaultDeps(): SharedCallmuxSupervisorDeps {
|
|
247
231
|
return {
|
|
248
|
-
|
|
249
|
-
spawn(command, args, opts) {
|
|
250
|
-
const proc = Bun.spawn([command, ...args], {
|
|
251
|
-
cwd: opts.cwd,
|
|
252
|
-
env: opts.env,
|
|
253
|
-
stdin: "ignore",
|
|
254
|
-
stdout: "inherit",
|
|
255
|
-
stderr: "inherit",
|
|
256
|
-
});
|
|
257
|
-
return {
|
|
258
|
-
pid: proc.pid,
|
|
259
|
-
exited: proc.exited,
|
|
260
|
-
kill: (signal?: NodeJS.Signals) => proc.kill(signal),
|
|
261
|
-
};
|
|
262
|
-
},
|
|
263
|
-
fetch,
|
|
232
|
+
createListener,
|
|
264
233
|
setInterval: (fn, ms) => setInterval(fn, ms),
|
|
265
234
|
clearInterval: (timer) => clearInterval(timer),
|
|
266
235
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -270,15 +239,11 @@ function defaultDeps(): SharedCallmuxSupervisorDeps {
|
|
|
270
239
|
};
|
|
271
240
|
}
|
|
272
241
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
function resolveCommand(command: string): string | null {
|
|
279
|
-
if (isAbsolute(command)) return existsSync(command) ? command : null;
|
|
280
|
-
if (command.includes("/")) return existsSync(command) ? command : null;
|
|
281
|
-
return Bun.which(command);
|
|
242
|
+
function mapCallmuxState(snapshot: ListenerHealthSnapshot): SharedCallmuxHealthSnapshot["state"] {
|
|
243
|
+
if (snapshot.state === "running") return "running";
|
|
244
|
+
if (snapshot.state === "starting" || snapshot.state === "reloading") return "starting";
|
|
245
|
+
if (snapshot.state === "stopped") return "stopped";
|
|
246
|
+
return "unhealthy";
|
|
282
247
|
}
|
|
283
248
|
|
|
284
249
|
function envOff(value: string | undefined): boolean {
|
|
@@ -292,12 +257,26 @@ function readJsonObject(path: string): Record<string, unknown> {
|
|
|
292
257
|
return isRecord(parsed) ? parsed : {};
|
|
293
258
|
}
|
|
294
259
|
|
|
295
|
-
function cloneServer(value: unknown):
|
|
260
|
+
function cloneServer(value: unknown): CallmuxConfig["servers"][string] | undefined {
|
|
296
261
|
if (!isRecord(value)) return undefined;
|
|
297
|
-
return JSON.parse(JSON.stringify(value)) as
|
|
262
|
+
return JSON.parse(JSON.stringify(value)) as CallmuxConfig["servers"][string];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function withRuntimeEnv(config: CallmuxConfig, env: Record<string, string>): CallmuxConfig {
|
|
266
|
+
const envEntries = Object.entries(env).filter(([, value]) => typeof value === "string");
|
|
267
|
+
if (envEntries.length === 0) return config;
|
|
268
|
+
const inheritedEnv = Object.fromEntries(envEntries);
|
|
269
|
+
const servers = Object.fromEntries(Object.entries(config.servers).map(([name, server]) => {
|
|
270
|
+
if (!("command" in server)) return [name, server];
|
|
271
|
+
return [name, {
|
|
272
|
+
...server,
|
|
273
|
+
env: { ...inheritedEnv, ...(isRecord(server.env) ? server.env as Record<string, string> : {}) },
|
|
274
|
+
}];
|
|
275
|
+
}));
|
|
276
|
+
return { ...config, servers };
|
|
298
277
|
}
|
|
299
278
|
|
|
300
|
-
function defaultTokenleanServer():
|
|
279
|
+
function defaultTokenleanServer(): CallmuxConfig["servers"][string] {
|
|
301
280
|
return {
|
|
302
281
|
command: "tl-mcp",
|
|
303
282
|
prefix: "",
|
|
@@ -306,7 +285,7 @@ function defaultTokenleanServer(): Record<string, unknown> {
|
|
|
306
285
|
};
|
|
307
286
|
}
|
|
308
287
|
|
|
309
|
-
function defaultGithubServer():
|
|
288
|
+
function defaultGithubServer(): CallmuxConfig["servers"][string] {
|
|
310
289
|
return {
|
|
311
290
|
command: "github-mcp-server",
|
|
312
291
|
args: ["stdio"],
|
|
@@ -331,6 +310,11 @@ function stringFromRecord(value: Record<string, unknown>, key: string): string |
|
|
|
331
310
|
return typeof value[key] === "string" ? value[key] : undefined;
|
|
332
311
|
}
|
|
333
312
|
|
|
313
|
+
function outputFormatFromRecord(value: Record<string, unknown>, key: string): CallmuxConfig["outputFormat"] | undefined {
|
|
314
|
+
const outputFormat = stringFromRecord(value, key);
|
|
315
|
+
return outputFormat === "auto" || outputFormat === "json" || outputFormat === "toon" ? outputFormat : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
334
318
|
function parseListenerUrl(value: string): { hostname: string; port: number } | undefined {
|
|
335
319
|
try {
|
|
336
320
|
const url = new URL(value);
|