agent-relay-server 0.96.0 → 0.98.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/docs/openapi.json +55 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-C4AeEHdz.js +4 -0
- package/public/assets/{MessageBubble-BO9eaLce.js.map → MessageBubble-C4AeEHdz.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BK2GoOql.js → PlanGraphPanel-cwPUGFXA.js} +2 -2
- package/public/assets/{PlanGraphPanel-BK2GoOql.js.map → PlanGraphPanel-cwPUGFXA.js.map} +1 -1
- package/public/assets/{activity-FPNsylmj.js → activity-BWGwnSg5.js} +2 -2
- package/public/assets/{activity-FPNsylmj.js.map → activity-BWGwnSg5.js.map} +1 -1
- package/public/assets/agent-profiles-C6N9cTJg.js +2 -0
- package/public/assets/agent-profiles-C6N9cTJg.js.map +1 -0
- package/public/assets/agents-FUBxhCEU.js +2 -0
- package/public/assets/{agents-DwXYbjeZ.js.map → agents-FUBxhCEU.js.map} +1 -1
- package/public/assets/{analytics-BiYHZABE.js → analytics-CUJbpoA7.js} +2 -2
- package/public/assets/{analytics-BiYHZABE.js.map → analytics-CUJbpoA7.js.map} +1 -1
- package/public/assets/{automation-deDG3zs5.js → automation-CMketzfB.js} +2 -2
- package/public/assets/{automation-deDG3zs5.js.map → automation-CMketzfB.js.map} +1 -1
- package/public/assets/{badge-BbfOskRI.js → badge-BLar6FjP.js} +2 -2
- package/public/assets/{badge-BbfOskRI.js.map → badge-BLar6FjP.js.map} +1 -1
- package/public/assets/branch-state-badge-CNko2_BL.js +2 -0
- package/public/assets/{branch-state-badge-ClWlvQks.js.map → branch-state-badge-CNko2_BL.js.map} +1 -1
- package/public/assets/{button-DWiw--lT.js → button-BrIB69qd.js} +2 -2
- package/public/assets/{button-DWiw--lT.js.map → button-BrIB69qd.js.map} +1 -1
- package/public/assets/card-lhnFnBoz.js +2 -0
- package/public/assets/{card-BeqvuYTT.js.map → card-lhnFnBoz.js.map} +1 -1
- package/public/assets/{channels-ln-_oQlI.js → channels-Tg8l4n5J.js} +2 -2
- package/public/assets/{channels-ln-_oQlI.js.map → channels-Tg8l4n5J.js.map} +1 -1
- package/public/assets/chat-BV_pnRhk.js +2 -0
- package/public/assets/{chat-DH8vR-mf.js.map → chat-BV_pnRhk.js.map} +1 -1
- package/public/assets/{connectors-CmxpixdH.js → connectors-B5wbjyUd.js} +2 -2
- package/public/assets/{connectors-CmxpixdH.js.map → connectors-B5wbjyUd.js.map} +1 -1
- package/public/assets/copy-button-Cd-LbSbg.js +2 -0
- package/public/assets/{copy-button-CIXy8ks6.js.map → copy-button-Cd-LbSbg.js.map} +1 -1
- package/public/assets/dist-30weaXto.js +2 -0
- package/public/assets/{dist-LFuRypZd.js.map → dist-30weaXto.js.map} +1 -1
- package/public/assets/{dist-CtuS_3Jl.js → dist-B5VvlkhI.js} +2 -2
- package/public/assets/{dist-CtuS_3Jl.js.map → dist-B5VvlkhI.js.map} +1 -1
- package/public/assets/dist-BEzpMqLy.js +2 -0
- package/public/assets/{dist-Q6OOS84U.js.map → dist-BEzpMqLy.js.map} +1 -1
- package/public/assets/{dist-CbxR2pzD.js → dist-BjOBkHHY.js} +2 -2
- package/public/assets/{dist-CbxR2pzD.js.map → dist-BjOBkHHY.js.map} +1 -1
- package/public/assets/{dist-zTw85jD8.js → dist-BnbGIUmi.js} +2 -2
- package/public/assets/{dist-zTw85jD8.js.map → dist-BnbGIUmi.js.map} +1 -1
- package/public/assets/{dist-BufB2Xy_.js → dist-Bp10H3oR.js} +2 -2
- package/public/assets/{dist-BufB2Xy_.js.map → dist-Bp10H3oR.js.map} +1 -1
- package/public/assets/{dist-Du_bcq-z.js → dist-CQF3Jiqg.js} +2 -2
- package/public/assets/{dist-Du_bcq-z.js.map → dist-CQF3Jiqg.js.map} +1 -1
- package/public/assets/dist-Cgy0FmP-.js +2 -0
- package/public/assets/{dist-CXkmW1Od.js.map → dist-Cgy0FmP-.js.map} +1 -1
- package/public/assets/{dist-KxiSP9h9.js → dist-DAl28ejX.js} +2 -2
- package/public/assets/{dist-KxiSP9h9.js.map → dist-DAl28ejX.js.map} +1 -1
- package/public/assets/{dist-eG_93I3z.js → dist-DDUGmmoi.js} +2 -2
- package/public/assets/{dist-eG_93I3z.js.map → dist-DDUGmmoi.js.map} +1 -1
- package/public/assets/{dist-BQXmO19g.js → dist-DZ97OosY.js} +2 -2
- package/public/assets/{dist-BQXmO19g.js.map → dist-DZ97OosY.js.map} +1 -1
- package/public/assets/{es2015-ibwCE_JR.js → es2015-B8Z9p8oQ.js} +2 -2
- package/public/assets/{es2015-ibwCE_JR.js.map → es2015-B8Z9p8oQ.js.map} +1 -1
- package/public/assets/{formatted-body-impl-l_m1i28m.js → formatted-body-impl-DzHQTBmn.js} +2 -2
- package/public/assets/{formatted-body-impl-l_m1i28m.js.map → formatted-body-impl-DzHQTBmn.js.map} +1 -1
- package/public/assets/index-CYLrIuJT.css +2 -0
- package/public/assets/index-CdEAo41I.js +23 -0
- package/public/assets/index-CdEAo41I.js.map +1 -0
- package/public/assets/{input-DUdtS3nG.js → input-B_fUHtLD.js} +2 -2
- package/public/assets/{input-DUdtS3nG.js.map → input-B_fUHtLD.js.map} +1 -1
- package/public/assets/{insights-DpNVkiVq.js → insights-C_pJNcFD.js} +2 -2
- package/public/assets/{insights-DpNVkiVq.js.map → insights-C_pJNcFD.js.map} +1 -1
- package/public/assets/{integrations-DscgKx2I.js → integrations-sYKtupOq.js} +2 -2
- package/public/assets/{integrations-DscgKx2I.js.map → integrations-sYKtupOq.js.map} +1 -1
- package/public/assets/{lib-Gp69YB9e.js → lib-ng6WZYjE.js} +2 -2
- package/public/assets/{lib-Gp69YB9e.js.map → lib-ng6WZYjE.js.map} +1 -1
- package/public/assets/{lucide-react-uFIMC7W7.js → lucide-react-BYZuAhQs.js} +2 -2
- package/public/assets/{lucide-react-uFIMC7W7.js.map → lucide-react-BYZuAhQs.js.map} +1 -1
- package/public/assets/maintenance-Ckz5BAor.js +2 -0
- package/public/assets/{maintenance-BhAxK-Fd.js.map → maintenance-Ckz5BAor.js.map} +1 -1
- package/public/assets/{managed-agents-Bg4aG1a9.js → managed-agents-BbZclnqP.js} +4 -4
- package/public/assets/{managed-agents-Bg4aG1a9.js.map → managed-agents-BbZclnqP.js.map} +1 -1
- package/public/assets/markdown-preview-impl-CtuHw6Mm.js +2 -0
- package/public/assets/{markdown-preview-impl-BJHjAo1H.js.map → markdown-preview-impl-CtuHw6Mm.js.map} +1 -1
- package/public/assets/memory-C5DK3rhW.js +2 -0
- package/public/assets/{memory-WwrerNC0.js.map → memory-C5DK3rhW.js.map} +1 -1
- package/public/assets/{messages-BY6mboBf.js → messages-CsjtiFEb.js} +2 -2
- package/public/assets/{messages-BY6mboBf.js.map → messages-CsjtiFEb.js.map} +1 -1
- package/public/assets/{orchestrators-ClNqQHnw.js → orchestrators-D9fMiBRy.js} +2 -2
- package/public/assets/{orchestrators-ClNqQHnw.js.map → orchestrators-D9fMiBRy.js.map} +1 -1
- package/public/assets/{overview-Bk91s8bA.js → overview-BLWoCnNQ.js} +2 -2
- package/public/assets/{overview-Bk91s8bA.js.map → overview-BLWoCnNQ.js.map} +1 -1
- package/public/assets/{pairs-IxkuPIfN.js → pairs-HnvpLz4C.js} +2 -2
- package/public/assets/{pairs-IxkuPIfN.js.map → pairs-HnvpLz4C.js.map} +1 -1
- package/public/assets/{projects-D7WJtj7Y.js → projects-BPI8z55X.js} +2 -2
- package/public/assets/{projects-D7WJtj7Y.js.map → projects-BPI8z55X.js.map} +1 -1
- package/public/assets/{react-dom-DF2cuNaf.js → react-dom-Dbhs84ug.js} +2 -2
- package/public/assets/{react-dom-DF2cuNaf.js.map → react-dom-Dbhs84ug.js.map} +1 -1
- package/public/assets/{registry-DUzVG_Oi.js → registry-BqAS3ADV.js} +2 -2
- package/public/assets/{registry-DUzVG_Oi.js.map → registry-BqAS3ADV.js.map} +1 -1
- package/public/assets/{security-D9ru8DHd.js → security-CWgtcQ2X.js} +2 -2
- package/public/assets/{security-D9ru8DHd.js.map → security-CWgtcQ2X.js.map} +1 -1
- package/public/assets/{select-Bu-gipoC.js → select-UdIYiKFs.js} +2 -2
- package/public/assets/{select-Bu-gipoC.js.map → select-UdIYiKFs.js.map} +1 -1
- package/public/assets/settings-ruDxPuza.js +6 -0
- package/public/assets/settings-ruDxPuza.js.map +1 -0
- package/public/assets/{store-DGM2s9L1.js → store-pKbf_WYd.js} +5 -5
- package/public/assets/{store-DGM2s9L1.js.map → store-pKbf_WYd.js.map} +1 -1
- package/public/assets/{tasks-C94dSpvI.js → tasks-DomQFwZP.js} +2 -2
- package/public/assets/{tasks-C94dSpvI.js.map → tasks-DomQFwZP.js.map} +1 -1
- package/public/assets/{teams-Cq7HWQe9.js → teams-CXNp0U3B.js} +2 -2
- package/public/assets/{teams-Cq7HWQe9.js.map → teams-CXNp0U3B.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BQr-VK32.js → terminal-viewer-impl-DiWBtzvZ.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BQr-VK32.js.map → terminal-viewer-impl-DiWBtzvZ.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-CeLM-PeW.js → use-keyboard-viewport-D8_NYDkh.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CeLM-PeW.js.map → use-keyboard-viewport-D8_NYDkh.js.map} +1 -1
- package/public/assets/user-avatar-DBs9Koqp.js +2 -0
- package/public/assets/{user-avatar-aCd8Fb5v.js.map → user-avatar-DBs9Koqp.js.map} +1 -1
- package/public/assets/{work-queue-BuyQhedx.js → work-queue-BD29QuLd.js} +2 -2
- package/public/assets/{work-queue-BuyQhedx.js.map → work-queue-BD29QuLd.js.map} +1 -1
- package/public/assets/workspaces-BolccWZy.js +3 -0
- package/public/assets/{workspaces-DXTrd2or.js.map → workspaces-BolccWZy.js.map} +1 -1
- package/public/index.html +21 -21
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapter.ts +9 -1
- package/src/agent-busy-health.ts +63 -62
- package/src/codex-wham-quota.ts +5 -76
- package/src/compaction-watch.ts +7 -6
- package/src/db/provider-quota-burn.ts +6 -18
- package/src/db/provider-quotas.ts +5 -6
- package/src/lifecycle-manager.ts +4 -2
- package/src/maintenance/jobs.ts +1 -1
- package/src/mcp/tools-spawn.ts +13 -2
- package/src/mcp-team-template-tools.ts +8 -1
- package/src/notification-types.ts +3 -1
- package/src/routes/agents-spawn.ts +4 -2
- package/src/routes/index.ts +3 -0
- package/src/routes/routing-policy.ts +25 -0
- package/src/routing-policy-push.ts +82 -0
- package/src/routing-policy-store.ts +148 -0
- package/src/routing-rule-resolver.ts +87 -0
- package/src/security.ts +6 -0
- package/src/services/spawn-agent.ts +46 -1
- package/src/spawn-targets.ts +44 -12
- package/public/assets/MessageBubble-BO9eaLce.js +0 -4
- package/public/assets/agent-profiles-lEZqu7Gm.js +0 -2
- package/public/assets/agent-profiles-lEZqu7Gm.js.map +0 -1
- package/public/assets/agents-DwXYbjeZ.js +0 -2
- package/public/assets/branch-state-badge-ClWlvQks.js +0 -2
- package/public/assets/card-BeqvuYTT.js +0 -2
- package/public/assets/chat-DH8vR-mf.js +0 -2
- package/public/assets/copy-button-CIXy8ks6.js +0 -2
- package/public/assets/dist-CXkmW1Od.js +0 -2
- package/public/assets/dist-LFuRypZd.js +0 -2
- package/public/assets/dist-Q6OOS84U.js +0 -2
- package/public/assets/index-BETon8ID.css +0 -2
- package/public/assets/index-D1GX2b-m.js +0 -23
- package/public/assets/index-D1GX2b-m.js.map +0 -1
- package/public/assets/maintenance-BhAxK-Fd.js +0 -2
- package/public/assets/markdown-preview-impl-BJHjAo1H.js +0 -2
- package/public/assets/memory-WwrerNC0.js +0 -2
- package/public/assets/settings-CoOySnqS.js +0 -6
- package/public/assets/settings-CoOySnqS.js.map +0 -1
- package/public/assets/user-avatar-aCd8Fb5v.js +0 -2
- package/public/assets/workspaces-DXTrd2or.js +0 -3
package/src/agent-busy-health.ts
CHANGED
|
@@ -5,14 +5,13 @@ import {
|
|
|
5
5
|
STUCK_BUSY_STARTUP_MS,
|
|
6
6
|
STUCK_BUSY_TRANSPORT_STALE_MS,
|
|
7
7
|
} from "./config";
|
|
8
|
-
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
8
|
+
import { isRecord, stringValue, type LivenessSignal } from "agent-relay-sdk";
|
|
9
9
|
import type { AgentCard } from "./types";
|
|
10
10
|
|
|
11
11
|
type BusyHealthKind = "provider-turn" | "startup" | "reconnect" | "shutdown";
|
|
12
12
|
|
|
13
13
|
interface ActiveWorkLike {
|
|
14
14
|
id?: unknown;
|
|
15
|
-
kind?: unknown;
|
|
16
15
|
label?: unknown;
|
|
17
16
|
startedAt?: unknown;
|
|
18
17
|
}
|
|
@@ -22,18 +21,19 @@ export interface AgentBusyHealth {
|
|
|
22
21
|
kind: BusyHealthKind;
|
|
23
22
|
thresholdMs: number;
|
|
24
23
|
activeWorkId?: string;
|
|
25
|
-
activeWorkKind?: string;
|
|
26
24
|
activeWorkLabel?: string;
|
|
27
25
|
activeWorkStartedAt: number;
|
|
28
26
|
activeWorkAgeMs: number;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/** Most recent of lastProviderEventAt and any fresh lifecycle/timeline transition (e.g. `compacting`). */
|
|
27
|
+
/** Most recent forward-progress timestamp from the contract LivenessSignal (provider tokens,
|
|
28
|
+
* a lifecycle/timeline transition, or live tool/subprocess work), floored at the work start. */
|
|
32
29
|
lastProgressAt: number;
|
|
33
|
-
/** Most recent transport
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
/** Most recent transport-liveness timestamp: LivenessSignal.transportLiveAt folded with the
|
|
31
|
+
* server-stamped agent.lastSeen as the floor. Always set. */
|
|
32
|
+
transportLiveAt: number;
|
|
33
|
+
/** Age of the most recent transport-liveness signal. */
|
|
36
34
|
transportStaleForMs: number;
|
|
35
|
+
/** True while the contract reports live tool/subprocess work (vetoes stuck-busy). */
|
|
36
|
+
hasLiveWork: boolean;
|
|
37
37
|
staleForMs: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -45,34 +45,35 @@ function activeWork(agent: AgentCard): ActiveWorkLike[] {
|
|
|
45
45
|
return Array.isArray(agent.meta?.activeWork) ? agent.meta.activeWork as ActiveWorkLike[] : [];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const ack = numberValue(transport.lastHeartbeatAckAt);
|
|
65
|
-
if (frame === undefined && ack === undefined) return undefined;
|
|
66
|
-
return Math.max(frame ?? 0, ack ?? 0);
|
|
48
|
+
// The work record that drives the busy AGE — the earliest-started one. Read only for the age
|
|
49
|
+
// threshold + display (id/label); the busy KIND comes from lifecycle markers (below) and the
|
|
50
|
+
// live-work question comes from LivenessSignal.hasLiveWork. We deliberately never enumerate
|
|
51
|
+
// `activeWork[].kind` here — that interpretation now lives behind the contract (#746).
|
|
52
|
+
function oldestWork(works: ActiveWorkLike[]): { work?: ActiveWorkLike; startedAt?: number } {
|
|
53
|
+
let best: ActiveWorkLike | undefined;
|
|
54
|
+
let bestAt: number | undefined;
|
|
55
|
+
for (const work of works) {
|
|
56
|
+
const at = numberValue(work.startedAt);
|
|
57
|
+
if (at === undefined) continue;
|
|
58
|
+
if (bestAt === undefined || at < bestAt) {
|
|
59
|
+
best = work;
|
|
60
|
+
bestAt = at;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { work: best ?? works[0], startedAt: bestAt };
|
|
67
64
|
}
|
|
68
65
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
// The ONLY runtime-liveness input the server reads: the normalized contract verdict the runner
|
|
67
|
+
// (per-provider adapter) ships on the card. Replaces the six raw `meta` reads (#713/#724) the
|
|
68
|
+
// stuck-busy detector used to re-derive. Returns undefined for cards from a pre-contract runner;
|
|
69
|
+
// the caller then degrades gracefully to the server-stamped lastSeen + work-start floors.
|
|
70
|
+
function readLiveness(agent: AgentCard): LivenessSignal | undefined {
|
|
71
|
+
const raw = agent.meta?.liveness;
|
|
72
|
+
if (!isRecord(raw)) return undefined;
|
|
73
|
+
const transportLiveAt = numberValue(raw.transportLiveAt);
|
|
74
|
+
const lastProgressAt = numberValue(raw.lastProgressAt);
|
|
75
|
+
if (transportLiveAt === undefined || lastProgressAt === undefined) return undefined;
|
|
76
|
+
return { transportLiveAt, lastProgressAt, hasLiveWork: raw.hasLiveWork === true };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function thresholdFor(kind: BusyHealthKind): number {
|
|
@@ -82,65 +83,65 @@ function thresholdFor(kind: BusyHealthKind): number {
|
|
|
82
83
|
return STUCK_BUSY_PROVIDER_TURN_MS;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
// Busy KIND classification — driven by provider-agnostic lifecycle markers (lifecycleAction /
|
|
87
|
+
// ready), NOT by provider runtime signals. The active-work set contributes only the start
|
|
88
|
+
// timestamp (age) and display id/label.
|
|
89
|
+
function classifyBusy(agent: AgentCard): { kind: BusyHealthKind; work?: ActiveWorkLike; startedAt: number } | null {
|
|
86
90
|
if (agent.status !== "busy") return null;
|
|
87
|
-
const
|
|
88
|
-
const providerTurn = works.find((item) => item.kind === "provider-turn");
|
|
89
|
-
if (providerTurn) {
|
|
90
|
-
return { kind: "provider-turn", work: providerTurn, startedAt: numberValue(providerTurn.startedAt) ?? agent.lastSeen };
|
|
91
|
-
}
|
|
92
|
-
|
|
91
|
+
const oldest = oldestWork(activeWork(agent));
|
|
93
92
|
const lifecycleAction = stringValue(agent.meta?.lifecycleAction) ?? "";
|
|
94
93
|
const lifecycleActionAt = numberValue(agent.meta?.lifecycleActionAt);
|
|
95
94
|
if (lifecycleAction.includes("shutdown") || lifecycleAction === "killing") {
|
|
96
|
-
return { kind: "shutdown", work:
|
|
95
|
+
return { kind: "shutdown", work: oldest.work, startedAt: lifecycleActionAt ?? oldest.startedAt ?? agent.lastSeen };
|
|
97
96
|
}
|
|
98
97
|
if (lifecycleAction.includes("reconnect") || lifecycleAction === "resuming") {
|
|
99
|
-
return { kind: "reconnect", work:
|
|
98
|
+
return { kind: "reconnect", work: oldest.work, startedAt: lifecycleActionAt ?? oldest.startedAt ?? agent.lastSeen };
|
|
100
99
|
}
|
|
101
100
|
if (!agent.ready) {
|
|
102
|
-
return { kind: "startup", work:
|
|
101
|
+
return { kind: "startup", work: oldest.work, startedAt: numberValue(agent.meta?.startedAt) ?? agent.createdAt };
|
|
103
102
|
}
|
|
104
|
-
return
|
|
103
|
+
return oldest.work ? { kind: "provider-turn", work: oldest.work, startedAt: oldest.startedAt ?? agent.lastSeen } : null;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
export function deriveAgentBusyHealth(agent: AgentCard, now: number = Date.now()): AgentBusyHealth | undefined {
|
|
108
|
-
const classified = classifyBusy(agent
|
|
107
|
+
const classified = classifyBusy(agent);
|
|
109
108
|
if (!classified) return undefined;
|
|
110
|
-
const works = activeWork(agent);
|
|
111
109
|
const thresholdMs = thresholdFor(classified.kind);
|
|
112
110
|
const activeWorkStartedAt = Math.max(0, classified.startedAt);
|
|
113
111
|
const activeWorkAgeMs = Math.max(0, now - activeWorkStartedAt);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
112
|
+
|
|
113
|
+
const liveness = readLiveness(agent);
|
|
114
|
+
// transportLiveAt: fold the server-stamped lastSeen as the FLOOR. The runner ships the max of
|
|
115
|
+
// its own frame/heartbeat-ack timestamps; the server can't expect the runner to know lastSeen,
|
|
116
|
+
// so a fresh heartbeat on EITHER side proves the link is alive. This is the #713/#724 veto:
|
|
117
|
+
// provider-token silence alone never trips stuck-busy while transport is demonstrably live.
|
|
118
|
+
const transportLiveAt = Math.max(liveness?.transportLiveAt ?? 0, agent.lastSeen);
|
|
119
|
+
const transportStaleForMs = Math.max(0, now - transportLiveAt);
|
|
120
|
+
// lastProgressAt floored at the work start so the no-progress age can't predate the turn.
|
|
121
|
+
const lastProgressAt = Math.max(liveness?.lastProgressAt ?? 0, activeWorkStartedAt);
|
|
118
122
|
const staleForMs = Math.max(0, now - lastProgressAt);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
// alive), so a genuine wedge whose runner has stopped reporting still trips.
|
|
123
|
-
const liveAt = transportLiveAt(agent);
|
|
124
|
-
const transportStaleForMs = liveAt === undefined ? Number.POSITIVE_INFINITY : Math.max(0, now - liveAt);
|
|
123
|
+
const hasLiveWork = liveness?.hasLiveWork ?? false;
|
|
124
|
+
|
|
125
|
+
// Threshold policy only — no provider knowledge, no raw-signal interpretation.
|
|
125
126
|
const stuck =
|
|
126
|
-
!
|
|
127
|
+
!hasLiveWork &&
|
|
127
128
|
activeWorkAgeMs >= thresholdMs &&
|
|
128
129
|
staleForMs >= thresholdMs &&
|
|
129
130
|
transportStaleForMs >= STUCK_BUSY_TRANSPORT_STALE_MS;
|
|
131
|
+
|
|
130
132
|
const work = classified.work;
|
|
131
133
|
return {
|
|
132
134
|
status: stuck ? "stuck-busy" : "ok",
|
|
133
135
|
kind: classified.kind,
|
|
134
136
|
thresholdMs,
|
|
135
137
|
...(stringValue(work?.id) ? { activeWorkId: stringValue(work?.id) } : {}),
|
|
136
|
-
...(stringValue(work?.kind) ? { activeWorkKind: stringValue(work?.kind) } : {}),
|
|
137
138
|
...(stringValue(work?.label) ? { activeWorkLabel: stringValue(work?.label) } : {}),
|
|
138
139
|
activeWorkStartedAt,
|
|
139
140
|
activeWorkAgeMs,
|
|
140
|
-
...(lastProviderEventAt !== undefined ? { lastProviderEventAt, lastProviderEventAgeMs: Math.max(0, now - lastProviderEventAt) } : {}),
|
|
141
141
|
lastProgressAt,
|
|
142
|
-
|
|
142
|
+
transportLiveAt,
|
|
143
143
|
transportStaleForMs,
|
|
144
|
+
hasLiveWork,
|
|
144
145
|
staleForMs,
|
|
145
146
|
};
|
|
146
147
|
}
|
package/src/codex-wham-quota.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { resolveStableCodexQuotaIdentity } from "agent-relay-sdk/provider-quota";
|
|
5
|
-
import { quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
|
|
4
|
+
import { codexWhamResetCredits, codexWhamUsageQuotaState, resolveStableCodexQuotaIdentity } from "agent-relay-sdk/provider-quota";
|
|
6
5
|
import { errMessage, isRecord, stringValue } from "agent-relay-sdk";
|
|
7
6
|
import { codexWhamQuotaEnabled } from "./config";
|
|
8
7
|
import { upsertProviderQuota } from "./db";
|
|
9
8
|
import { emitRelayEvent } from "./events";
|
|
10
9
|
import { getProviderQuotaConfig } from "./provider-quota-config-store";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
10
|
+
import type { ProviderQuotaError } from "./types";
|
|
11
|
+
|
|
12
|
+
// Re-export for callers that import the parsing functions from this module (e.g. tests).
|
|
13
|
+
export { codexWhamUsageQuotaState, codexWhamResetCredits } from "agent-relay-sdk/provider-quota";
|
|
13
14
|
|
|
14
15
|
const WHAM_API_BASE = "https://chatgpt.com/backend-api";
|
|
15
16
|
const WHAM_USAGE_PATH = "/wham/usage";
|
|
@@ -75,43 +76,6 @@ export async function collectCodexWhamQuota(options: {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export function codexWhamUsageQuotaState(payload: unknown, now = Date.now()): QuotaState | undefined {
|
|
79
|
-
if (!isRecord(payload) || !isRecord(payload.rate_limit)) return undefined;
|
|
80
|
-
const windows = [
|
|
81
|
-
codexWhamWindow("primary", payload.rate_limit.primary_window, now),
|
|
82
|
-
codexWhamWindow("secondary", payload.rate_limit.secondary_window, now),
|
|
83
|
-
].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window));
|
|
84
|
-
if (windows.length !== 2) return undefined;
|
|
85
|
-
const quota = quotaStateFromProbeMetrics({
|
|
86
|
-
agentId: "codex-wham-quota",
|
|
87
|
-
contextPercent: 0,
|
|
88
|
-
quotaWindows: windows,
|
|
89
|
-
source: "api",
|
|
90
|
-
confidence: "reported",
|
|
91
|
-
timestamp: now,
|
|
92
|
-
});
|
|
93
|
-
// quotaStateFromProbeMetrics always maps source to "probe"; override to "ingest" so the
|
|
94
|
-
// per-window source precedence in mergeQuotaWindows knows WHAM is authoritative over probe.
|
|
95
|
-
return quota ? { ...quota, source: "ingest" as const } : undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function codexWhamResetCredits(usagePayload: unknown, creditEventsPayload: unknown, now = Date.now()): CodexResetCredits | undefined {
|
|
99
|
-
if (!isRecord(usagePayload) || !isRecord(usagePayload.credits)) return undefined;
|
|
100
|
-
const balance = numberValue(usagePayload.credits.balance);
|
|
101
|
-
if (balance === undefined) return undefined;
|
|
102
|
-
const events = isRecord(creditEventsPayload) && Array.isArray(creditEventsPayload.data)
|
|
103
|
-
? creditEventsPayload.data.length
|
|
104
|
-
: undefined;
|
|
105
|
-
return {
|
|
106
|
-
balance,
|
|
107
|
-
...(typeof usagePayload.credits.has_credits === "boolean" ? { hasCredits: usagePayload.credits.has_credits } : {}),
|
|
108
|
-
...(typeof usagePayload.credits.unlimited === "boolean" ? { unlimited: usagePayload.credits.unlimited } : {}),
|
|
109
|
-
...(typeof usagePayload.credits.overage_limit_reached === "boolean" ? { overageLimitReached: usagePayload.credits.overage_limit_reached } : {}),
|
|
110
|
-
...(events !== undefined ? { creditUsageEvents: events } : {}),
|
|
111
|
-
updatedAt: now,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
79
|
function defaultCodexAuthPath(): string {
|
|
116
80
|
return join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
|
|
117
81
|
}
|
|
@@ -149,41 +113,6 @@ async function fetchWhamJson(path: string, auth: CodexWhamAuth, fetchImpl: typeo
|
|
|
149
113
|
return response.json();
|
|
150
114
|
}
|
|
151
115
|
|
|
152
|
-
function codexWhamWindow(name: "primary" | "secondary", value: unknown, now: number): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
|
|
153
|
-
if (!isRecord(value)) return undefined;
|
|
154
|
-
const used = numberValue(value.used_percent);
|
|
155
|
-
if (used === undefined) return undefined;
|
|
156
|
-
const resetsAt = resetAtMs(value.reset_at) ?? resetAfterSecondsToMs(value.reset_after_seconds, now);
|
|
157
|
-
return {
|
|
158
|
-
name,
|
|
159
|
-
utilization: used > 1 ? used / 100 : used,
|
|
160
|
-
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
161
|
-
unit: "%",
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function resetAtMs(value: unknown): number | undefined {
|
|
166
|
-
const number = numberValue(value);
|
|
167
|
-
if (number !== undefined) return number < 10_000_000_000 ? number * 1000 : number;
|
|
168
|
-
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
169
|
-
const parsed = Date.parse(value);
|
|
170
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resetAfterSecondsToMs(value: unknown, now: number): number | undefined {
|
|
174
|
-
const seconds = numberValue(value);
|
|
175
|
-
return seconds === undefined ? undefined : now + Math.max(0, seconds) * 1000;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function numberValue(value: unknown): number | undefined {
|
|
179
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
180
|
-
if (typeof value === "string" && value.trim()) {
|
|
181
|
-
const parsed = Number(value);
|
|
182
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
183
|
-
}
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
116
|
class CodexWhamHttpError extends Error {
|
|
188
117
|
constructor(readonly status: number) {
|
|
189
118
|
super(`Codex WHAM quota endpoint returned HTTP ${status}`);
|
package/src/compaction-watch.ts
CHANGED
|
@@ -39,9 +39,9 @@ function commandKind(type: string): WatchKind | undefined {
|
|
|
39
39
|
return undefined;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
return typeof
|
|
42
|
+
function agentHasTmuxSession(agent: AgentCard | null): boolean {
|
|
43
|
+
const session = agent?.meta?.tmuxSession;
|
|
44
|
+
return typeof session === "string" && session.trim().length > 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export class CompactionWatch {
|
|
@@ -70,13 +70,14 @@ export class CompactionWatch {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Arm a watch when a context command's keystrokes are confirmed delivered.
|
|
73
|
-
// Only
|
|
74
|
-
//
|
|
73
|
+
// Only agents with a tmux session go through the blind keystroke-inject path;
|
|
74
|
+
// API-based providers (Codex app-server) compact reliably without this guard.
|
|
75
|
+
// The adapter exposes the tmux-inject path implicitly via meta.tmuxSession (#743).
|
|
75
76
|
noteCommandSucceeded(type: string, commandId: string, target: string): void {
|
|
76
77
|
const kind = commandKind(type);
|
|
77
78
|
if (!kind) return;
|
|
78
79
|
const agent = getAgent(target);
|
|
79
|
-
if (
|
|
80
|
+
if (!agentHasTmuxSession(agent)) return;
|
|
80
81
|
const armedAt = this.now();
|
|
81
82
|
this.pending.set(target, { agentId: target, commandId, kind, armedAt, deadline: armedAt + this.stallMs });
|
|
82
83
|
}
|
|
@@ -4,24 +4,12 @@ const QUOTA_BURN_MIN_SAMPLE_SPAN_MS = 10 * 60_000;
|
|
|
4
4
|
|
|
5
5
|
type QuotaWindow = QuotaState["windows"][number];
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function quotaWindowRole(provider: string, windowName: string): ProviderQuotaBurnRole | undefined {
|
|
12
|
-
const normalizedProvider = normalizeProvider(provider);
|
|
13
|
-
if (normalizedProvider === "codex") {
|
|
14
|
-
if (windowName === "primary") return "5h";
|
|
15
|
-
if (windowName === "secondary") return "7d";
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
if (normalizedProvider === "claude") {
|
|
19
|
-
if (windowName === "five_hour") return "5h";
|
|
20
|
-
if (windowName === "seven_day" || windowName === "seven_day_sonnet" || windowName === "seven_day_opus") return "7d";
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
7
|
+
// Provider-agnostic: window role is expressed by name only. Each adapter uses the
|
|
8
|
+
// conventional names (five_hour/primary → 5h, seven_day*/secondary → 7d); the server
|
|
9
|
+
// never branches on provider identity to resolve a role (#742 / #725 §4.3).
|
|
10
|
+
function quotaWindowRole(windowName: string): ProviderQuotaBurnRole | undefined {
|
|
23
11
|
if (windowName === "five_hour" || windowName === "primary") return "5h";
|
|
24
|
-
if (windowName === "seven_day" || windowName === "secondary") return "7d";
|
|
12
|
+
if (windowName === "seven_day" || windowName === "seven_day_sonnet" || windowName === "seven_day_opus" || windowName === "secondary") return "7d";
|
|
25
13
|
return undefined;
|
|
26
14
|
}
|
|
27
15
|
|
|
@@ -58,7 +46,7 @@ function deriveProviderQuotaBurnWindow(input: {
|
|
|
58
46
|
now: number;
|
|
59
47
|
resetBoundaryToleranceMs: number;
|
|
60
48
|
}): ProviderQuotaBurnWindow | undefined {
|
|
61
|
-
const role = quotaWindowRole(input.
|
|
49
|
+
const role = quotaWindowRole(input.currentWindow.name);
|
|
62
50
|
if (!role) return undefined;
|
|
63
51
|
const low = lowConfidenceBurnWindow(role, input.currentWindow);
|
|
64
52
|
const resetAt = finiteResetBoundary(input.currentWindow);
|
|
@@ -370,12 +370,11 @@ function mergeQuotaWindows(existing: StoredQuotaState | undefined, incoming: Quo
|
|
|
370
370
|
if (!incomingWindow) {
|
|
371
371
|
mergedTimes.set(key, existingUpdatedAt); mergedSources.set(key, existingSource); return window;
|
|
372
372
|
}
|
|
373
|
-
// Per-window source precedence
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// must NOT block authoritative data correcting a stale probe that pinned the window high.
|
|
373
|
+
// Per-window source precedence: #712/#716 are handled by the symmetric three-way split below.
|
|
374
|
+
// Higher existing → keep (no regression check needed; lower-priority input simply loses).
|
|
375
|
+
// Higher incoming → accept (no regression check needed; higher-priority source is authoritative).
|
|
376
|
+
// Same priority → apply the regression guard (rejects improbable utilization drops from noisy
|
|
377
|
+
// same-quality sources, e.g. a wedged probe self-report; #725 §4.3).
|
|
379
378
|
const existingPriority = quotaSourcePriority(existingSource);
|
|
380
379
|
const incomingPriority = quotaSourcePriority(incoming.source);
|
|
381
380
|
if (existingPriority > incomingPriority) {
|
package/src/lifecycle-manager.ts
CHANGED
|
@@ -622,8 +622,10 @@ export function __resetLifecycleManager(): void {
|
|
|
622
622
|
singleton = null;
|
|
623
623
|
}
|
|
624
624
|
|
|
625
|
-
function managedModelUnavailableMessage(
|
|
626
|
-
|
|
625
|
+
function managedModelUnavailableMessage(_policy: SpawnPolicy, error: string, exited?: ManagedSessionExitDiagnostics): string | null {
|
|
626
|
+
// extractClaudeModelUnavailableMessage returns null for text that doesn't match
|
|
627
|
+
// the Claude-specific error pattern, so this is safe to call unconditionally —
|
|
628
|
+
// no provider=== branch needed here; the parser is the gate (#743).
|
|
627
629
|
return extractClaudeModelUnavailableMessage([
|
|
628
630
|
error,
|
|
629
631
|
exited?.lastError,
|
package/src/maintenance/jobs.ts
CHANGED
|
@@ -83,7 +83,7 @@ function isManagedBusyWatchCandidate(agent: ReturnType<typeof listAgents>[number
|
|
|
83
83
|
|
|
84
84
|
function stuckBusyBody(agentId: string, health: NonNullable<ReturnType<typeof deriveAgentBusyHealth>>): string {
|
|
85
85
|
const staleMinutes = Math.max(1, Math.floor(health.staleForMs / 60_000));
|
|
86
|
-
const work = health.activeWorkLabel || health.
|
|
86
|
+
const work = health.activeWorkLabel || health.activeWorkId || health.kind;
|
|
87
87
|
return `Agent \`${agentId}\` appears stuck busy in ${work} for ${staleMinutes} min with no provider progress. Use interrupt first if available; restart or shutdown if the turn is wedged.`;
|
|
88
88
|
}
|
|
89
89
|
|
package/src/mcp/tools-spawn.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnAgent, type SpawnAgentInput } from "../services/spawn-agent";
|
|
1
|
+
import { spawnAgent, summarizeSpawnCommandForCaller, type SpawnAgentInput } from "../services/spawn-agent";
|
|
2
2
|
import { authContextFromMcp } from "../services/auth-context";
|
|
3
3
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
|
|
4
4
|
import { buildSpawnTargets } from "../spawn-targets";
|
|
@@ -162,7 +162,18 @@ export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string,
|
|
|
162
162
|
resumeWorkspace: parseResumeWorkspace(args.resumeWorkspace),
|
|
163
163
|
};
|
|
164
164
|
const result = await spawnAgent(input, authContextFromMcp(auth));
|
|
165
|
-
return
|
|
165
|
+
// #734 — return the lean shape callers need (spawnRequestId + agentId for idempotency/shutdown)
|
|
166
|
+
// plus a sanitized command summary. The full resolved provisioning already went to the
|
|
167
|
+
// orchestrator on the dispatched command; echoing it here blows the MCP result token cap.
|
|
168
|
+
return {
|
|
169
|
+
ok: result.ok,
|
|
170
|
+
spawnRequestId: result.spawnRequestId,
|
|
171
|
+
orchestratorId: result.orchestratorId,
|
|
172
|
+
provider: result.provider,
|
|
173
|
+
agentId: result.agentId,
|
|
174
|
+
registered: result.registered,
|
|
175
|
+
command: summarizeSpawnCommandForCaller(result.command),
|
|
176
|
+
};
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
export function relaySpawnTargets(auth: McpAuthContext): Record<string, unknown> {
|
|
@@ -2,6 +2,7 @@ import { APPROVAL_MODES, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES } from "a
|
|
|
2
2
|
import { McpAuthError } from "./mcp-errors";
|
|
3
3
|
import { authContextFromMcp } from "./services/auth-context";
|
|
4
4
|
import { deleteTeamTemplate, getTeamTemplate, instantiateTeamTemplate, listTeamTemplates, saveTeamTemplate } from "./services/team-templates";
|
|
5
|
+
import { summarizeSpawnCommandForCaller } from "./services/spawn-agent";
|
|
5
6
|
import { ValidationError } from "./db";
|
|
6
7
|
|
|
7
8
|
const COMPOSITION_ENTRY_SCHEMA = {
|
|
@@ -123,7 +124,13 @@ export async function relayTeamTemplateTool(auth: McpAuthLike, name: string, arg
|
|
|
123
124
|
}
|
|
124
125
|
if (name === "relay_team_spawn_from_template") {
|
|
125
126
|
requireScope(auth, "teams:write");
|
|
126
|
-
|
|
127
|
+
const instantiated = await instantiateTeamTemplate({ ...args, slug: slugArg(args) }, authContextFromMcp(auth));
|
|
128
|
+
// #734 — each spawned result echoes its full resolved agentProfile; strip the heavy inlined
|
|
129
|
+
// provisioning file contents (multiplied by N spawned agents here) before returning to the caller.
|
|
130
|
+
return {
|
|
131
|
+
...instantiated,
|
|
132
|
+
spawned: instantiated.spawned.map((s) => ({ ...s, command: summarizeSpawnCommandForCaller(s.command) })),
|
|
133
|
+
} as unknown as Record<string, unknown>;
|
|
127
134
|
}
|
|
128
135
|
throw new ValidationError(`unknown tool: ${name}`);
|
|
129
136
|
}
|
|
@@ -25,7 +25,8 @@ export type NotificationType =
|
|
|
25
25
|
| "workspace.stranded_escalation"
|
|
26
26
|
| "workspace.auto_abandoned"
|
|
27
27
|
| "workspace.review_request"
|
|
28
|
-
| "workspace.review_changes_requested"
|
|
28
|
+
| "workspace.review_changes_requested"
|
|
29
|
+
| "routing.policy.changed";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The entities a notification is ABOUT: routing inputs (#442) and explainability subjects (#446).
|
|
@@ -94,6 +95,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
94
95
|
"workspace.auto_abandoned": null,
|
|
95
96
|
"workspace.review_request": null,
|
|
96
97
|
"workspace.review_changes_requested": null,
|
|
98
|
+
"routing.policy.changed": 15 * MINUTES,
|
|
97
99
|
};
|
|
98
100
|
|
|
99
101
|
export const NOTIFICATION_TYPES = Object.keys(DEFAULT_TTL_MS) as NotificationType[];
|
|
@@ -5,7 +5,7 @@ import { VALID_AGENT_STATUSES, error, json, parseBody, type Handler } from "./_s
|
|
|
5
5
|
import { ValidationError, listAgents } from "../db";
|
|
6
6
|
import { cleanMeta, cleanNullableString, cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
7
7
|
import { registerAgent } from "../services/register-agent";
|
|
8
|
-
import { spawnAgent, type SpawnAgentInput } from "../services/spawn-agent";
|
|
8
|
+
import { spawnAgent, summarizeSpawnCommandForCaller, type SpawnAgentInput } from "../services/spawn-agent";
|
|
9
9
|
import { parseProjectInstructions } from "../project-instructions";
|
|
10
10
|
import { parseProjectMcp } from "../project-mcp";
|
|
11
11
|
import { parseProjectSettings } from "../project-settings";
|
|
@@ -153,7 +153,9 @@ export const postAgentSpawn: Handler = async (req) => {
|
|
|
153
153
|
waitForRegistrationMs: 0,
|
|
154
154
|
};
|
|
155
155
|
const result = await spawnAgent(input, authContextFromRequest(req));
|
|
156
|
-
|
|
156
|
+
// #734 — the dispatched command carries the full resolved agentProfile to the orchestrator;
|
|
157
|
+
// strip the heavy inlined provisioning file contents from the echoed-back summary.
|
|
158
|
+
return json({ ok: true, orchestratorId: result.orchestratorId, provider: result.provider, command: summarizeSpawnCommandForCaller(result.command) }, 202);
|
|
157
159
|
} catch (e) {
|
|
158
160
|
if (e instanceof McpAuthError) return error(e.message, 403);
|
|
159
161
|
if (e instanceof McpNotFoundError) return error(e.message, 404);
|
package/src/routes/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomati
|
|
|
8
8
|
import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
|
|
9
9
|
import { getProviderQuotasRoute, postProviderQuotaLeaseAcquireRoute, postProviderQuotaLeaseReleaseRoute, postProviderQuotaPublisherLeaseAcquireRoute, postProviderQuotaPublisherLeaseReleaseRoute, postProviderQuotaRoute } from "./provider-quotas";
|
|
10
10
|
import { getProviderQuotaConfigRoute, getProviderQuotaConfigsRoute, putProviderQuotaConfigRoute } from "./provider-quota-config";
|
|
11
|
+
import { getRoutingPolicyRoute, putRoutingPolicyRoute } from "./routing-policy";
|
|
11
12
|
import { deleteProvisioningCapabilityRoute, getProvisioningCapabilitiesRoute, getProvisioningCapabilityRoute, patchProvisioningCapabilityRoute, patchProvisioningVariantRoute, postProvisioningCapabilityRoute, postProvisioningVariantImportRoute, postProvisioningVariantMcpRoute, postProvisioningVariantUpdateRoute, postProvisioningVariantValidateRoute } from "./provisioning-registry";
|
|
12
13
|
import { getTeamsConfigRoute, putTeamsConfigRoute } from "./teams-config";
|
|
13
14
|
import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
|
|
@@ -281,6 +282,8 @@ const routes: Route[] = [
|
|
|
281
282
|
route("GET", "/api/providers/quota-config", getProviderQuotaConfigsRoute),
|
|
282
283
|
route("GET", "/api/providers/:provider/quota-config", getProviderQuotaConfigRoute),
|
|
283
284
|
route("PUT", "/api/providers/:provider/quota-config", putProviderQuotaConfigRoute),
|
|
285
|
+
route("GET", "/api/routing-policy", getRoutingPolicyRoute),
|
|
286
|
+
route("PUT", "/api/routing-policy", putRoutingPolicyRoute),
|
|
284
287
|
route("PUT", "/api/providers/:provider/models/:alias", putProviderModelRoute),
|
|
285
288
|
route("PATCH", "/api/providers/:provider/models/:alias", patchProviderModelRoute),
|
|
286
289
|
route("DELETE", "/api/providers/:provider/models/:alias", deleteProviderModelRoute),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Routing-policy routes (#728). GET/PUT for the relay-owned routing-policy document.
|
|
2
|
+
// GET: agent:read (coordinators read the policy to apply rules).
|
|
3
|
+
// PUT: settings:write:routing (admin writes).
|
|
4
|
+
import { ValidationError } from "../db";
|
|
5
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
6
|
+
import { getRoutingPolicy, setRoutingPolicy } from "../routing-policy-store";
|
|
7
|
+
import { isRecord } from "agent-relay-sdk";
|
|
8
|
+
import { cleanString } from "../validation";
|
|
9
|
+
|
|
10
|
+
export const getRoutingPolicyRoute: Handler = () => {
|
|
11
|
+
return json(getRoutingPolicy());
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const putRoutingPolicyRoute: Handler = async (req) => {
|
|
15
|
+
const parsed = await parseBody<unknown>(req);
|
|
16
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
17
|
+
try {
|
|
18
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
19
|
+
const policy = setRoutingPolicy(parsed.body, { updatedBy });
|
|
20
|
+
return json(policy);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
};
|