agent-relay-server 0.11.3 → 0.11.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/docs/openapi.json +5 -2
- package/package.json +2 -2
- package/public/index.html +36 -1
- package/src/db.ts +10 -2
- package/src/dev.ts +2 -2
- package/src/lifecycle-manager.ts +31 -54
- package/src/managed-policy.ts +80 -0
- package/src/mcp.ts +16 -8
- package/src/routes.ts +88 -61
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.11.3",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -2725,7 +2725,7 @@
|
|
|
2725
2725
|
"tags": [
|
|
2726
2726
|
"Orchestrators"
|
|
2727
2727
|
],
|
|
2728
|
-
"description": "Dispatches a host action to the orchestrator over the command bus. `restart`/`shutdown` target a managed agent (omit `agentId` for all). `upgrade` triggers a remote self-upgrade: the relay stamps a desired version and the orchestrator installs
|
|
2728
|
+
"description": "Dispatches a host action to the orchestrator over the command bus. `restart`/`shutdown` target a managed agent (omit `agentId` for all). `upgrade` triggers a remote self-upgrade: the relay stamps a desired version and the orchestrator installs the selected runtime packages, then restarts itself decoupled so the bus keepalive survives its own teardown. Runtime-prefix installs use npm directly; non-prefix installs require an available `agent-relay upgrade --no-restart` CLI. The upgrade command is intentionally left `running` — the relay settles it (succeeded/failed) by reconciling the version the orchestrator reports on its post-restart re-register, within a deadline, or immediately when the command fails. Safety rails: semver-only `targetVersion` (defaults to the relay's own version), explicit self-upgrade capability, systemd/launchd supervision, and no downgrade unless `force` is set.",
|
|
2729
2729
|
"parameters": [
|
|
2730
2730
|
{
|
|
2731
2731
|
"name": "id",
|
|
@@ -6603,6 +6603,9 @@
|
|
|
6603
6603
|
},
|
|
6604
6604
|
"defaultTags": {
|
|
6605
6605
|
"type": "string"
|
|
6606
|
+
},
|
|
6607
|
+
"chatCaptureMode": {
|
|
6608
|
+
"type": "string"
|
|
6606
6609
|
}
|
|
6607
6610
|
}
|
|
6608
6611
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"CONTRIBUTING.md"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"agent-relay-sdk": "0.2.
|
|
35
|
+
"agent-relay-sdk": "0.2.3"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"postinstall": "node scripts/install-bin-shim.cjs",
|
package/public/index.html
CHANGED
|
@@ -123527,6 +123527,32 @@ function OrphanPanel() {
|
|
|
123527
123527
|
})]
|
|
123528
123528
|
});
|
|
123529
123529
|
}
|
|
123530
|
+
function prLabel(url) {
|
|
123531
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
123532
|
+
return match ? `PR #${match[1]}` : "View PR";
|
|
123533
|
+
}
|
|
123534
|
+
function MergeOutcome({ workspace }) {
|
|
123535
|
+
const result = workspace.metadata?.mergeResult;
|
|
123536
|
+
const prUrl = result?.prUrl;
|
|
123537
|
+
const error = result?.error || workspace.metadata?.mergeError;
|
|
123538
|
+
if (!prUrl && !error) return null;
|
|
123539
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [prUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", {
|
|
123540
|
+
href: prUrl,
|
|
123541
|
+
target: "_blank",
|
|
123542
|
+
rel: "noreferrer",
|
|
123543
|
+
title: prUrl,
|
|
123544
|
+
className: "inline-flex items-center gap-0.5 rounded border border-violet-500/30 px-1.5 py-0 text-[10px] text-violet-400 hover:text-violet-300",
|
|
123545
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitMerge, { className: "h-2.5 w-2.5" }), prLabel(prUrl)]
|
|
123546
|
+
}), error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
|
|
123547
|
+
variant: "outline",
|
|
123548
|
+
className: "max-w-[16rem] border-amber-500/30 text-amber-400 text-[10px]",
|
|
123549
|
+
title: error,
|
|
123550
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlert, { className: "mr-0.5 h-2.5 w-2.5 shrink-0" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
123551
|
+
className: "truncate",
|
|
123552
|
+
children: ["merge: ", error]
|
|
123553
|
+
})]
|
|
123554
|
+
})] });
|
|
123555
|
+
}
|
|
123530
123556
|
function WorkspaceRow({ workspace }) {
|
|
123531
123557
|
const now = useNow();
|
|
123532
123558
|
const [expanded, setExpanded] = (0, import_react.useState)(false);
|
|
@@ -123565,7 +123591,8 @@ function WorkspaceRow({ workspace }) {
|
|
|
123565
123591
|
variant: "outline",
|
|
123566
123592
|
className: `text-[10px] ${statusTone(workspace.status)}`,
|
|
123567
123593
|
children: STATUS_LABEL[workspace.status] || workspace.status
|
|
123568
|
-
})
|
|
123594
|
+
}),
|
|
123595
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MergeOutcome, { workspace })
|
|
123569
123596
|
]
|
|
123570
123597
|
})]
|
|
123571
123598
|
}),
|
|
@@ -152711,6 +152738,10 @@ if ("serviceWorker" in navigator) {
|
|
|
152711
152738
|
max-width: 12rem;
|
|
152712
152739
|
}
|
|
152713
152740
|
|
|
152741
|
+
.max-w-\[16rem\] {
|
|
152742
|
+
max-width: 16rem;
|
|
152743
|
+
}
|
|
152744
|
+
|
|
152714
152745
|
.max-w-\[18rem\] {
|
|
152715
152746
|
max-width: 18rem;
|
|
152716
152747
|
}
|
|
@@ -155661,6 +155692,10 @@ if ("serviceWorker" in navigator) {
|
|
|
155661
155692
|
color: var(--color-red-400);
|
|
155662
155693
|
}
|
|
155663
155694
|
|
|
155695
|
+
.hover\:text-violet-300:hover {
|
|
155696
|
+
color: var(--color-violet-300);
|
|
155697
|
+
}
|
|
155698
|
+
|
|
155664
155699
|
.hover\:text-zinc-100:hover {
|
|
155665
155700
|
color: var(--color-zinc-100);
|
|
155666
155701
|
}
|
package/src/db.ts
CHANGED
|
@@ -4951,6 +4951,14 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
|
|
|
4951
4951
|
const workspace = agent.workspace;
|
|
4952
4952
|
if (!workspace) return null;
|
|
4953
4953
|
if (workspace.mode === "isolated" && workspace.id && workspace.repoRoot && workspace.worktreePath) {
|
|
4954
|
+
// A live agent re-registers its workspace on every heartbeat. That must not
|
|
4955
|
+
// clobber progress the workspace has made past the live editing state
|
|
4956
|
+
// (a merge that opened a PR, ready/review/conflict, terminal states) nor
|
|
4957
|
+
// wipe accumulated metadata such as the merge result / PR link. Only the
|
|
4958
|
+
// "active" live state is refreshable from registration; everything else is
|
|
4959
|
+
// preserved and metadata is merged, not replaced.
|
|
4960
|
+
const existing = getWorkspace(workspace.id);
|
|
4961
|
+
const preserveStatus = existing != null && existing.status !== "active";
|
|
4954
4962
|
return upsertWorkspace({
|
|
4955
4963
|
id: workspace.id,
|
|
4956
4964
|
repoRoot: workspace.repoRoot,
|
|
@@ -4961,11 +4969,11 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
|
|
|
4961
4969
|
baseSha: workspace.baseSha,
|
|
4962
4970
|
mode: workspace.mode,
|
|
4963
4971
|
requestedMode: workspace.requestedMode,
|
|
4964
|
-
status: workspace.status ?? "active",
|
|
4972
|
+
status: preserveStatus ? existing!.status : (workspace.status ?? "active"),
|
|
4965
4973
|
ownerAgentId: agent.agentId || undefined,
|
|
4966
4974
|
ownerPolicyName: agent.policyName,
|
|
4967
4975
|
ownerAutomationRunId: agent.automationRunId,
|
|
4968
|
-
metadata: { provider: agent.provider, label: agent.label, sessionName: agent.sessionName, tmuxSession: agent.tmuxSession },
|
|
4976
|
+
metadata: { ...(existing?.metadata ?? {}), provider: agent.provider, label: agent.label, sessionName: agent.sessionName, tmuxSession: agent.tmuxSession },
|
|
4969
4977
|
});
|
|
4970
4978
|
}
|
|
4971
4979
|
// Shared-mode occupancy: one row per agent sharing a git repo, with no
|
package/src/dev.ts
CHANGED
|
@@ -154,7 +154,7 @@ export async function executeDevInstallPlan(plan: DevInstallPlan, options: { dry
|
|
|
154
154
|
const packed = await executeDevPackPlan(plan.packPlan);
|
|
155
155
|
await mkdir(plan.prefix, { recursive: true });
|
|
156
156
|
ensureDevPrefixPackageJson(plan.prefix);
|
|
157
|
-
const command = ["install", "--prefix", plan.prefix, "--no-audit", "--no-fund", ...packed.tarballs];
|
|
157
|
+
const command = ["install", "--prefix", plan.prefix, "--no-audit", "--no-fund", "--force", ...packed.tarballs];
|
|
158
158
|
const result = await execFileAsync("npm", command);
|
|
159
159
|
const output = [
|
|
160
160
|
packed.output,
|
|
@@ -353,7 +353,7 @@ export function formatDevInstallPlan(plan: DevInstallPlan): string {
|
|
|
353
353
|
formatDevPackPlan(plan.packPlan),
|
|
354
354
|
"",
|
|
355
355
|
"Install command:",
|
|
356
|
-
` npm install --prefix ${plan.prefix} --no-audit --no-fund <packed tarballs...>`,
|
|
356
|
+
` npm install --prefix ${plan.prefix} --no-audit --no-fund --force <packed tarballs...>`,
|
|
357
357
|
].join("\n");
|
|
358
358
|
}
|
|
359
359
|
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
-
import { resolveProviderSelection } from "agent-relay-sdk/provider-catalog";
|
|
3
2
|
import { createCommand } from "./commands-db";
|
|
4
3
|
import { createActivityEvent, deleteWorkspace, getAgent, getDb, getOrchestrator, listOrchestrators, listWorkspaces, resolveQueuedPolicyMessages } from "./db";
|
|
5
4
|
import {
|
|
@@ -9,8 +8,8 @@ import {
|
|
|
9
8
|
upsertManagedAgentState,
|
|
10
9
|
} from "./config-store";
|
|
11
10
|
import { emitRelayEvent } from "./events";
|
|
12
|
-
import {
|
|
13
|
-
import type { Command, ManagedAgent, ManagedAgentState, ManagedSessionExitDiagnostics, SpawnPolicy
|
|
11
|
+
import { buildManagedSpawnParams } from "./managed-policy";
|
|
12
|
+
import type { Command, ManagedAgent, ManagedAgentState, ManagedSessionExitDiagnostics, SpawnPolicy } from "./types";
|
|
14
13
|
|
|
15
14
|
const DEFAULT_TICK_MS = 10_000;
|
|
16
15
|
// A spawn that never reaches "running" (runner crashed, never registered, host
|
|
@@ -227,34 +226,8 @@ export class LifecycleManager {
|
|
|
227
226
|
target: orch.agentId,
|
|
228
227
|
correlationId: spawnRequestId,
|
|
229
228
|
params: {
|
|
230
|
-
|
|
231
|
-
provider: policy.provider,
|
|
232
|
-
cwd: policy.cwd,
|
|
233
|
-
workspaceMode: effectivePolicyWorkspaceMode(policy),
|
|
234
|
-
...(policy.rig ? { rig: policy.rig } : {}),
|
|
235
|
-
...resolvedModelParams(policy),
|
|
236
|
-
label: policy.label,
|
|
237
|
-
tags: policy.tags,
|
|
238
|
-
capabilities: policy.capabilities,
|
|
239
|
-
approvalMode: policy.permissionMode,
|
|
240
|
-
permissionMode: policy.permissionMode,
|
|
241
|
-
providerArgs: policy.providerArgs,
|
|
242
|
-
prompt: policy.prompt,
|
|
243
|
-
headless: true,
|
|
244
|
-
policyName: policy.name,
|
|
245
|
-
spawnRequestId,
|
|
246
|
-
env: runnerRuntimeTokenEnv({
|
|
247
|
-
orchestratorId: policy.orchestratorId,
|
|
248
|
-
cwd: policy.cwd,
|
|
249
|
-
provider: policy.provider,
|
|
250
|
-
label: policy.label,
|
|
251
|
-
policyName: policy.name,
|
|
252
|
-
spawnRequestId,
|
|
253
|
-
createdBy: "lifecycle-manager",
|
|
254
|
-
}),
|
|
229
|
+
...buildManagedSpawnParams(policy, spawnRequestId, { createdBy: "lifecycle-manager", requestedAt: this.now() }),
|
|
255
230
|
reason,
|
|
256
|
-
requestedBy: "lifecycle-manager",
|
|
257
|
-
requestedAt: this.now(),
|
|
258
231
|
},
|
|
259
232
|
});
|
|
260
233
|
this.emitCommand(command);
|
|
@@ -302,7 +275,11 @@ export class LifecycleManager {
|
|
|
302
275
|
}
|
|
303
276
|
|
|
304
277
|
restartAgent(policy: SpawnPolicy, reason = "reconcile-restart"): Command | null {
|
|
278
|
+
const orch = getOrchestrator(policy.orchestratorId);
|
|
279
|
+
if (!orch) return null;
|
|
305
280
|
const state = getManagedAgentState(policy.name);
|
|
281
|
+
const restartRequestId = `sp_${crypto.randomUUID()}`;
|
|
282
|
+
const restartSpawn = buildManagedSpawnParams(policy, restartRequestId, { createdBy: "lifecycle-manager", requestedAt: this.now() });
|
|
306
283
|
const restarted = upsertManagedAgentState({
|
|
307
284
|
policyName: policy.name,
|
|
308
285
|
status: "stopping",
|
|
@@ -310,14 +287,35 @@ export class LifecycleManager {
|
|
|
310
287
|
orchestratorId: policy.orchestratorId,
|
|
311
288
|
provider: policy.provider,
|
|
312
289
|
tmuxSession: state?.tmuxSession,
|
|
313
|
-
spawnRequestId:
|
|
314
|
-
lastSpawnAt:
|
|
290
|
+
spawnRequestId: restartRequestId,
|
|
291
|
+
lastSpawnAt: this.now(),
|
|
315
292
|
lastStopAt: this.now(),
|
|
316
293
|
restartCount: (state?.restartCount ?? 0) + 1,
|
|
317
294
|
consecutiveFailures: state?.consecutiveFailures ?? 0,
|
|
318
295
|
});
|
|
319
296
|
this.emitState(restarted, `restart: ${reason}`);
|
|
320
|
-
|
|
297
|
+
const command = createCommand({
|
|
298
|
+
type: "agent.restart",
|
|
299
|
+
source: "system",
|
|
300
|
+
target: orch.agentId,
|
|
301
|
+
correlationId: state?.spawnRequestId,
|
|
302
|
+
params: {
|
|
303
|
+
action: "restart",
|
|
304
|
+
policyName: policy.name,
|
|
305
|
+
spawnRequestId: state?.spawnRequestId,
|
|
306
|
+
agentId: state?.agentId,
|
|
307
|
+
tmuxSession: state?.tmuxSession,
|
|
308
|
+
graceful: true,
|
|
309
|
+
timeoutMs: 10_000,
|
|
310
|
+
reason,
|
|
311
|
+
orchestratorId: orch.id,
|
|
312
|
+
restartSpawn,
|
|
313
|
+
requestedBy: "lifecycle-manager",
|
|
314
|
+
requestedAt: this.now(),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
this.emitCommand(command);
|
|
318
|
+
return command;
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
private reconcilePolicy(policy: SpawnPolicy): void {
|
|
@@ -641,24 +639,3 @@ function alwaysReloadTags(tags: string[]): string[] {
|
|
|
641
639
|
.filter(Boolean);
|
|
642
640
|
}
|
|
643
641
|
|
|
644
|
-
function effectivePolicyWorkspaceMode(policy: SpawnPolicy): WorkspaceMode {
|
|
645
|
-
if (policy.workspaceMode && policy.workspaceMode !== "inherit") return policy.workspaceMode;
|
|
646
|
-
return policy.binding?.type === "channel" ? "shared" : "inherit";
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function resolvedModelParams(policy: SpawnPolicy): Record<string, string> {
|
|
650
|
-
if (!policy.model && !policy.effort) return {};
|
|
651
|
-
try {
|
|
652
|
-
const selection = resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort });
|
|
653
|
-
return {
|
|
654
|
-
...(selection.modelAlias ? { model: selection.modelAlias } : {}),
|
|
655
|
-
...(selection.providerModel ? { providerModel: selection.providerModel } : {}),
|
|
656
|
-
...(selection.effort ? { effort: selection.effort } : {}),
|
|
657
|
-
};
|
|
658
|
-
} catch {
|
|
659
|
-
return {
|
|
660
|
-
...(policy.model ? { model: policy.model } : {}),
|
|
661
|
-
...(policy.effort ? { effort: policy.effort } : {}),
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveProviderSelection } from "agent-relay-sdk/provider-catalog";
|
|
2
|
+
import { getAgentProfile } from "./config-store";
|
|
3
|
+
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
4
|
+
import type { SpawnPolicy, WorkspaceMode } from "./types";
|
|
5
|
+
|
|
6
|
+
export function managedPolicyProviderArgs(policy: SpawnPolicy): string[] {
|
|
7
|
+
if (policy.provider === "claude" && policy.mode === "always-on" && policy.prompt?.trim()) {
|
|
8
|
+
return [...policy.providerArgs, "--append-system-prompt", policy.prompt.trim()];
|
|
9
|
+
}
|
|
10
|
+
return policy.providerArgs;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function managedPolicyLaunchPrompt(policy: SpawnPolicy): string | undefined {
|
|
14
|
+
if (policy.provider === "claude" && policy.mode === "always-on") return undefined;
|
|
15
|
+
return policy.prompt;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function effectiveManagedPolicyWorkspaceMode(policy: SpawnPolicy): WorkspaceMode {
|
|
19
|
+
if (policy.workspaceMode && policy.workspaceMode !== "inherit") return policy.workspaceMode;
|
|
20
|
+
return policy.binding?.type === "channel" ? "shared" : "inherit";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolvedModelParams(policy: SpawnPolicy): Record<string, string> {
|
|
24
|
+
if (!policy.model && !policy.effort) return {};
|
|
25
|
+
try {
|
|
26
|
+
const selection = resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort });
|
|
27
|
+
return {
|
|
28
|
+
...(selection.modelAlias ? { model: selection.modelAlias } : {}),
|
|
29
|
+
...(selection.providerModel ? { providerModel: selection.providerModel } : {}),
|
|
30
|
+
...(selection.effort ? { effort: selection.effort } : {}),
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return {
|
|
34
|
+
...(policy.model ? { model: policy.model } : {}),
|
|
35
|
+
...(policy.effort ? { effort: policy.effort } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ManagedSpawnContext {
|
|
41
|
+
createdBy: string;
|
|
42
|
+
requestedAt?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildManagedSpawnParams(policy: SpawnPolicy, requestId: string, ctx: ManagedSpawnContext): Record<string, unknown> {
|
|
46
|
+
const providerArgs = managedPolicyProviderArgs(policy);
|
|
47
|
+
const prompt = managedPolicyLaunchPrompt(policy);
|
|
48
|
+
const agentProfile = policy.profile ? getAgentProfile(policy.profile)?.value : undefined;
|
|
49
|
+
return {
|
|
50
|
+
action: "spawn",
|
|
51
|
+
provider: policy.provider,
|
|
52
|
+
cwd: policy.cwd,
|
|
53
|
+
workspaceMode: effectiveManagedPolicyWorkspaceMode(policy),
|
|
54
|
+
...(policy.rig ? { rig: policy.rig } : {}),
|
|
55
|
+
...resolvedModelParams(policy),
|
|
56
|
+
...(policy.profile ? { profile: policy.profile } : {}),
|
|
57
|
+
...(agentProfile ? { agentProfile } : {}),
|
|
58
|
+
label: policy.label,
|
|
59
|
+
tags: policy.tags,
|
|
60
|
+
capabilities: policy.capabilities,
|
|
61
|
+
approvalMode: policy.permissionMode,
|
|
62
|
+
permissionMode: policy.permissionMode,
|
|
63
|
+
providerArgs,
|
|
64
|
+
...(prompt ? { prompt } : {}),
|
|
65
|
+
headless: true,
|
|
66
|
+
policyName: policy.name,
|
|
67
|
+
spawnRequestId: requestId,
|
|
68
|
+
env: runnerRuntimeTokenEnv({
|
|
69
|
+
orchestratorId: policy.orchestratorId,
|
|
70
|
+
cwd: policy.cwd,
|
|
71
|
+
provider: policy.provider,
|
|
72
|
+
label: policy.label,
|
|
73
|
+
policyName: policy.name,
|
|
74
|
+
spawnRequestId: requestId,
|
|
75
|
+
createdBy: ctx.createdBy,
|
|
76
|
+
}),
|
|
77
|
+
requestedBy: ctx.createdBy,
|
|
78
|
+
requestedAt: ctx.requestedAt ?? Date.now(),
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/mcp.ts
CHANGED
|
@@ -520,8 +520,8 @@ function relayAgentStatus(args: Record<string, unknown>): Record<string, unknown
|
|
|
520
520
|
|
|
521
521
|
function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
522
522
|
const provider = enumField(args.provider, "provider", VALID_SPAWN_PROVIDERS) as SpawnProvider;
|
|
523
|
-
const orchestrator = selectSpawnOrchestrator(provider, optionalString(args.orchestratorId, "orchestratorId", 200));
|
|
524
523
|
const cwd = optionalString(args.cwd, "cwd", 500);
|
|
524
|
+
const orchestrator = selectSpawnOrchestrator(provider, optionalString(args.orchestratorId, "orchestratorId", 200), cwd);
|
|
525
525
|
const resolvedCwd = cwd || orchestrator.baseDir;
|
|
526
526
|
if (cwd && !pathWithinBase(cwd, orchestrator.baseDir)) {
|
|
527
527
|
throw new ValidationError(`cwd must be within orchestrator base directory: ${orchestrator.baseDir}`);
|
|
@@ -704,13 +704,21 @@ function policyStatusPayload(policy: NonNullable<ReturnType<typeof getSpawnPolic
|
|
|
704
704
|
};
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
function selectSpawnOrchestrator(provider: SpawnProvider, orchestratorId?: string): NonNullable<ReturnType<typeof getOrchestrator>> {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
707
|
+
function selectSpawnOrchestrator(provider: SpawnProvider, orchestratorId?: string, cwd?: string): NonNullable<ReturnType<typeof getOrchestrator>> {
|
|
708
|
+
if (orchestratorId) {
|
|
709
|
+
const orchestrator = getOrchestrator(orchestratorId);
|
|
710
|
+
if (!orchestrator) throw new McpNotFoundError(`orchestrator ${orchestratorId} not found`);
|
|
711
|
+
if (orchestrator.status !== "online") throw new ValidationError("orchestrator is offline");
|
|
712
|
+
if (!orchestrator.providers.includes(provider)) throw new ValidationError(`orchestrator does not have provider available: ${provider}`);
|
|
713
|
+
return orchestrator;
|
|
714
|
+
}
|
|
715
|
+
const candidates = listOrchestrators().filter((item) => item.status === "online" && item.providers.includes(provider));
|
|
716
|
+
if (cwd) {
|
|
717
|
+
const match = candidates.find((item) => pathWithinBase(cwd, item.baseDir));
|
|
718
|
+
if (match) return match;
|
|
719
|
+
}
|
|
720
|
+
const orchestrator = candidates[0];
|
|
721
|
+
if (!orchestrator) throw new McpNotFoundError(`no orchestrator available for provider: ${provider}`);
|
|
714
722
|
return orchestrator;
|
|
715
723
|
}
|
|
716
724
|
|
package/src/routes.ts
CHANGED
|
@@ -143,6 +143,7 @@ import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeP
|
|
|
143
143
|
import type { ProviderConfig } from "../runner/src/adapter";
|
|
144
144
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
145
145
|
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
146
|
+
import { buildManagedSpawnParams, effectiveManagedPolicyWorkspaceMode } from "./managed-policy";
|
|
146
147
|
import {
|
|
147
148
|
getComponentAuth,
|
|
148
149
|
getIntegrationAuth,
|
|
@@ -2232,16 +2233,35 @@ function restartSpawnParamsForAgent(
|
|
|
2232
2233
|
const label = metaString(agent.meta, "label") ?? agent.label;
|
|
2233
2234
|
const approvalMode = metaString(agent.meta, "approvalMode") as SpawnApprovalMode | undefined;
|
|
2234
2235
|
const model = metaString(agent.meta, "model");
|
|
2235
|
-
const effort = metaString(agent.meta, "effort");
|
|
2236
|
-
const providerModel = metaString(agent.meta, "providerModel");
|
|
2236
|
+
const effort = metaString(agent.meta, "effort") as ProviderEffort | undefined;
|
|
2237
2237
|
const providerArgs = metaStringArray(agent.meta, "providerArgs");
|
|
2238
|
+
const profileName = metaString(agent.meta, "profile");
|
|
2239
|
+
const agentProfile = profileName ? getAgentProfile(profileName)?.value : undefined;
|
|
2240
|
+
const workspaceMode = metaString(agent.meta, "workspaceMode") ?? "inherit";
|
|
2241
|
+
let resolvedModel: Record<string, string> = {};
|
|
2242
|
+
if (model || effort) {
|
|
2243
|
+
try {
|
|
2244
|
+
const selection = resolveProviderSelection({ provider, model, effort });
|
|
2245
|
+
resolvedModel = {
|
|
2246
|
+
...(selection.modelAlias ? { model: selection.modelAlias } : {}),
|
|
2247
|
+
...(selection.providerModel ? { providerModel: selection.providerModel } : {}),
|
|
2248
|
+
...(selection.effort ? { effort: selection.effort } : {}),
|
|
2249
|
+
};
|
|
2250
|
+
} catch {
|
|
2251
|
+
resolvedModel = {
|
|
2252
|
+
...(model ? { model } : {}),
|
|
2253
|
+
...(effort ? { effort } : {}),
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2238
2257
|
return {
|
|
2239
2258
|
action: "spawn",
|
|
2240
2259
|
provider,
|
|
2241
|
-
...
|
|
2242
|
-
...(providerModel ? { providerModel } : {}),
|
|
2243
|
-
...(effort ? { effort } : {}),
|
|
2260
|
+
...resolvedModel,
|
|
2244
2261
|
cwd,
|
|
2262
|
+
workspaceMode,
|
|
2263
|
+
...(profileName ? { profile: profileName } : {}),
|
|
2264
|
+
...(agentProfile ? { agentProfile } : {}),
|
|
2245
2265
|
...(label ? { label } : {}),
|
|
2246
2266
|
agentId: agent.id,
|
|
2247
2267
|
tags: agent.tags,
|
|
@@ -2250,6 +2270,7 @@ function restartSpawnParamsForAgent(
|
|
|
2250
2270
|
permissionMode: approvalMode ?? "guarded",
|
|
2251
2271
|
...(providerArgs.length ? { providerArgs } : {}),
|
|
2252
2272
|
...(policyName ? { policyName } : {}),
|
|
2273
|
+
headless: true,
|
|
2253
2274
|
spawnRequestId: requestId,
|
|
2254
2275
|
env: runnerRuntimeTokenEnv({
|
|
2255
2276
|
orchestratorId: orchestrator.id,
|
|
@@ -2269,6 +2290,15 @@ function spawnRequestIdForRestart(): string {
|
|
|
2269
2290
|
return `sp_${crypto.randomUUID()}`;
|
|
2270
2291
|
}
|
|
2271
2292
|
|
|
2293
|
+
function compactMemoryParams(agent: AgentCard): Record<string, unknown> {
|
|
2294
|
+
const alwaysReload = agent.tags
|
|
2295
|
+
.filter((tag) => tag.startsWith("memory-reload:"))
|
|
2296
|
+
.map((tag) => tag.slice("memory-reload:".length).trim())
|
|
2297
|
+
.filter(Boolean);
|
|
2298
|
+
if (!alwaysReload.length) return {};
|
|
2299
|
+
return { memory: { alwaysReload } };
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2272
2302
|
const postAgentAction: Handler = async (req, params) => {
|
|
2273
2303
|
const parsed = await parseBody<unknown>(req);
|
|
2274
2304
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -2304,6 +2334,7 @@ const postAgentAction: Handler = async (req, params) => {
|
|
|
2304
2334
|
...(metaPolicyName ? { policyName: metaPolicyName } : {}),
|
|
2305
2335
|
...(metaSpawnRequestId ? { spawnRequestId: metaSpawnRequestId } : {}),
|
|
2306
2336
|
...(action === "restart" ? { restartSpawn: restartSpawnParamsForAgent(agent, orchestrator, metaPolicyName, metaSpawnRequestId) } : {}),
|
|
2337
|
+
...(action === "compact" ? compactMemoryParams(agent) : {}),
|
|
2307
2338
|
requestedBy: "dashboard",
|
|
2308
2339
|
requestedAt: Date.now(),
|
|
2309
2340
|
},
|
|
@@ -3509,6 +3540,21 @@ function compareSemverCore(a: string, b: string): number {
|
|
|
3509
3540
|
return 0;
|
|
3510
3541
|
}
|
|
3511
3542
|
|
|
3543
|
+
function orchestratorSelfUpgradeError(orch: ReturnType<typeof getOrchestrator>): string | undefined {
|
|
3544
|
+
if (!orch) return "orchestrator not found";
|
|
3545
|
+
if (orch.status !== "online") return "orchestrator is offline";
|
|
3546
|
+
if (!orch.capabilities?.relayCommandBus) {
|
|
3547
|
+
return `orchestrator ${orch.id} does not support relay command bus upgrades; upgrade it manually first`;
|
|
3548
|
+
}
|
|
3549
|
+
if (!orch.capabilities?.selfUpgrade) {
|
|
3550
|
+
return `orchestrator ${orch.id}${orch.version ? ` v${orch.version}` : ""} does not advertise self-upgrade support; upgrade it manually once to ${VERSION} or newer`;
|
|
3551
|
+
}
|
|
3552
|
+
if ((orch.supervisor !== "systemd" && orch.supervisor !== "launchd") || !orch.selfUnit) {
|
|
3553
|
+
return `orchestrator ${orch.id} cannot self-upgrade under supervisor ${orch.supervisor ?? "unknown"}; upgrade it manually`;
|
|
3554
|
+
}
|
|
3555
|
+
return undefined;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3512
3558
|
/**
|
|
3513
3559
|
* Settle an in-flight orchestrator upgrade by comparing the version the
|
|
3514
3560
|
* orchestrator now reports against the desired version (decision 2a). Called on
|
|
@@ -3517,11 +3563,18 @@ function compareSemverCore(a: string, b: string): number {
|
|
|
3517
3563
|
function reconcileOrchestratorUpgrade(orch: ReturnType<typeof getOrchestrator>): void {
|
|
3518
3564
|
if (!orch) return;
|
|
3519
3565
|
const up = orch.upgrade;
|
|
3520
|
-
if (!up
|
|
3566
|
+
if (!up) return;
|
|
3521
3567
|
const reported = orch.version;
|
|
3522
|
-
if (reported && reported === up.desiredVersion) {
|
|
3568
|
+
if (reported && reported === up.desiredVersion && up.status === "succeeded" && up.error) {
|
|
3569
|
+
const { error: _previousError, ...successState } = up;
|
|
3570
|
+
setOrchestratorUpgradeState(orch.id, successState);
|
|
3571
|
+
emitOrchestratorStatus(orch.id);
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (reported && reported === up.desiredVersion && (up.status === "pending" || up.status === "failed")) {
|
|
3523
3575
|
if (up.commandId) updateCommand(up.commandId, { status: "succeeded", result: { version: reported, ...(up.fromVersion ? { fromVersion: up.fromVersion } : {}) } });
|
|
3524
|
-
|
|
3576
|
+
const { error: _previousError, ...successState } = up;
|
|
3577
|
+
setOrchestratorUpgradeState(orch.id, { ...successState, status: "succeeded", settledAt: Date.now() });
|
|
3525
3578
|
auditEvent({
|
|
3526
3579
|
clientId: `server-orchestrator-upgraded-${orch.id}-${Date.now()}`,
|
|
3527
3580
|
kind: "state",
|
|
@@ -3533,7 +3586,7 @@ function reconcileOrchestratorUpgrade(orch: ReturnType<typeof getOrchestrator>):
|
|
|
3533
3586
|
metadata: { orchestratorId: orch.id, version: reported, commandId: up.commandId },
|
|
3534
3587
|
});
|
|
3535
3588
|
emitOrchestratorStatus(orch.id);
|
|
3536
|
-
} else if (Date.now() - up.requestedAt > ORCH_UPGRADE_DEADLINE_MS) {
|
|
3589
|
+
} else if (up.status === "pending" && Date.now() - up.requestedAt > ORCH_UPGRADE_DEADLINE_MS) {
|
|
3537
3590
|
const errMsg = `upgrade timed out; orchestrator still reports ${reported ?? "unknown"} (wanted ${up.desiredVersion})`;
|
|
3538
3591
|
if (up.commandId) updateCommand(up.commandId, { status: "failed", error: errMsg });
|
|
3539
3592
|
setOrchestratorUpgradeState(orch.id, { ...up, status: "failed", settledAt: Date.now(), error: errMsg });
|
|
@@ -3551,6 +3604,28 @@ function reconcileOrchestratorUpgrade(orch: ReturnType<typeof getOrchestrator>):
|
|
|
3551
3604
|
}
|
|
3552
3605
|
}
|
|
3553
3606
|
|
|
3607
|
+
function settleFailedOrchestratorUpgrade(command: Command): void {
|
|
3608
|
+
if (command.type !== "orchestrator.upgrade" || command.status !== "failed") return;
|
|
3609
|
+
const orchestratorId = typeof command.params?.orchestratorId === "string" ? command.params.orchestratorId : undefined;
|
|
3610
|
+
if (!orchestratorId) return;
|
|
3611
|
+
const orch = getOrchestrator(orchestratorId);
|
|
3612
|
+
const up = orch?.upgrade;
|
|
3613
|
+
if (!orch || !up || up.status !== "pending" || up.commandId !== command.id) return;
|
|
3614
|
+
const errMsg = command.error ?? "orchestrator upgrade failed";
|
|
3615
|
+
setOrchestratorUpgradeState(orch.id, { ...up, status: "failed", settledAt: Date.now(), error: errMsg });
|
|
3616
|
+
auditEvent({
|
|
3617
|
+
clientId: `server-orchestrator-upgrade-command-failed-${orch.id}-${command.id}`,
|
|
3618
|
+
kind: "state",
|
|
3619
|
+
title: "Orchestrator upgrade failed",
|
|
3620
|
+
body: errMsg,
|
|
3621
|
+
meta: orch.id,
|
|
3622
|
+
icon: "ti-alert-triangle",
|
|
3623
|
+
view: "orchestrators",
|
|
3624
|
+
metadata: { orchestratorId: orch.id, commandId: command.id },
|
|
3625
|
+
});
|
|
3626
|
+
emitOrchestratorStatus(orch.id);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3554
3629
|
const postOrchestratorAction: Handler = async (req, params) => {
|
|
3555
3630
|
const parsed = await parseBody<unknown>(req);
|
|
3556
3631
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -3571,6 +3646,8 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
3571
3646
|
const providers = (cleanStringArray(parsed.body.providers, "providers") ?? ["orchestrator"])
|
|
3572
3647
|
.filter((p) => (VALID_ORCH_UPGRADE_PROVIDERS as readonly string[]).includes(p));
|
|
3573
3648
|
if (!providers.length) return error(`providers must be any of: ${VALID_ORCH_UPGRADE_PROVIDERS.join(", ")}`);
|
|
3649
|
+
const selfUpgradeError = orchestratorSelfUpgradeError(orch);
|
|
3650
|
+
if (selfUpgradeError) return error(selfUpgradeError, 409);
|
|
3574
3651
|
if (!force && orch.version && compareSemverCore(targetVersion, orch.version) < 0) {
|
|
3575
3652
|
return error(`refusing downgrade ${orch.version} → ${targetVersion}; pass force to override`, 409);
|
|
3576
3653
|
}
|
|
@@ -4284,6 +4361,7 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4284
4361
|
}
|
|
4285
4362
|
}
|
|
4286
4363
|
}
|
|
4364
|
+
settleFailedOrchestratorUpgrade(command);
|
|
4287
4365
|
emitCommand(command);
|
|
4288
4366
|
auditCommandOutcome(command);
|
|
4289
4367
|
return json(command);
|
|
@@ -4429,60 +4507,9 @@ function policyStatusPayload(policy: SpawnPolicy) {
|
|
|
4429
4507
|
}
|
|
4430
4508
|
|
|
4431
4509
|
function managedSpawnParams(policy: SpawnPolicy, requestId: string): Record<string, unknown> {
|
|
4432
|
-
|
|
4433
|
-
const providerArgs = managedPolicyProviderArgs(policy);
|
|
4434
|
-
const prompt = managedPolicyLaunchPrompt(policy);
|
|
4435
|
-
const agentProfile = policy.profile ? getAgentProfile(policy.profile)?.value : undefined;
|
|
4436
|
-
return {
|
|
4437
|
-
action: "spawn",
|
|
4438
|
-
provider: policy.provider,
|
|
4439
|
-
model: selection.modelAlias,
|
|
4440
|
-
providerModel: selection.providerModel,
|
|
4441
|
-
effort: selection.effort,
|
|
4442
|
-
profile: policy.profile,
|
|
4443
|
-
agentProfile,
|
|
4444
|
-
cwd: policy.cwd,
|
|
4445
|
-
workspaceMode: effectiveManagedPolicyWorkspaceMode(policy),
|
|
4446
|
-
label: policy.label,
|
|
4447
|
-
tags: policy.tags,
|
|
4448
|
-
capabilities: policy.capabilities,
|
|
4449
|
-
approvalMode: policy.permissionMode,
|
|
4450
|
-
permissionMode: policy.permissionMode,
|
|
4451
|
-
providerArgs,
|
|
4452
|
-
...(prompt ? { prompt } : {}),
|
|
4453
|
-
headless: true,
|
|
4454
|
-
policyName: policy.name,
|
|
4455
|
-
spawnRequestId: requestId,
|
|
4456
|
-
env: runnerRuntimeTokenEnv({
|
|
4457
|
-
orchestratorId: policy.orchestratorId,
|
|
4458
|
-
cwd: policy.cwd,
|
|
4459
|
-
provider: policy.provider,
|
|
4460
|
-
label: policy.label,
|
|
4461
|
-
policyName: policy.name,
|
|
4462
|
-
spawnRequestId: requestId,
|
|
4463
|
-
createdBy: "managed-agent",
|
|
4464
|
-
}),
|
|
4465
|
-
requestedBy: "managed-agent",
|
|
4466
|
-
requestedAt: Date.now(),
|
|
4467
|
-
};
|
|
4510
|
+
return buildManagedSpawnParams(policy, requestId, { createdBy: "managed-agent" });
|
|
4468
4511
|
}
|
|
4469
4512
|
|
|
4470
|
-
function effectiveManagedPolicyWorkspaceMode(policy: SpawnPolicy): WorkspaceMode {
|
|
4471
|
-
if (policy.workspaceMode && policy.workspaceMode !== "inherit") return policy.workspaceMode;
|
|
4472
|
-
return policy.binding?.type === "channel" ? "shared" : "inherit";
|
|
4473
|
-
}
|
|
4474
|
-
|
|
4475
|
-
function managedPolicyProviderArgs(policy: SpawnPolicy): string[] {
|
|
4476
|
-
if (policy.provider === "claude" && policy.mode === "always-on" && policy.prompt?.trim()) {
|
|
4477
|
-
return [...policy.providerArgs, "--append-system-prompt", policy.prompt.trim()];
|
|
4478
|
-
}
|
|
4479
|
-
return policy.providerArgs;
|
|
4480
|
-
}
|
|
4481
|
-
|
|
4482
|
-
function managedPolicyLaunchPrompt(policy: SpawnPolicy): string | undefined {
|
|
4483
|
-
if (policy.provider === "claude" && policy.mode === "always-on") return undefined;
|
|
4484
|
-
return policy.prompt;
|
|
4485
|
-
}
|
|
4486
4513
|
|
|
4487
4514
|
function requirePolicyAndOrchestrator(name: string): { policy: SpawnPolicy; orch: NonNullable<ReturnType<typeof getOrchestrator>> } | Response {
|
|
4488
4515
|
const entry = getSpawnPolicy(name);
|