agent-relay-server 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/public/dashboard/actions.js +819 -0
- package/public/dashboard/api.js +336 -0
- package/public/dashboard/app.js +34 -0
- package/public/dashboard/charts.js +128 -0
- package/public/dashboard/computed.js +693 -0
- package/public/dashboard/constants.js +28 -0
- package/public/dashboard/display.js +345 -0
- package/public/dashboard/state.js +129 -0
- package/public/dashboard/utils.js +207 -0
- package/public/index.html +61 -41
- package/scripts/orchestrator-spawn-smoke.ts +140 -0
- package/src/cli.ts +5 -4
- package/src/config.ts +1 -0
- package/src/db.ts +205 -23
- package/src/routes.ts +74 -48
- package/src/types.ts +33 -0
- package/src/upgrade.ts +80 -7
- package/public/dashboard.js +0 -3019
package/src/db.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { VERSION } from "./config.ts";
|
|
3
|
+
import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "./config.ts";
|
|
4
4
|
import type {
|
|
5
5
|
AgentCard,
|
|
6
6
|
ActivityEvent,
|
|
@@ -11,12 +11,14 @@ import type {
|
|
|
11
11
|
ChannelBindingMode,
|
|
12
12
|
ChannelRouteTarget,
|
|
13
13
|
ChannelSummary,
|
|
14
|
+
ChannelTargetHealth,
|
|
14
15
|
CreatePairInput,
|
|
15
16
|
HealthCheck,
|
|
16
17
|
HealthReport,
|
|
17
18
|
ManagedAgent,
|
|
18
19
|
Message,
|
|
19
20
|
Orchestrator,
|
|
21
|
+
OrchestratorHealth,
|
|
20
22
|
OrchestratorStatus,
|
|
21
23
|
PairActionInput,
|
|
22
24
|
PairMessageInput,
|
|
@@ -628,6 +630,94 @@ function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
|
|
|
628
630
|
return "";
|
|
629
631
|
}
|
|
630
632
|
|
|
633
|
+
function channelTargetMatches(target: ChannelRouteTarget): AgentCard[] {
|
|
634
|
+
const candidates = listAgents().filter((agent) => (
|
|
635
|
+
agent.id !== "user" &&
|
|
636
|
+
agent.id !== "system" &&
|
|
637
|
+
agent.kind !== "channel" &&
|
|
638
|
+
agent.meta?.kind !== "channel"
|
|
639
|
+
));
|
|
640
|
+
if (target.type === "agent" || target.type === "orchestrator") {
|
|
641
|
+
const agent = getAgent(target.id);
|
|
642
|
+
return agent ? [agent] : [];
|
|
643
|
+
}
|
|
644
|
+
if (target.type === "label") return candidates.filter((agent) => agent.label === target.id);
|
|
645
|
+
if (target.type === "tag") return candidates.filter((agent) => agent.tags.includes(target.id));
|
|
646
|
+
if (target.type === "capability") return candidates.filter((agent) => agent.capabilities.includes(target.id));
|
|
647
|
+
if (target.type === "broadcast") return candidates;
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function channelTargetMatchSnapshot(agent: AgentCard): ChannelTargetHealth["matches"][number] {
|
|
652
|
+
return {
|
|
653
|
+
id: agent.id,
|
|
654
|
+
name: agent.name,
|
|
655
|
+
status: agent.status,
|
|
656
|
+
ready: agent.ready,
|
|
657
|
+
lastSeen: agent.lastSeen,
|
|
658
|
+
label: agent.label,
|
|
659
|
+
tags: agent.tags,
|
|
660
|
+
capabilities: agent.capabilities,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isHealthyChannelTarget(agent: AgentCard, now: number): boolean {
|
|
665
|
+
return isDeliveryAgent(agent) && agent.ready && agent.lastSeen > now - STALE_TTL_MS;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function describeTarget(target: ChannelRouteTarget): string {
|
|
669
|
+
return bindingTargetToLegacyTarget(target);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function channelTargetHealth(binding: ChannelBinding, now: number = Date.now()): ChannelTargetHealth {
|
|
673
|
+
const target = binding.target;
|
|
674
|
+
const targetLabel = describeTarget(target);
|
|
675
|
+
const matches = channelTargetMatches(target);
|
|
676
|
+
const snapshots = matches.map(channelTargetMatchSnapshot);
|
|
677
|
+
|
|
678
|
+
if (target.type === "agent" || target.type === "orchestrator") {
|
|
679
|
+
const agent = matches[0];
|
|
680
|
+
if (!agent) {
|
|
681
|
+
return { status: "error", detail: `Target ${targetLabel} is not registered`, target, matches: [] };
|
|
682
|
+
}
|
|
683
|
+
if (agent.id !== "user" && agent.id !== "system" && (agent.kind === "channel" || agent.meta?.kind === "channel")) {
|
|
684
|
+
return { status: "error", detail: `Target ${targetLabel} is a channel, not a delivery agent`, target, matches: snapshots };
|
|
685
|
+
}
|
|
686
|
+
if (agent.status === "offline") {
|
|
687
|
+
return { status: "error", detail: `Target ${targetLabel} is offline`, target, matches: snapshots };
|
|
688
|
+
}
|
|
689
|
+
if (!agent.ready) {
|
|
690
|
+
return { status: "warning", detail: `Target ${targetLabel} is online but not ready`, target, matches: snapshots };
|
|
691
|
+
}
|
|
692
|
+
if (agent.id !== "user" && agent.id !== "system" && agent.lastSeen <= now - STALE_TTL_MS) {
|
|
693
|
+
return { status: "warning", detail: `Target ${targetLabel} heartbeat is stale`, target, matches: snapshots };
|
|
694
|
+
}
|
|
695
|
+
return { status: "ok", detail: `Target ${targetLabel} is online and ready`, target, matches: snapshots };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const healthyMatches = matches.filter((agent) => isHealthyChannelTarget(agent, now));
|
|
699
|
+
if (matches.length === 0) {
|
|
700
|
+
return { status: "error", detail: `Target ${targetLabel} has no matching agents`, target, matches: [] };
|
|
701
|
+
}
|
|
702
|
+
if (healthyMatches.length === 0) {
|
|
703
|
+
return { status: "error", detail: `Target ${targetLabel} has no online ready agents`, target, matches: snapshots };
|
|
704
|
+
}
|
|
705
|
+
if (binding.mode === "exclusive" && healthyMatches.length > 1) {
|
|
706
|
+
return {
|
|
707
|
+
status: "warning",
|
|
708
|
+
detail: `Target ${targetLabel} matches ${healthyMatches.length} online ready agents for an exclusive binding`,
|
|
709
|
+
target,
|
|
710
|
+
matches: snapshots,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
status: "ok",
|
|
715
|
+
detail: `Target ${targetLabel} has ${healthyMatches.length} online ready agent${healthyMatches.length === 1 ? "" : "s"}`,
|
|
716
|
+
target,
|
|
717
|
+
matches: snapshots,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
631
721
|
function rowToChannelSummary(row: any): ChannelSummary {
|
|
632
722
|
const binding = row.binding_id ? rowToChannelBinding({
|
|
633
723
|
id: row.binding_id,
|
|
@@ -653,6 +743,7 @@ function rowToChannelSummary(row: any): ChannelSummary {
|
|
|
653
743
|
direction: row.direction,
|
|
654
744
|
target: binding ? bindingTargetToLegacyTarget(binding.target) : undefined,
|
|
655
745
|
binding,
|
|
746
|
+
targetHealth: binding ? channelTargetHealth(binding) : undefined,
|
|
656
747
|
topicChannels: parseStringArray(row.topic_channels),
|
|
657
748
|
capabilities: parseStringArray(row.channel_capabilities),
|
|
658
749
|
tags: parseStringArray(row.agent_tags),
|
|
@@ -965,25 +1056,31 @@ export function upsertChannelBinding(input: {
|
|
|
965
1056
|
const targetId = input.target.type === "broadcast" ? "" : input.target.id;
|
|
966
1057
|
const targetKey = input.target.type === "broadcast" ? "broadcast" : `${input.target.type}:${targetId}`;
|
|
967
1058
|
const id = `${input.channelId}:${conversationKey || "default"}:${targetKey}`;
|
|
1059
|
+
const mode = input.mode ?? "exclusive";
|
|
968
1060
|
const now = Date.now();
|
|
969
|
-
db.
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1061
|
+
db.transaction(() => {
|
|
1062
|
+
if (mode === "exclusive") {
|
|
1063
|
+
db.prepare("DELETE FROM channel_bindings WHERE channel_id = ? AND conversation_key = ?").run(input.channelId, conversationKey);
|
|
1064
|
+
}
|
|
1065
|
+
db.prepare(`
|
|
1066
|
+
INSERT INTO channel_bindings (id, channel_id, conversation_key, conversation_id, target_type, target_id, mode, priority, created_at, updated_at)
|
|
1067
|
+
VALUES ($id, $channelId, $conversationKey, $conversationId, $targetType, $targetId, $mode, $priority, $now, $now)
|
|
1068
|
+
ON CONFLICT(channel_id, conversation_key, target_type, target_id) DO UPDATE SET
|
|
1069
|
+
mode = $mode,
|
|
1070
|
+
priority = $priority,
|
|
1071
|
+
updated_at = $now
|
|
1072
|
+
`).run({
|
|
1073
|
+
$id: id,
|
|
1074
|
+
$channelId: input.channelId,
|
|
1075
|
+
$conversationKey: conversationKey,
|
|
1076
|
+
$conversationId: input.conversationId ?? null,
|
|
1077
|
+
$targetType: input.target.type,
|
|
1078
|
+
$targetId: targetId,
|
|
1079
|
+
$mode: mode,
|
|
1080
|
+
$priority: input.priority ?? 0,
|
|
1081
|
+
$now: now,
|
|
1082
|
+
});
|
|
1083
|
+
})();
|
|
987
1084
|
|
|
988
1085
|
const row = db.prepare("SELECT * FROM channel_bindings WHERE id = ?").get(id) as any;
|
|
989
1086
|
return rowToChannelBinding(row);
|
|
@@ -1733,6 +1830,21 @@ function isChannelAgentId(agentId: string): boolean {
|
|
|
1733
1830
|
));
|
|
1734
1831
|
}
|
|
1735
1832
|
|
|
1833
|
+
function legacyChannelTargets(agent: AgentCard | null | undefined): string[] {
|
|
1834
|
+
if (!agent || !isChannelAgentId(agent.id)) return [];
|
|
1835
|
+
const aliases = new Set<string>();
|
|
1836
|
+
const provider = channelProviderForAgent(agent);
|
|
1837
|
+
if (provider && provider !== "custom") aliases.add(provider);
|
|
1838
|
+
const channelType = stringValue(agent.meta?.channelType);
|
|
1839
|
+
if (channelType) aliases.add(channelType);
|
|
1840
|
+
const transport = stringValue(agent.meta?.transport);
|
|
1841
|
+
if (transport) aliases.add(transport);
|
|
1842
|
+
const providerTag = agent.tags.find((tag) => tag.startsWith("channel:"))?.slice("channel:".length);
|
|
1843
|
+
if (providerTag) aliases.add(providerTag);
|
|
1844
|
+
aliases.delete(agent.id);
|
|
1845
|
+
return [...aliases];
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1736
1848
|
function matchingDeliveryAgents(target: string): AgentCard[] {
|
|
1737
1849
|
if (!target) return [];
|
|
1738
1850
|
const candidates = listAgents().filter(isDeliveryAgent);
|
|
@@ -1963,11 +2075,15 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
1963
2075
|
const agentTags = agent?.tags ?? [];
|
|
1964
2076
|
const agentCaps = agent?.capabilities ?? [];
|
|
1965
2077
|
const agentLabel = agent?.label;
|
|
2078
|
+
const agentLegacyChannelTargets = legacyChannelTargets(agent);
|
|
1966
2079
|
|
|
1967
2080
|
const conditions: string[] = [];
|
|
1968
2081
|
const params: any[] = [];
|
|
1969
2082
|
|
|
1970
|
-
// Build target matching: direct + broadcast + tag + capability + label
|
|
2083
|
+
// Build target matching: direct + broadcast + tag + capability + label.
|
|
2084
|
+
// Channel agents also accept legacy bare provider targets (for example
|
|
2085
|
+
// "telegram") so older clients keep working after canonical IDs became
|
|
2086
|
+
// provider:account ("telegram:default").
|
|
1971
2087
|
const targetClauses = ["to_target = ?", "to_target = 'broadcast'"];
|
|
1972
2088
|
params.push(query.for);
|
|
1973
2089
|
|
|
@@ -1983,6 +2099,10 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
1983
2099
|
targetClauses.push("to_target = ?");
|
|
1984
2100
|
params.push(`label:${agentLabel}`);
|
|
1985
2101
|
}
|
|
2102
|
+
for (const target of agentLegacyChannelTargets) {
|
|
2103
|
+
targetClauses.push("to_target = ?");
|
|
2104
|
+
params.push(target);
|
|
2105
|
+
}
|
|
1986
2106
|
conditions.push(`(${targetClauses.join(" OR ")})`);
|
|
1987
2107
|
|
|
1988
2108
|
// Hide active claims held by someone else, but let expired claims surface so
|
|
@@ -2267,6 +2387,21 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
2267
2387
|
detail: offlineClaimedTasks > 0 ? `${offlineClaimedTasks} active task(s) are claimed by offline agents` : undefined,
|
|
2268
2388
|
});
|
|
2269
2389
|
|
|
2390
|
+
const unhealthyChannelTargets = listChannels().filter((channel) => channel.targetHealth && channel.targetHealth.status !== "ok");
|
|
2391
|
+
const channelTargetErrors = unhealthyChannelTargets.filter((channel) => channel.targetHealth?.status === "error");
|
|
2392
|
+
const channelTargetDetail = unhealthyChannelTargets
|
|
2393
|
+
.slice(0, 3)
|
|
2394
|
+
.map((channel) => `${channel.name}: ${channel.targetHealth?.detail}`)
|
|
2395
|
+
.join("; ");
|
|
2396
|
+
checks.push({
|
|
2397
|
+
name: "channel-delivery-targets",
|
|
2398
|
+
status: channelTargetErrors.length > 0 ? "error" : unhealthyChannelTargets.length > 0 ? "warn" : "ok",
|
|
2399
|
+
count: unhealthyChannelTargets.length,
|
|
2400
|
+
detail: unhealthyChannelTargets.length > 0
|
|
2401
|
+
? `${unhealthyChannelTargets.length} channel delivery target issue(s): ${channelTargetDetail}${unhealthyChannelTargets.length > 3 ? "; ..." : ""}`
|
|
2402
|
+
: undefined,
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2270
2405
|
const status = checks.some((check) => check.status === "error")
|
|
2271
2406
|
? "error"
|
|
2272
2407
|
: checks.some((check) => check.status === "warn")
|
|
@@ -2278,6 +2413,15 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
2278
2413
|
// --- Orchestrators ---
|
|
2279
2414
|
|
|
2280
2415
|
function rowToOrchestrator(row: any): Orchestrator {
|
|
2416
|
+
const meta = parseJson<Record<string, unknown>>(row.meta, {});
|
|
2417
|
+
const version = stringValue(meta.version);
|
|
2418
|
+
const gitSha = stringValue(meta.gitSha);
|
|
2419
|
+
const protocolRaw = meta.protocolVersion;
|
|
2420
|
+
const protocolVersion = typeof protocolRaw === "number"
|
|
2421
|
+
? protocolRaw
|
|
2422
|
+
: typeof protocolRaw === "string" && protocolRaw.trim() !== ""
|
|
2423
|
+
? Number(protocolRaw)
|
|
2424
|
+
: undefined;
|
|
2281
2425
|
return {
|
|
2282
2426
|
id: row.id,
|
|
2283
2427
|
hostname: row.hostname,
|
|
@@ -2286,13 +2430,40 @@ function rowToOrchestrator(row: any): Orchestrator {
|
|
|
2286
2430
|
providers: parseJson<SpawnProvider[]>(row.providers, []),
|
|
2287
2431
|
baseDir: row.base_dir,
|
|
2288
2432
|
envKeys: parseJson<string[]>(row.env_keys, []),
|
|
2289
|
-
|
|
2433
|
+
...(version ? { version } : {}),
|
|
2434
|
+
...(Number.isFinite(protocolVersion) ? { protocolVersion } : {}),
|
|
2435
|
+
...(gitSha ? { gitSha } : {}),
|
|
2436
|
+
health: orchestratorHealth(version, Number.isFinite(protocolVersion) ? protocolVersion : undefined),
|
|
2437
|
+
meta,
|
|
2290
2438
|
managedAgents: parseJson<ManagedAgent[]>(row.managed_agents, []),
|
|
2291
2439
|
lastSeen: row.last_seen,
|
|
2292
2440
|
createdAt: row.created_at,
|
|
2293
2441
|
};
|
|
2294
2442
|
}
|
|
2295
2443
|
|
|
2444
|
+
function orchestratorHealth(version: string | undefined, protocolVersion: number | undefined): OrchestratorHealth {
|
|
2445
|
+
const issues: OrchestratorHealth["issues"] = [];
|
|
2446
|
+
if (!version) {
|
|
2447
|
+
issues.push({ code: "missing-version", detail: "Orchestrator did not report a version; restart or upgrade it." });
|
|
2448
|
+
} else if (version !== VERSION) {
|
|
2449
|
+
issues.push({ code: "outdated", detail: `Orchestrator ${version} does not match server ${VERSION}.` });
|
|
2450
|
+
}
|
|
2451
|
+
if (protocolVersion !== ORCHESTRATOR_PROTOCOL_VERSION) {
|
|
2452
|
+
issues.push({
|
|
2453
|
+
code: "protocol-mismatch",
|
|
2454
|
+
detail: `Orchestrator protocol ${protocolVersion ?? "unknown"} does not match server protocol ${ORCHESTRATOR_PROTOCOL_VERSION}.`,
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
if (issues.length > 0) {
|
|
2458
|
+
issues.push({ code: "restart-required", detail: "Restart the orchestrator after upgrading Agent Relay." });
|
|
2459
|
+
}
|
|
2460
|
+
return {
|
|
2461
|
+
status: issues.some((issue) => issue.code === "protocol-mismatch") ? "error" : issues.length > 0 ? "warn" : "ok",
|
|
2462
|
+
restartRequired: issues.length > 0,
|
|
2463
|
+
issues,
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2296
2467
|
export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrator {
|
|
2297
2468
|
const now = Date.now();
|
|
2298
2469
|
const agentId = `orchestrator-${input.id}`;
|
|
@@ -2315,7 +2486,12 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
|
|
|
2315
2486
|
$providers: JSON.stringify(input.providers),
|
|
2316
2487
|
$baseDir: input.baseDir,
|
|
2317
2488
|
$envKeys: JSON.stringify(input.envKeys ?? []),
|
|
2318
|
-
$meta: JSON.stringify(
|
|
2489
|
+
$meta: JSON.stringify({
|
|
2490
|
+
...(input.meta ?? {}),
|
|
2491
|
+
...(input.version ? { version: input.version } : {}),
|
|
2492
|
+
...(input.protocolVersion !== undefined ? { protocolVersion: input.protocolVersion } : {}),
|
|
2493
|
+
...(input.gitSha ? { gitSha: input.gitSha } : {}),
|
|
2494
|
+
}),
|
|
2319
2495
|
$now: now,
|
|
2320
2496
|
});
|
|
2321
2497
|
|
|
@@ -2327,7 +2503,13 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
|
|
|
2327
2503
|
machine: input.hostname,
|
|
2328
2504
|
capabilities: ["orchestrator", "spawn"],
|
|
2329
2505
|
status: "online",
|
|
2330
|
-
meta: {
|
|
2506
|
+
meta: {
|
|
2507
|
+
orchestratorId: input.id,
|
|
2508
|
+
builtin: true,
|
|
2509
|
+
...(input.version ? { version: input.version } : {}),
|
|
2510
|
+
...(input.protocolVersion !== undefined ? { protocolVersion: input.protocolVersion } : {}),
|
|
2511
|
+
...(input.gitSha ? { gitSha: input.gitSha } : {}),
|
|
2512
|
+
},
|
|
2331
2513
|
});
|
|
2332
2514
|
|
|
2333
2515
|
return getOrchestrator(input.id)!;
|
package/src/routes.ts
CHANGED
|
@@ -775,6 +775,44 @@ function auditEvent(input: ActivityEventInput): void {
|
|
|
775
775
|
}
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
type AgentActivityState = {
|
|
779
|
+
key: "available" | "busy" | "not-ready" | "offline";
|
|
780
|
+
title: string;
|
|
781
|
+
icon: string;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
function agentActivityState(agent?: Pick<AgentCard, "ready" | "status"> | null): AgentActivityState | null {
|
|
785
|
+
if (!agent) return null;
|
|
786
|
+
if (agent.status === "offline") return { key: "offline", title: "Agent offline", icon: "ti-plug-off" };
|
|
787
|
+
if (!agent.ready) return { key: "not-ready", title: "Agent not ready", icon: "ti-loader" };
|
|
788
|
+
if (agent.status === "busy") return { key: "busy", title: "Agent busy", icon: "ti-activity" };
|
|
789
|
+
return { key: "available", title: "Agent available", icon: "ti-circle-check" };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function auditAgentStateTransition(agentId: string, before: AgentCard | null | undefined, after: AgentCard | null | undefined): void {
|
|
793
|
+
const previous = agentActivityState(before);
|
|
794
|
+
const next = agentActivityState(after);
|
|
795
|
+
if (!next || previous?.key === next.key) return;
|
|
796
|
+
auditEvent({
|
|
797
|
+
clientId: "server-agent-" + agentId + "-state-" + next.key + "-" + Date.now(),
|
|
798
|
+
kind: "state",
|
|
799
|
+
title: next.title,
|
|
800
|
+
meta: agentId,
|
|
801
|
+
icon: next.icon,
|
|
802
|
+
view: "agents",
|
|
803
|
+
agentId,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function orchestratorControlMeta(control: Record<string, unknown>): Record<string, unknown> {
|
|
808
|
+
return {
|
|
809
|
+
delivery: "interrupt",
|
|
810
|
+
priority: "urgent",
|
|
811
|
+
// Legacy orchestrators read control details from message meta.
|
|
812
|
+
orchestratorControl: control,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
778
816
|
// --- Agent routes ---
|
|
779
817
|
|
|
780
818
|
const postAgent: Handler = async (req) => {
|
|
@@ -836,21 +874,14 @@ const patchAgentStatus: Handler = async (req, params) => {
|
|
|
836
874
|
const guard = normalizeAgentSessionGuard(req, body);
|
|
837
875
|
const session = validateAgentSession(params.id!, guard);
|
|
838
876
|
if (!session.ok) return error(session.error!, agentSessionStatus(session.error));
|
|
877
|
+
const before = getAgent(params.id!);
|
|
839
878
|
if (!setStatus(params.id!, body.status as any, guard)) return error("agent not found", 404);
|
|
879
|
+
auditAgentStateTransition(params.id!, before, getAgent(params.id!));
|
|
840
880
|
} catch (e) {
|
|
841
881
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
842
882
|
throw e;
|
|
843
883
|
}
|
|
844
884
|
emitAgentStatus(params.id!);
|
|
845
|
-
auditEvent({
|
|
846
|
-
clientId: "server-agent-" + params.id! + "-status-" + body.status + "-" + Date.now(),
|
|
847
|
-
kind: "state",
|
|
848
|
-
title: "Agent " + body.status,
|
|
849
|
-
meta: params.id!,
|
|
850
|
-
icon: body.status === "offline" ? "ti-plug-off" : "ti-activity",
|
|
851
|
-
view: "agents",
|
|
852
|
-
agentId: params.id!,
|
|
853
|
-
});
|
|
854
885
|
return json({ ok: true });
|
|
855
886
|
};
|
|
856
887
|
|
|
@@ -903,21 +934,14 @@ const patchAgentReady: Handler = async (req, params) => {
|
|
|
903
934
|
const guard = normalizeAgentSessionGuard(req, body);
|
|
904
935
|
const session = validateAgentSession(params.id!, guard);
|
|
905
936
|
if (!session.ok) return error(session.error!, agentSessionStatus(session.error));
|
|
937
|
+
const before = getAgent(params.id!);
|
|
906
938
|
if (!markReady(params.id!, body.ready, guard)) return error("agent not found", 404);
|
|
939
|
+
auditAgentStateTransition(params.id!, before, getAgent(params.id!));
|
|
907
940
|
} catch (e) {
|
|
908
941
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
909
942
|
throw e;
|
|
910
943
|
}
|
|
911
944
|
emitAgentStatus(params.id!);
|
|
912
|
-
auditEvent({
|
|
913
|
-
clientId: "server-agent-" + params.id! + "-ready-" + body.ready + "-" + Date.now(),
|
|
914
|
-
kind: "state",
|
|
915
|
-
title: body.ready ? "Agent ready" : "Agent not ready",
|
|
916
|
-
meta: params.id!,
|
|
917
|
-
icon: body.ready ? "ti-circle-check" : "ti-loader",
|
|
918
|
-
view: "agents",
|
|
919
|
-
agentId: params.id!,
|
|
920
|
-
});
|
|
921
945
|
return json({ ok: true });
|
|
922
946
|
};
|
|
923
947
|
|
|
@@ -1008,17 +1032,15 @@ const postAgentSpawn: Handler = async (req) => {
|
|
|
1008
1032
|
if (orchestrators.length > 0) {
|
|
1009
1033
|
// Route through the first available orchestrator
|
|
1010
1034
|
const orch = orchestrators[0]!;
|
|
1035
|
+
const control = { action: "spawn", provider, cwd, label, approvalMode, requestedBy: "dashboard", requestedAt: Date.now() };
|
|
1011
1036
|
const msg = sendMessage({
|
|
1012
1037
|
from: "system",
|
|
1013
1038
|
to: orch.agentId,
|
|
1014
1039
|
kind: "control",
|
|
1015
1040
|
subject: "Spawn agent",
|
|
1016
1041
|
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1017
|
-
payload: { orchestratorControl:
|
|
1018
|
-
meta:
|
|
1019
|
-
delivery: "interrupt",
|
|
1020
|
-
priority: "urgent",
|
|
1021
|
-
},
|
|
1042
|
+
payload: { orchestratorControl: control },
|
|
1043
|
+
meta: orchestratorControlMeta(control),
|
|
1022
1044
|
});
|
|
1023
1045
|
emitNewMessage(msg);
|
|
1024
1046
|
auditEvent({
|
|
@@ -1104,8 +1126,16 @@ const postOrchestrator: Handler = async (req) => {
|
|
|
1104
1126
|
}
|
|
1105
1127
|
}
|
|
1106
1128
|
const envKeys = cleanStringArray(parsed.body.envKeys, "envKeys");
|
|
1129
|
+
const version = cleanString(parsed.body.version, "version", { max: 80 });
|
|
1130
|
+
const gitSha = cleanString(parsed.body.gitSha, "gitSha", { max: 80 });
|
|
1131
|
+
const protocolVersionRaw = parsed.body.protocolVersion;
|
|
1132
|
+
const protocolVersion = protocolVersionRaw === undefined
|
|
1133
|
+
? undefined
|
|
1134
|
+
: typeof protocolVersionRaw === "number" && Number.isInteger(protocolVersionRaw) && protocolVersionRaw > 0
|
|
1135
|
+
? protocolVersionRaw
|
|
1136
|
+
: (() => { throw new ValidationError("protocolVersion must be a positive integer"); })();
|
|
1107
1137
|
const meta = cleanMeta(parsed.body.meta);
|
|
1108
|
-
const orch = upsertOrchestrator({ id, hostname, providers: providers ?? ["claude", "codex"], baseDir, envKeys, meta });
|
|
1138
|
+
const orch = upsertOrchestrator({ id, hostname, providers: providers ?? ["claude", "codex"], baseDir, envKeys, version, protocolVersion, gitSha, meta });
|
|
1109
1139
|
auditEvent({
|
|
1110
1140
|
clientId: "server-orchestrator-register-" + id + "-" + Date.now(),
|
|
1111
1141
|
kind: "state",
|
|
@@ -1192,6 +1222,16 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
1192
1222
|
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 4000 });
|
|
1193
1223
|
|
|
1194
1224
|
// Send control message to orchestrator's agent inbox
|
|
1225
|
+
const control = {
|
|
1226
|
+
action: "spawn",
|
|
1227
|
+
provider,
|
|
1228
|
+
cwd: cwd || orch.baseDir,
|
|
1229
|
+
label,
|
|
1230
|
+
approvalMode,
|
|
1231
|
+
prompt,
|
|
1232
|
+
requestedBy: "dashboard",
|
|
1233
|
+
requestedAt: Date.now(),
|
|
1234
|
+
};
|
|
1195
1235
|
const msg = sendMessage({
|
|
1196
1236
|
from: "system",
|
|
1197
1237
|
to: orch.agentId,
|
|
@@ -1199,21 +1239,9 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
1199
1239
|
subject: "Spawn agent",
|
|
1200
1240
|
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1201
1241
|
payload: {
|
|
1202
|
-
orchestratorControl:
|
|
1203
|
-
action: "spawn",
|
|
1204
|
-
provider,
|
|
1205
|
-
cwd: cwd || orch.baseDir,
|
|
1206
|
-
label,
|
|
1207
|
-
approvalMode,
|
|
1208
|
-
prompt,
|
|
1209
|
-
requestedBy: "dashboard",
|
|
1210
|
-
requestedAt: Date.now(),
|
|
1211
|
-
},
|
|
1212
|
-
},
|
|
1213
|
-
meta: {
|
|
1214
|
-
delivery: "interrupt",
|
|
1215
|
-
priority: "urgent",
|
|
1242
|
+
orchestratorControl: control,
|
|
1216
1243
|
},
|
|
1244
|
+
meta: orchestratorControlMeta(control),
|
|
1217
1245
|
});
|
|
1218
1246
|
emitNewMessage(msg);
|
|
1219
1247
|
auditEvent({
|
|
@@ -1245,6 +1273,12 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
1245
1273
|
if (!action) return error("action required");
|
|
1246
1274
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
1247
1275
|
|
|
1276
|
+
const control = {
|
|
1277
|
+
action,
|
|
1278
|
+
agentId,
|
|
1279
|
+
requestedBy: "dashboard",
|
|
1280
|
+
requestedAt: Date.now(),
|
|
1281
|
+
};
|
|
1248
1282
|
const msg = sendMessage({
|
|
1249
1283
|
from: "system",
|
|
1250
1284
|
to: orch.agentId,
|
|
@@ -1252,17 +1286,9 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
1252
1286
|
subject: action === "restart" ? "Restart agent" : "Shutdown agent",
|
|
1253
1287
|
body: agentId ? `${action} ${agentId}` : `${action} all managed agents`,
|
|
1254
1288
|
payload: {
|
|
1255
|
-
orchestratorControl:
|
|
1256
|
-
action,
|
|
1257
|
-
agentId,
|
|
1258
|
-
requestedBy: "dashboard",
|
|
1259
|
-
requestedAt: Date.now(),
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
meta: {
|
|
1263
|
-
delivery: "interrupt",
|
|
1264
|
-
priority: "urgent",
|
|
1289
|
+
orchestratorControl: control,
|
|
1265
1290
|
},
|
|
1291
|
+
meta: orchestratorControlMeta(control),
|
|
1266
1292
|
});
|
|
1267
1293
|
emitNewMessage(msg);
|
|
1268
1294
|
auditEvent({
|
package/src/types.ts
CHANGED
|
@@ -290,6 +290,22 @@ export interface ChannelBinding {
|
|
|
290
290
|
updatedAt: number;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
export interface ChannelTargetHealth {
|
|
294
|
+
status: "ok" | "warning" | "error";
|
|
295
|
+
detail: string;
|
|
296
|
+
target: ChannelRouteTarget;
|
|
297
|
+
matches: Array<{
|
|
298
|
+
id: string;
|
|
299
|
+
name: string;
|
|
300
|
+
status: AgentCard["status"];
|
|
301
|
+
ready: boolean;
|
|
302
|
+
lastSeen: number;
|
|
303
|
+
label?: string;
|
|
304
|
+
tags: string[];
|
|
305
|
+
capabilities: string[];
|
|
306
|
+
}>;
|
|
307
|
+
}
|
|
308
|
+
|
|
293
309
|
export interface ChannelSummary {
|
|
294
310
|
id: string;
|
|
295
311
|
name: string;
|
|
@@ -302,6 +318,7 @@ export interface ChannelSummary {
|
|
|
302
318
|
direction: ChannelDirection;
|
|
303
319
|
target?: string;
|
|
304
320
|
binding?: ChannelBinding;
|
|
321
|
+
targetHealth?: ChannelTargetHealth;
|
|
305
322
|
topicChannels: string[];
|
|
306
323
|
capabilities: string[];
|
|
307
324
|
tags: string[];
|
|
@@ -394,12 +411,25 @@ export interface Orchestrator {
|
|
|
394
411
|
providers: SpawnProvider[];
|
|
395
412
|
baseDir: string;
|
|
396
413
|
envKeys: string[]; // names only, never values
|
|
414
|
+
version?: string;
|
|
415
|
+
protocolVersion?: number;
|
|
416
|
+
gitSha?: string;
|
|
417
|
+
health?: OrchestratorHealth;
|
|
397
418
|
meta: Record<string, unknown>;
|
|
398
419
|
managedAgents: ManagedAgent[];
|
|
399
420
|
lastSeen: number;
|
|
400
421
|
createdAt: number;
|
|
401
422
|
}
|
|
402
423
|
|
|
424
|
+
export interface OrchestratorHealth {
|
|
425
|
+
status: "ok" | "warn" | "error";
|
|
426
|
+
restartRequired: boolean;
|
|
427
|
+
issues: Array<{
|
|
428
|
+
code: "missing-version" | "outdated" | "protocol-mismatch" | "restart-required";
|
|
429
|
+
detail: string;
|
|
430
|
+
}>;
|
|
431
|
+
}
|
|
432
|
+
|
|
403
433
|
export interface ManagedAgent {
|
|
404
434
|
agentId: string;
|
|
405
435
|
provider: SpawnProvider;
|
|
@@ -417,6 +447,9 @@ export interface RegisterOrchestratorInput {
|
|
|
417
447
|
providers: SpawnProvider[];
|
|
418
448
|
baseDir: string;
|
|
419
449
|
envKeys?: string[];
|
|
450
|
+
version?: string;
|
|
451
|
+
protocolVersion?: number;
|
|
452
|
+
gitSha?: string;
|
|
420
453
|
meta?: Record<string, unknown>;
|
|
421
454
|
}
|
|
422
455
|
|