@abelxiaoxing/cadence 1.0.13 → 1.1.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/README.md +29 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +393 -0
- package/src/run-store.ts +918 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ROLES } from "./contracts.ts";
|
|
6
|
+
|
|
7
|
+
export const ROUTE_POLICY_VERSION = 2 as const;
|
|
8
|
+
export const ROUTE_DIALECTS = [
|
|
9
|
+
"openai-completions",
|
|
10
|
+
"openai-responses",
|
|
11
|
+
"anthropic-messages",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type WorkerRole = (typeof ROLES)[number];
|
|
15
|
+
export type RouteDialect = (typeof ROUTE_DIALECTS)[number];
|
|
16
|
+
|
|
17
|
+
export interface RouteCapabilities {
|
|
18
|
+
roles: WorkerRole[];
|
|
19
|
+
dialects: RouteDialect[];
|
|
20
|
+
contextWindow: number;
|
|
21
|
+
maxTokens: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RouteBase {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: "inherited" | "custom";
|
|
27
|
+
capabilities: RouteCapabilities;
|
|
28
|
+
fingerprint: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InheritedRoutePolicy extends RouteBase {
|
|
32
|
+
kind: "inherited";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CustomRoutePolicy extends RouteBase {
|
|
36
|
+
kind: "custom";
|
|
37
|
+
url: string;
|
|
38
|
+
model: string;
|
|
39
|
+
dialect: RouteDialect;
|
|
40
|
+
apiKeyEnv?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type WorkerRoutePolicy = InheritedRoutePolicy | CustomRoutePolicy;
|
|
44
|
+
|
|
45
|
+
export interface RoutePolicy {
|
|
46
|
+
version: typeof ROUTE_POLICY_VERSION;
|
|
47
|
+
routes: Readonly<Record<string, WorkerRoutePolicy>>;
|
|
48
|
+
roles: Readonly<Record<WorkerRole, readonly string[]>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RoutePolicySource =
|
|
52
|
+
| { kind: "project" | "user"; path: string }
|
|
53
|
+
| { kind: "none" };
|
|
54
|
+
|
|
55
|
+
export type RoutePolicyDiagnosticCode =
|
|
56
|
+
| "policy-missing"
|
|
57
|
+
| "policy-unreadable"
|
|
58
|
+
| "policy-invalid-json"
|
|
59
|
+
| "policy-invalid"
|
|
60
|
+
| "route-invalid"
|
|
61
|
+
| "route-duplicate"
|
|
62
|
+
| "role-invalid"
|
|
63
|
+
| "route-reference-invalid"
|
|
64
|
+
| "unsupported-policy-version";
|
|
65
|
+
|
|
66
|
+
export interface RoutePolicyDiagnostic {
|
|
67
|
+
code: RoutePolicyDiagnosticCode;
|
|
68
|
+
field?: string;
|
|
69
|
+
routeId?: string;
|
|
70
|
+
role?: WorkerRole;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ParsedRoutePolicy =
|
|
74
|
+
| { ok: true; policy: RoutePolicy }
|
|
75
|
+
| { ok: false; diagnostics: RoutePolicyDiagnostic[] };
|
|
76
|
+
|
|
77
|
+
export type RoutePolicyResolution =
|
|
78
|
+
| {
|
|
79
|
+
ok: true;
|
|
80
|
+
source: Extract<RoutePolicySource, { kind: "project" | "user" }>;
|
|
81
|
+
policy: RoutePolicy;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
ok: false;
|
|
85
|
+
source: RoutePolicySource;
|
|
86
|
+
diagnostics: RoutePolicyDiagnostic[];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Internal fail-closed policy used while the selected external policy is
|
|
91
|
+
* missing or invalid. It deliberately contains no inherited-parent route:
|
|
92
|
+
* the broker therefore reports endpoint-unavailable without making a Worker
|
|
93
|
+
* request, while the local durable control plane remains available.
|
|
94
|
+
*
|
|
95
|
+
* This value is not accepted by parseRoutePolicy and is never persisted as a
|
|
96
|
+
* user policy. A corrected external policy replaces it before the next
|
|
97
|
+
* control operation.
|
|
98
|
+
*/
|
|
99
|
+
export function unavailableRoutePolicy(): RoutePolicy {
|
|
100
|
+
return {
|
|
101
|
+
version: ROUTE_POLICY_VERSION,
|
|
102
|
+
routes: Object.freeze({}),
|
|
103
|
+
roles: Object.freeze(
|
|
104
|
+
Object.fromEntries(
|
|
105
|
+
ROLES.map((role) => [role, Object.freeze([] as string[])]),
|
|
106
|
+
),
|
|
107
|
+
) as unknown as RoutePolicy["roles"],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ROUTE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/iu;
|
|
112
|
+
const MODEL_ID = /^[a-z0-9][a-z0-9._:/-]{0,255}$/iu;
|
|
113
|
+
const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/u;
|
|
114
|
+
const MAX_POLICY_BYTES = 1024 * 1024;
|
|
115
|
+
|
|
116
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
117
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasExactKeys(
|
|
121
|
+
value: Record<string, unknown>,
|
|
122
|
+
required: readonly string[],
|
|
123
|
+
optional: readonly string[] = [],
|
|
124
|
+
): boolean {
|
|
125
|
+
const allowed = new Set([...required, ...optional]);
|
|
126
|
+
return (
|
|
127
|
+
required.every((key) => Object.hasOwn(value, key)) &&
|
|
128
|
+
Object.keys(value).every((key) => allowed.has(key))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function uniqueStringArray(
|
|
133
|
+
value: unknown,
|
|
134
|
+
allowed?: readonly string[],
|
|
135
|
+
): value is string[] {
|
|
136
|
+
return (
|
|
137
|
+
Array.isArray(value) &&
|
|
138
|
+
value.length > 0 &&
|
|
139
|
+
value.length <= 64 &&
|
|
140
|
+
value.every(
|
|
141
|
+
(entry) =>
|
|
142
|
+
typeof entry === "string" &&
|
|
143
|
+
ROUTE_ID.test(entry) &&
|
|
144
|
+
(!allowed || allowed.includes(entry)),
|
|
145
|
+
) &&
|
|
146
|
+
new Set(value).size === value.length
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function positiveInteger(value: unknown): value is number {
|
|
151
|
+
return Number.isSafeInteger(value) && (value as number) > 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function httpUrl(value: unknown): value is string {
|
|
155
|
+
if (typeof value !== "string" || value.length > 2048) return false;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = new URL(value);
|
|
158
|
+
return (
|
|
159
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
160
|
+
parsed.username === "" &&
|
|
161
|
+
parsed.password === ""
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function canonicalJson(value: unknown): string {
|
|
169
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
170
|
+
if (isRecord(value)) {
|
|
171
|
+
return `{${Object.entries(value)
|
|
172
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
173
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
174
|
+
.join(",")}}`;
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify(value) ?? "null";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function routeFingerprint(route: Record<string, unknown>): string {
|
|
180
|
+
return createHash("sha256").update(canonicalJson(route)).digest("hex");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseCapabilities(value: unknown): RouteCapabilities | null {
|
|
184
|
+
if (
|
|
185
|
+
!isRecord(value) ||
|
|
186
|
+
!hasExactKeys(value, ["roles", "dialects", "contextWindow", "maxTokens"]) ||
|
|
187
|
+
!uniqueStringArray(value.roles, ROLES) ||
|
|
188
|
+
!uniqueStringArray(value.dialects, ROUTE_DIALECTS) ||
|
|
189
|
+
!positiveInteger(value.contextWindow) ||
|
|
190
|
+
!positiveInteger(value.maxTokens) ||
|
|
191
|
+
value.maxTokens > value.contextWindow
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
roles: [...value.roles] as WorkerRole[],
|
|
197
|
+
dialects: [...value.dialects] as RouteDialect[],
|
|
198
|
+
contextWindow: value.contextWindow,
|
|
199
|
+
maxTokens: value.maxTokens,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseRoute(
|
|
204
|
+
id: string,
|
|
205
|
+
value: unknown,
|
|
206
|
+
): WorkerRoutePolicy | RoutePolicyDiagnostic {
|
|
207
|
+
if (!ROUTE_ID.test(id) || !isRecord(value)) {
|
|
208
|
+
return {
|
|
209
|
+
code: "route-invalid",
|
|
210
|
+
routeId: ROUTE_ID.test(id) ? id : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const capabilities = parseCapabilities(value.capabilities);
|
|
214
|
+
if (!capabilities) {
|
|
215
|
+
return { code: "route-invalid", routeId: id, field: "capabilities" };
|
|
216
|
+
}
|
|
217
|
+
if (value.kind === "inherited") {
|
|
218
|
+
if (!hasExactKeys(value, ["kind", "capabilities"])) {
|
|
219
|
+
return { code: "route-invalid", routeId: id };
|
|
220
|
+
}
|
|
221
|
+
const raw = { kind: "inherited" as const, capabilities };
|
|
222
|
+
return {
|
|
223
|
+
id,
|
|
224
|
+
...raw,
|
|
225
|
+
fingerprint: routeFingerprint(raw),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (value.kind !== "custom") {
|
|
229
|
+
return { code: "route-invalid", routeId: id, field: "kind" };
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
!hasExactKeys(
|
|
233
|
+
value,
|
|
234
|
+
["kind", "url", "model", "dialect", "capabilities"],
|
|
235
|
+
["apiKeyEnv"],
|
|
236
|
+
) ||
|
|
237
|
+
!httpUrl(value.url) ||
|
|
238
|
+
typeof value.model !== "string" ||
|
|
239
|
+
!MODEL_ID.test(value.model) ||
|
|
240
|
+
!(ROUTE_DIALECTS as readonly unknown[]).includes(value.dialect) ||
|
|
241
|
+
!capabilities.dialects.includes(value.dialect as RouteDialect) ||
|
|
242
|
+
(value.apiKeyEnv !== undefined &&
|
|
243
|
+
(typeof value.apiKeyEnv !== "string" || !ENV_NAME.test(value.apiKeyEnv)))
|
|
244
|
+
) {
|
|
245
|
+
const field = !httpUrl(value.url)
|
|
246
|
+
? "url"
|
|
247
|
+
: typeof value.model !== "string" || !MODEL_ID.test(value.model)
|
|
248
|
+
? "model"
|
|
249
|
+
: !(ROUTE_DIALECTS as readonly unknown[]).includes(value.dialect)
|
|
250
|
+
? "dialect"
|
|
251
|
+
: value.apiKeyEnv !== undefined &&
|
|
252
|
+
(typeof value.apiKeyEnv !== "string" ||
|
|
253
|
+
!ENV_NAME.test(value.apiKeyEnv))
|
|
254
|
+
? "apiKeyEnv"
|
|
255
|
+
: undefined;
|
|
256
|
+
return { code: "route-invalid", routeId: id, ...(field ? { field } : {}) };
|
|
257
|
+
}
|
|
258
|
+
const raw = {
|
|
259
|
+
kind: "custom" as const,
|
|
260
|
+
url: value.url,
|
|
261
|
+
model: value.model,
|
|
262
|
+
dialect: value.dialect as RouteDialect,
|
|
263
|
+
...(typeof value.apiKeyEnv === "string"
|
|
264
|
+
? { apiKeyEnv: value.apiKeyEnv }
|
|
265
|
+
: {}),
|
|
266
|
+
capabilities,
|
|
267
|
+
};
|
|
268
|
+
return { id, ...raw, fingerprint: routeFingerprint(raw) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
|
|
272
|
+
if (
|
|
273
|
+
!isRecord(value) ||
|
|
274
|
+
!hasExactKeys(value, ["version", "routes", "roles"])
|
|
275
|
+
) {
|
|
276
|
+
return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
|
|
277
|
+
}
|
|
278
|
+
if (value.version !== ROUTE_POLICY_VERSION) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
diagnostics: [{ code: "unsupported-policy-version", field: "version" }],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (!isRecord(value.routes) || !isRecord(value.roles)) {
|
|
285
|
+
return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const diagnostics: RoutePolicyDiagnostic[] = [];
|
|
289
|
+
const routes: Record<string, WorkerRoutePolicy> = {};
|
|
290
|
+
for (const [id, candidate] of Object.entries(value.routes)) {
|
|
291
|
+
const parsed = parseRoute(id, candidate);
|
|
292
|
+
if ("code" in parsed) diagnostics.push(parsed);
|
|
293
|
+
else routes[id] = parsed;
|
|
294
|
+
}
|
|
295
|
+
if (Object.keys(routes).length !== Object.keys(value.routes).length) {
|
|
296
|
+
return { ok: false, diagnostics };
|
|
297
|
+
}
|
|
298
|
+
const fingerprints = new Map<string, string>();
|
|
299
|
+
for (const route of Object.values(routes)) {
|
|
300
|
+
const prior = fingerprints.get(route.fingerprint);
|
|
301
|
+
if (prior) {
|
|
302
|
+
diagnostics.push({
|
|
303
|
+
code: "route-duplicate",
|
|
304
|
+
routeId: route.id,
|
|
305
|
+
field: prior,
|
|
306
|
+
});
|
|
307
|
+
} else {
|
|
308
|
+
fingerprints.set(route.fingerprint, route.id);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const roleMap = value.roles as Record<string, unknown>;
|
|
312
|
+
if (
|
|
313
|
+
Object.keys(value.roles).length !== ROLES.length ||
|
|
314
|
+
!ROLES.every((role) => Object.hasOwn(roleMap, role))
|
|
315
|
+
) {
|
|
316
|
+
diagnostics.push({ code: "role-invalid" });
|
|
317
|
+
} else {
|
|
318
|
+
for (const role of ROLES) {
|
|
319
|
+
const routeIds = roleMap[role];
|
|
320
|
+
if (!uniqueStringArray(routeIds)) {
|
|
321
|
+
diagnostics.push({ code: "role-invalid", role });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
for (const routeId of routeIds) {
|
|
325
|
+
const route = routes[routeId];
|
|
326
|
+
if (!route?.capabilities.roles.includes(role)) {
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
code: "route-reference-invalid",
|
|
329
|
+
role,
|
|
330
|
+
routeId,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics };
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
policy: {
|
|
340
|
+
version: ROUTE_POLICY_VERSION,
|
|
341
|
+
routes: Object.freeze(structuredClone(routes)),
|
|
342
|
+
roles: Object.freeze(
|
|
343
|
+
Object.fromEntries(
|
|
344
|
+
ROLES.map((role) => [
|
|
345
|
+
role,
|
|
346
|
+
Object.freeze([...(roleMap[role] as string[])]),
|
|
347
|
+
]),
|
|
348
|
+
),
|
|
349
|
+
) as unknown as RoutePolicy["roles"],
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Parent-model route used by read-only Design and Diagnose packets. */
|
|
355
|
+
export function parentRoutePolicy(): RoutePolicy {
|
|
356
|
+
const parsed = parseRoutePolicy({
|
|
357
|
+
version: ROUTE_POLICY_VERSION,
|
|
358
|
+
routes: {
|
|
359
|
+
parent: {
|
|
360
|
+
kind: "inherited",
|
|
361
|
+
capabilities: {
|
|
362
|
+
roles: [...ROLES],
|
|
363
|
+
dialects: [...ROUTE_DIALECTS],
|
|
364
|
+
contextWindow: 1_000_000,
|
|
365
|
+
maxTokens: 500_000,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
roles: Object.fromEntries(ROLES.map((role) => [role, ["parent"]])),
|
|
370
|
+
});
|
|
371
|
+
if (!parsed.ok) throw new Error("parent route policy is invalid");
|
|
372
|
+
return parsed.policy;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function selectedSource(cwd: string, home: string): RoutePolicySource {
|
|
376
|
+
const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
|
|
377
|
+
if (existsSync(projectPath)) return { kind: "project", path: projectPath };
|
|
378
|
+
const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
|
|
379
|
+
if (existsSync(userPath)) return { kind: "user", path: userPath };
|
|
380
|
+
return { kind: "none" };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function loadRoutePolicy(
|
|
384
|
+
options: { cwd?: string; home?: string } = {},
|
|
385
|
+
): RoutePolicyResolution {
|
|
386
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
387
|
+
const home = path.resolve(options.home ?? homedir());
|
|
388
|
+
const source = selectedSource(cwd, home);
|
|
389
|
+
if (source.kind === "none") {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
source,
|
|
393
|
+
diagnostics: [{ code: "policy-missing" }],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
let bytes: string;
|
|
397
|
+
try {
|
|
398
|
+
const stat = lstatSync(source.path);
|
|
399
|
+
if (
|
|
400
|
+
!stat.isFile() ||
|
|
401
|
+
stat.isSymbolicLink() ||
|
|
402
|
+
stat.size > MAX_POLICY_BYTES
|
|
403
|
+
) {
|
|
404
|
+
throw new Error("unsafe policy file");
|
|
405
|
+
}
|
|
406
|
+
bytes = readFileSync(source.path, "utf8");
|
|
407
|
+
} catch {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
source,
|
|
411
|
+
diagnostics: [{ code: "policy-unreadable" }],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
let value: unknown;
|
|
415
|
+
try {
|
|
416
|
+
value = JSON.parse(bytes);
|
|
417
|
+
} catch {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
source,
|
|
421
|
+
diagnostics: [{ code: "policy-invalid-json" }],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const parsed = parseRoutePolicy(value);
|
|
425
|
+
return parsed.ok
|
|
426
|
+
? { ok: true, source, policy: parsed.policy }
|
|
427
|
+
: { ok: false, source, diagnostics: parsed.diagnostics };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function inspectRoutePolicy(
|
|
431
|
+
resolution: RoutePolicyResolution,
|
|
432
|
+
health: Readonly<Record<string, { state: string; retryAt?: number }>> = {},
|
|
433
|
+
): Record<string, unknown> {
|
|
434
|
+
if (!resolution.ok) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
source: { kind: resolution.source.kind },
|
|
438
|
+
diagnostics: structuredClone(resolution.diagnostics),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
source: { kind: resolution.source.kind },
|
|
444
|
+
routes: Object.values(resolution.policy.routes).map((route) => ({
|
|
445
|
+
id: route.id,
|
|
446
|
+
kind: route.kind,
|
|
447
|
+
fingerprint: route.fingerprint,
|
|
448
|
+
capabilities: structuredClone(route.capabilities),
|
|
449
|
+
health: health[route.id]?.state ?? "healthy",
|
|
450
|
+
...(health[route.id]?.retryAt === undefined
|
|
451
|
+
? {}
|
|
452
|
+
: { retryAt: health[route.id]?.retryAt }),
|
|
453
|
+
})),
|
|
454
|
+
roles: structuredClone(resolution.policy.roles),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export interface ResolvedCustomRoute {
|
|
459
|
+
url: string;
|
|
460
|
+
model: string;
|
|
461
|
+
dialect: RouteDialect;
|
|
462
|
+
contextWindow: number;
|
|
463
|
+
maxTokens: number;
|
|
464
|
+
apiKey?: string;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function resolveCustomRoute(
|
|
468
|
+
route: CustomRoutePolicy,
|
|
469
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
470
|
+
): ResolvedCustomRoute {
|
|
471
|
+
const apiKey = route.apiKeyEnv ? environment[route.apiKeyEnv] : undefined;
|
|
472
|
+
if (route.apiKeyEnv && !apiKey) {
|
|
473
|
+
throw new Error("route-credential-unavailable:apiKeyEnv");
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
url: route.url,
|
|
477
|
+
model: route.model,
|
|
478
|
+
dialect: route.dialect,
|
|
479
|
+
contextWindow: route.capabilities.contextWindow,
|
|
480
|
+
maxTokens: route.capabilities.maxTokens,
|
|
481
|
+
...(apiKey ? { apiKey } : {}),
|
|
482
|
+
};
|
|
483
|
+
}
|