agent-relay-orchestrator 0.102.2 → 0.103.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.102.2",
3
+ "version": "0.103.4",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "test": "bun test"
18
18
  },
19
19
  "dependencies": {
20
- "agent-relay-sdk": "0.2.83"
20
+ "agent-relay-providers": "0.102.2",
21
+ "agent-relay-sdk": "0.2.87"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/bun": "latest",
package/src/config.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { homedir, hostname as osHostname } from "node:os";
3
3
  import { join, dirname } from "node:path";
4
- import { DEFAULT_RELAY_URL } from "agent-relay-sdk";
4
+ import { getAllManifests, getManifest } from "agent-relay-providers";
5
+ import { DEFAULT_RELAY_URL, type SpawnProvider } from "agent-relay-sdk";
5
6
 
6
7
  export interface OrchestratorConfig {
7
8
  id: string;
8
9
  hostname: string;
9
10
  relayUrl: string;
10
11
  token?: string;
11
- providers: ("claude" | "codex")[];
12
+ providers: SpawnProvider[];
12
13
  baseDir: string;
13
14
  env: Record<string, string>;
14
15
  heartbeatIntervalMs: number;
@@ -17,6 +18,7 @@ export interface OrchestratorConfig {
17
18
  }
18
19
 
19
20
  const DEFAULT_CONFIG_PATH = join(homedir(), ".agent-relay", "orchestrator.json");
21
+ const DEFAULT_PROVIDER_IDS = getAllManifests().map((manifest) => manifest.id as SpawnProvider);
20
22
 
21
23
  function envNumberOrDefault(name: string, fallback: number): number {
22
24
  return Number(process.env[name]) || fallback;
@@ -42,8 +44,9 @@ export function bunBinFromEnv(): string | undefined {
42
44
  return process.env.AGENT_RELAY_BUN_BIN;
43
45
  }
44
46
 
45
- export function codexCommandFromEnv(): string | undefined {
46
- return process.env.AGENT_RELAY_CODEX_COMMAND;
47
+ export function providerCommandFromEnv(provider: string): string | undefined {
48
+ const prefix = getManifest(provider)?.home?.envPrefix ?? provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
49
+ return process.env[`AGENT_RELAY_${prefix}_COMMAND`];
47
50
  }
48
51
 
49
52
  export function providerHomeRootFromEnv(): string {
@@ -104,6 +107,13 @@ interface RawConfig {
104
107
  apiPort?: number;
105
108
  }
106
109
 
110
+ function normalizeProviders(values: string[] | undefined): SpawnProvider[] {
111
+ const requested = values?.map((value) => value.trim()).filter(Boolean) ?? [];
112
+ const allowed = new Set(DEFAULT_PROVIDER_IDS);
113
+ const normalized = requested.filter((value): value is SpawnProvider => allowed.has(value as SpawnProvider));
114
+ return normalized.length > 0 ? normalized : [...DEFAULT_PROVIDER_IDS];
115
+ }
116
+
107
117
  export function loadConfig(path?: string): OrchestratorConfig {
108
118
  const configPath = path || process.env.AGENT_RELAY_ORCHESTRATOR_CONFIG || DEFAULT_CONFIG_PATH;
109
119
 
@@ -116,7 +126,7 @@ export function loadConfig(path?: string): OrchestratorConfig {
116
126
  const hostname = raw.hostname || process.env.AGENT_RELAY_ORCHESTRATOR_HOSTNAME || osHostname();
117
127
  const relayUrl = raw.relayUrl || process.env.AGENT_RELAY_URL || DEFAULT_RELAY_URL;
118
128
  const token = raw.token || process.env.AGENT_RELAY_TOKEN || undefined;
119
- const providers = (raw.providers || process.env.AGENT_RELAY_ORCHESTRATOR_PROVIDERS?.split(",") || ["claude", "codex"]) as ("claude" | "codex")[];
129
+ const providers = normalizeProviders(raw.providers || process.env.AGENT_RELAY_ORCHESTRATOR_PROVIDERS?.split(","));
120
130
  const baseDir = raw.baseDir || process.env.AGENT_RELAY_ORCHESTRATOR_BASE_DIR || join(homedir(), "projects");
121
131
  const env = raw.env || {};
122
132
  const heartbeatIntervalMs = raw.heartbeatIntervalMs || 30_000;
@@ -133,7 +143,7 @@ export function initConfigFile(config: Partial<RawConfig>): string {
133
143
  id: osHostname().replace(/\./g, "-"),
134
144
  hostname: osHostname(),
135
145
  relayUrl: DEFAULT_RELAY_URL,
136
- providers: ["claude", "codex"],
146
+ providers: [...DEFAULT_PROVIDER_IDS],
137
147
  baseDir: join(homedir(), "projects"),
138
148
  apiPort: 4860,
139
149
  env: {},
package/src/control.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { errMessage, isRecord, normalizeAgentLifecycle, normalizeWorkspaceMode } from "agent-relay-sdk";
2
+ import { getAllManifests, getManifest } from "agent-relay-providers";
2
3
  import type { OrchestratorConfig } from "./config";
3
4
  import type { ManagedAgentReport, RelayClient, RelayCommand } from "./relay";
4
5
  import { handleSelfUpgrade } from "./self-upgrade";
@@ -264,17 +265,13 @@ function shutdownTimeoutMs(ctrl: Record<string, any>): number | undefined {
264
265
  return Number.isSafeInteger(ctrl.timeoutMs) && ctrl.timeoutMs > 0 ? Math.min(ctrl.timeoutMs, 60_000) : undefined;
265
266
  }
266
267
 
267
- // Both the control-spawn and restart-source paths build SpawnOptions from a
268
- // loose record in the same way. The two prior copies differed only in two
269
- // immaterial spots, unified here to the safer form:
270
- // - provider: `=== "codex" ? "codex" : "claude"` (SPAWN_PROVIDERS is exactly
271
- // claude|codex, so this matches control's `|| "claude"` for every valid
272
- // input and stays defensive against junk).
273
- // - cwd: `source.cwd || baseDir` — an empty-string cwd now falls back to
274
- // baseDir on the restart path too (was a latent bug; empty cwd is invalid).
275
268
  function spawnOptionsFromRecord(source: Record<string, any>, config: OrchestratorConfig): SpawnOptions {
269
+ const fallbackProvider = config.providers[0] ?? (getAllManifests()[0]?.id ?? "claude");
270
+ const provider = typeof source.provider === "string" && getManifest(source.provider)
271
+ ? source.provider
272
+ : fallbackProvider;
276
273
  return {
277
- provider: source.provider === "codex" ? "codex" : "claude",
274
+ provider,
278
275
  cwd: source.cwd || config.baseDir,
279
276
  rig: typeof source.rig === "string" ? source.rig : undefined,
280
277
  model: modelFromControl(source),
@@ -1,8 +1,9 @@
1
1
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { delimiter, join, resolve } from "node:path";
4
+ import { getManifest, type ProviderProbeFeatureCheck } from "agent-relay-providers";
4
5
  import { providerCatalogList, type ProviderCatalogEntry } from "agent-relay-sdk/provider-catalog";
5
- import { errMessage } from "agent-relay-sdk";
6
+ import { errMessage, type SpawnProvider } from "agent-relay-sdk";
6
7
  import type { OrchestratorConfig } from "./config";
7
8
  import { VERSION } from "./version";
8
9
 
@@ -15,7 +16,7 @@ interface ProviderProbeDetail {
15
16
  }
16
17
 
17
18
  interface ProviderProbeResult {
18
- name: "claude" | "codex";
19
+ name: SpawnProvider;
19
20
  available: boolean;
20
21
  checkedAt: number;
21
22
  reason?: string;
@@ -27,7 +28,7 @@ interface ProviderProbeResult {
27
28
  }
28
29
 
29
30
  interface ProviderProbeSnapshot {
30
- providers: ("claude" | "codex")[];
31
+ providers: SpawnProvider[];
31
32
  providerStatus: ProviderProbeResult[];
32
33
  providerCatalog: ProviderCatalogEntry[];
33
34
  checkedAt: number;
@@ -62,11 +63,14 @@ export class ProviderProbeCache {
62
63
  }
63
64
  }
64
65
 
65
- export async function probeProvider(provider: "claude" | "codex", timeoutMs = DEFAULT_TIMEOUT_MS): Promise<ProviderProbeResult> {
66
- const cli = await probeCommand(provider, ["--version"], timeoutMs, provider, (path) => isRelayProviderShim(path, provider));
66
+ export async function probeProvider(provider: SpawnProvider, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<ProviderProbeResult> {
67
+ const manifest = getManifest(provider);
68
+ const cliCommand = manifest?.probe?.command ?? provider;
69
+ const cliArgs = manifest?.probe?.args ?? ["--version"];
70
+ const cli = await probeCommand(cliCommand, cliArgs, timeoutMs, cliCommand, (path) => isRelayProviderShim(path, provider));
67
71
  const runner = await probeRunner(provider, timeoutMs);
68
72
  const available = cli.ok && runner.ok;
69
- const features = probeProviderFeatures(provider);
73
+ const features = probeProviderFeatures(manifest?.probe?.featureChecks ?? []);
70
74
  return {
71
75
  name: provider,
72
76
  available,
@@ -80,17 +84,19 @@ export async function probeProvider(provider: "claude" | "codex", timeoutMs = DE
80
84
  };
81
85
  }
82
86
 
83
- function probeProviderFeatures(provider: "claude" | "codex"): Record<string, boolean> {
87
+ function probeProviderFeatures(checks: ProviderProbeFeatureCheck[]): Record<string, boolean> {
84
88
  const features: Record<string, boolean> = {};
85
- if (provider === "claude") {
86
- const hasRigBinary = !!resolveExecutable("claude-rig");
87
- const hasRigDir = existsSync(join(homedir(), ".claude-rig"));
88
- if (hasRigBinary || hasRigDir) features.rig = true;
89
+ for (const check of checks) {
90
+ const hasCommand = check.command ? !!resolveExecutable(check.command) : false;
91
+ const hasHomeDir = check.homeDir ? existsSync(join(homedir(), check.homeDir)) : false;
92
+ if (hasCommand || hasHomeDir) {
93
+ features[check.name] = true;
94
+ }
89
95
  }
90
96
  return features;
91
97
  }
92
98
 
93
- async function probeRunner(provider: "claude" | "codex", timeoutMs: number): Promise<ProviderProbeDetail> {
99
+ async function probeRunner(provider: SpawnProvider, timeoutMs: number): Promise<ProviderProbeDetail> {
94
100
  const repoLauncher = resolve(import.meta.dir, "../../runner/src/index.ts");
95
101
  if (existsSync(repoLauncher)) {
96
102
  const bun = resolveExecutable("bun");
@@ -175,7 +181,7 @@ function isExecutable(path: string): boolean {
175
181
  }
176
182
  }
177
183
 
178
- function isRelayProviderShim(path: string, provider: "claude" | "codex"): boolean {
184
+ function isRelayProviderShim(path: string, provider: SpawnProvider): boolean {
179
185
  try {
180
186
  const content = readFileSync(path, "utf8").slice(0, 1024);
181
187
  return content.includes(`${provider}-relay ${provider}`);
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { existsSync, readdirSync } from "node:fs";
3
3
  import { createServer } from "node:net";
4
4
  import { join } from "node:path";
5
+ import { getManifest, type ProviderQuotaPollDescriptor } from "agent-relay-providers";
5
6
  import {
6
7
  DEFAULT_PROVIDER_QUOTA_CONFIG,
7
8
  QUOTA_FAILURE_LOG_INTERVAL_MS,
@@ -17,7 +18,7 @@ import {
17
18
  } from "agent-relay-sdk/provider-quota";
18
19
  import type { ProviderQuotaConfig, ProviderQuotaConfigMap, ProviderQuotaLeaseAcquireInput, ProviderQuotaUpdateInput } from "agent-relay-sdk";
19
20
  import { errMessage } from "agent-relay-sdk";
20
- import { codexCommandFromEnv, providerHomeRootFromEnv, type OrchestratorConfig } from "./config";
21
+ import { providerCommandFromEnv, providerHomeRootFromEnv, type OrchestratorConfig } from "./config";
21
22
 
22
23
  const QUOTA_LEASE_TTL_MS = 90_000;
23
24
  const QUOTA_LEASE_RENEW_MS = 30_000;
@@ -173,10 +174,11 @@ export class OrchestratorQuotaPoller {
173
174
  // A disabled provider (#605) is collected from at all: no discovery → no
174
175
  // polling/API calls, leases released by releaseRemovedCandidates, and no
175
176
  // skip-marker row (disabled is intentional, not a credential failure).
176
- if (this.config.providers.includes("codex") && this.configFor("codex").enabled) {
177
- const found = await this.discoverCodexCandidates();
177
+ for (const provider of this.config.providers) {
178
+ if (!this.configFor(provider).enabled) continue;
179
+ const found = await this.discoverProviderCandidates(provider);
178
180
  candidates.push(...found.candidates);
179
- if (found.skipReason) skips.push({ provider: "codex", reason: found.skipReason });
181
+ if (found.skipReason) skips.push({ provider, reason: found.skipReason });
180
182
  }
181
183
  const deduped = new Map<string, QuotaCandidate>();
182
184
  for (const candidate of candidates) {
@@ -185,20 +187,32 @@ export class OrchestratorQuotaPoller {
185
187
  return { candidates: [...deduped.values()], skips };
186
188
  }
187
189
 
188
- private async discoverCodexCandidates(): Promise<{ candidates: QuotaCandidate[]; skipReason?: string }> {
190
+ private async discoverProviderCandidates(provider: string): Promise<{ candidates: QuotaCandidate[]; skipReason?: string }> {
191
+ const manifest = getManifest(provider);
192
+ const quotaPoll = manifest?.quotaPoll;
193
+ if (!quotaPoll || quotaPoll.strategy === "none") return { candidates: [] };
194
+ if (quotaPoll.strategy === "codex-app-server") return this.discoverCodexCandidates(provider, quotaPoll);
195
+ return { candidates: [] };
196
+ }
197
+
198
+ private async discoverCodexCandidates(provider: string, _quotaPoll: ProviderQuotaPollDescriptor): Promise<{ candidates: QuotaCandidate[]; skipReason?: string }> {
199
+ const manifest = getManifest(provider);
200
+ const providerLabel = manifest?.label ?? provider;
201
+ const markerFile = manifest?.home?.quotaCredentialFile ?? "auth.json";
189
202
  const homes = [
190
- this.config.env.CODEX_HOME || process.env.CODEX_HOME || join(homedir(), ".codex"),
191
- ...providerHomeConfigDirs("codex", "auth.json"),
203
+ this.providerHomeDir(provider),
204
+ ...providerHomeConfigDirs(provider, markerFile),
192
205
  ];
193
- const appServerUrl = this.config.env.CODEX_APP_SERVER_URL || process.env.CODEX_APP_SERVER_URL;
206
+ const envPrefix = manifest?.home?.envPrefix ?? provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
207
+ const appServerUrl = this.config.env[`${envPrefix}_APP_SERVER_URL`] || process.env[`${envPrefix}_APP_SERVER_URL`];
194
208
  const candidates: QuotaCandidate[] = [];
195
209
  for (const codexHome of homes) {
196
210
  const identity = await resolveStableCodexQuotaIdentityFromHome({ codexHome });
197
211
  if (identity) candidates.push({ ...identity, codexHome, ...(appServerUrl ? { appServerUrl } : {}) });
198
212
  }
199
213
  if (candidates.length === 0) {
200
- const reason = "no Codex account id found in auth.json";
201
- this.logOnce("codex:no-stable-auth", `quota refresh skipped for codex: ${reason}`);
214
+ const reason = `no ${providerLabel} account id found in ${markerFile}`;
215
+ this.logOnce(`${provider}:no-stable-auth`, `quota refresh skipped for ${provider}: ${reason}`);
202
216
  return { candidates, skipReason: reason };
203
217
  }
204
218
  return { candidates };
@@ -319,14 +333,15 @@ export class OrchestratorQuotaPoller {
319
333
  }
320
334
 
321
335
  private async collect(candidate: QuotaCandidate): Promise<ProviderQuotaSample> {
322
- if (candidate.provider === "codex") {
336
+ const strategy = getManifest(candidate.provider)?.quotaPoll?.strategy;
337
+ if (strategy === "codex-app-server") {
323
338
  return collectCodexQuotaSample({
324
339
  agentId: this.sourceAgentId(),
325
340
  rateLimitsRead: () => this.options.codexRateLimitsRead
326
341
  ? this.options.codexRateLimitsRead(candidate.appServerUrl ?? "")
327
342
  : candidate.appServerUrl
328
343
  ? codexRateLimitsRead(candidate.appServerUrl)
329
- : codexRateLimitsReadFromHome(candidate.codexHome),
344
+ : codexRateLimitsReadFromHome(candidate.provider, candidate.codexHome),
330
345
  });
331
346
  }
332
347
  return {};
@@ -370,6 +385,12 @@ export class OrchestratorQuotaPoller {
370
385
  this.logStates.set(key, { key, at: now });
371
386
  this.log(message);
372
387
  }
388
+
389
+ private providerHomeDir(provider: string): string {
390
+ const manifest = getManifest(provider);
391
+ const envPrefix = manifest?.home?.envPrefix ?? provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
392
+ return this.config.env[`${envPrefix}_HOME`] || process.env[`${envPrefix}_HOME`] || join(homedir(), manifest?.home?.configDir ?? `.${provider}`);
393
+ }
373
394
  }
374
395
 
375
396
  async function codexRateLimitsRead(appServerUrl: string, attempts = 1): Promise<unknown> {
@@ -387,10 +408,10 @@ async function codexRateLimitsRead(appServerUrl: string, attempts = 1): Promise<
387
408
  }
388
409
  }
389
410
 
390
- async function codexRateLimitsReadFromHome(codexHome: string | undefined): Promise<unknown> {
391
- if (!codexHome) throw new QuotaCollectionError("creds_not_ready", "Codex home is not configured");
411
+ async function codexRateLimitsReadFromHome(provider: string, codexHome: string | undefined): Promise<unknown> {
412
+ if (!codexHome) throw new QuotaCollectionError("creds_not_ready", `${provider} home is not configured`);
392
413
  const appServerUrl = await freeLoopbackWsUrl();
393
- const command = codexCommandFromEnv() || "codex";
414
+ const command = providerCommandFromEnv(provider) || getManifest(provider)?.probe?.command || provider;
394
415
  const proc = Bun.spawn([command, "app-server", "--listen", appServerUrl], {
395
416
  env: {
396
417
  ...process.env,
@@ -440,7 +461,7 @@ function providerHomeRoot(): string {
440
461
  return providerHomeRootFromEnv();
441
462
  }
442
463
 
443
- function providerHomeConfigDirs(provider: "claude" | "codex", markerFile: string): string[] {
464
+ function providerHomeConfigDirs(provider: string, markerFile: string): string[] {
444
465
  const root = join(providerHomeRoot(), provider);
445
466
  const dirs: string[] = [];
446
467
  for (const profile of safeReadDir(root)) {
package/src/relay.ts CHANGED
@@ -2,7 +2,7 @@ import type { OrchestratorConfig } from "./config";
2
2
  import type { ProviderProbeCache } from "./provider-probe";
3
3
  import { detectSelfSupervision } from "./self-supervision";
4
4
  import { GIT_SHA, ORCHESTRATOR_PROTOCOL_VERSION, VERSION, runtimeMetadata } from "./version";
5
- import type { AgentLifecycle, ProviderQuotaConfigMap, ProviderQuotaLeaseAcquireInput, ProviderQuotaLeaseAcquireResult, ProviderQuotaUpdateInput, WorkspaceMetadata, WorkspaceMode, ManagedSessionExitDiagnostics as SdkManagedSessionExitDiagnostics } from "agent-relay-sdk";
5
+ import type { AgentLifecycle, ProviderQuotaConfigMap, ProviderQuotaLeaseAcquireInput, ProviderQuotaLeaseAcquireResult, ProviderQuotaUpdateInput, WorkspaceMetadata, WorkspaceMode, ManagedSessionExitDiagnostics as SdkManagedSessionExitDiagnostics, SpawnProvider } from "agent-relay-sdk";
6
6
  import { ReconnectionManager, RelayHttpClient } from "agent-relay-sdk";
7
7
 
8
8
  export interface RelayClient {
@@ -29,7 +29,7 @@ interface RunnerTokenRemint {
29
29
 
30
30
  export interface ManagedAgentReport {
31
31
  agentId: string;
32
- provider: "claude" | "codex";
32
+ provider: SpawnProvider;
33
33
  model?: string;
34
34
  effort?: string;
35
35
  profile?: string;
@@ -1,12 +1,13 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
+ import { getAllManifests } from "agent-relay-providers";
4
5
  import { runInstallWithRetry } from "agent-relay-sdk";
5
6
  import type { OrchestratorConfig } from "./config";
6
7
  import type { RelayClient, RelayCommand } from "./relay";
7
8
  import { detectSelfSupervision, type SelfSupervision } from "./self-supervision";
8
9
 
9
- const VALID_PROVIDERS = new Set(["auto", "all", "codex", "claude", "orchestrator"]);
10
+ const VALID_PROVIDERS = new Set(["auto", "all", "orchestrator", ...getAllManifests().map((manifest) => manifest.id)]);
10
11
  const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
11
12
 
12
13
  interface SelfUpgradeOptions {
@@ -213,13 +214,10 @@ function packagesForProviders(targetVersion: string, providers: string[]): strin
213
214
  if (includeAll || includeAuto || selected.has("orchestrator")) {
214
215
  packages.add("agent-relay-orchestrator");
215
216
  }
216
- if (includeAll || selected.has("codex")) {
217
- packages.add("agent-relay-runner");
218
- packages.add("agent-relay-codex");
219
- }
220
- if (includeAll || selected.has("claude")) {
221
- packages.add("agent-relay-runner");
222
- packages.add("agent-relay-plugin");
217
+ for (const manifest of getAllManifests()) {
218
+ if (!manifest.upgrade?.packages?.length) continue;
219
+ if (!includeAll && !selected.has(manifest.id)) continue;
220
+ for (const providerPackage of manifest.upgrade.packages) packages.add(providerPackage);
223
221
  }
224
222
 
225
223
  if (packages.size === 0) packages.add("agent-relay-orchestrator");
@@ -1,9 +1,9 @@
1
1
  import type { OrchestratorConfig } from "../config";
2
- import type { AgentLifecycle, WorkspaceMetadata, WorkspaceMode } from "agent-relay-sdk";
2
+ import type { AgentLifecycle, SpawnProvider, WorkspaceMetadata, WorkspaceMode } from "agent-relay-sdk";
3
3
  import type { ResumeWorkspaceTarget } from "../workspace-probe/types";
4
4
 
5
5
  export interface SpawnOptions {
6
- provider: "claude" | "codex";
6
+ provider: SpawnProvider;
7
7
  cwd: string;
8
8
  rig?: string;
9
9
  model?: string;