@abelxiaoxing/cadence 1.8.3 → 2.0.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.
Files changed (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. package/src/workspace-store.ts +0 -634
@@ -1,545 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { lstatSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import path from "node:path";
5
- import { canonicalJson } from "./canonical.ts";
6
- import { ROLES } from "./contracts.ts";
7
-
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
- export interface ParentModelCapabilities {
25
- contextWindow: number;
26
- maxTokens: number;
27
- }
28
-
29
- interface RouteBase {
30
- id: string;
31
- kind: "inherited" | "custom";
32
- capabilities: RouteCapabilities;
33
- fingerprint: string;
34
- }
35
-
36
- export interface InheritedRoutePolicy extends RouteBase {
37
- kind: "inherited";
38
- }
39
-
40
- export interface CustomRoutePolicy extends RouteBase {
41
- kind: "custom";
42
- url: string;
43
- model: string;
44
- dialect: RouteDialect;
45
- apiKeyEnv?: string;
46
- }
47
-
48
- export type WorkerRoutePolicy = InheritedRoutePolicy | CustomRoutePolicy;
49
-
50
- export interface RoutePolicy {
51
- routes: Readonly<Record<string, WorkerRoutePolicy>>;
52
- roles: Readonly<Record<WorkerRole, readonly string[]>>;
53
- }
54
-
55
- export type RoutePolicySource =
56
- | { kind: "project" | "user"; path: string }
57
- | { kind: "default" };
58
-
59
- export type RoutePolicyDiagnosticCode =
60
- | "policy-unreadable"
61
- | "policy-invalid-json"
62
- | "policy-invalid"
63
- | "policy-version-unsupported"
64
- | "route-invalid"
65
- | "route-duplicate"
66
- | "role-invalid"
67
- | "route-reference-invalid";
68
-
69
- export interface RoutePolicyDiagnostic {
70
- code: RoutePolicyDiagnosticCode;
71
- field?: string;
72
- routeId?: string;
73
- role?: WorkerRole;
74
- }
75
-
76
- export type ParsedRoutePolicy =
77
- | { ok: true; policy: RoutePolicy }
78
- | { ok: false; diagnostics: RoutePolicyDiagnostic[] };
79
-
80
- export type RoutePolicyResolution =
81
- | {
82
- ok: true;
83
- source: RoutePolicySource;
84
- policy: RoutePolicy;
85
- }
86
- | {
87
- ok: false;
88
- source: RoutePolicySource;
89
- diagnostics: RoutePolicyDiagnostic[];
90
- };
91
-
92
- /**
93
- * Internal fail-closed policy used while a selected explicit policy is
94
- * invalid. It deliberately contains no inherited-parent route:
95
- * the broker therefore reports endpoint-unavailable without making a Worker
96
- * request, while the local durable control plane remains available.
97
- *
98
- * This value is not accepted by parseRoutePolicy and is never persisted as a
99
- * user policy. A corrected external policy replaces it before the next
100
- * control operation.
101
- */
102
- export function unavailableRoutePolicy(): RoutePolicy {
103
- return {
104
- routes: Object.freeze({}),
105
- roles: Object.freeze(
106
- Object.fromEntries(
107
- ROLES.map((role) => [role, Object.freeze([] as string[])]),
108
- ),
109
- ) as unknown as RoutePolicy["roles"],
110
- };
111
- }
112
-
113
- const ROUTE_ID = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/iu;
114
- const MODEL_ID = /^[a-z0-9][a-z0-9._:/-]{0,255}$/iu;
115
- const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/u;
116
- const MAX_POLICY_BYTES = 1024 * 1024;
117
-
118
- function isRecord(value: unknown): value is Record<string, unknown> {
119
- return value !== null && typeof value === "object" && !Array.isArray(value);
120
- }
121
-
122
- function hasExactKeys(
123
- value: Record<string, unknown>,
124
- required: readonly string[],
125
- optional: readonly string[] = [],
126
- ): boolean {
127
- const allowed = new Set([...required, ...optional]);
128
- return (
129
- required.every((key) => Object.hasOwn(value, key)) &&
130
- Object.keys(value).every((key) => allowed.has(key))
131
- );
132
- }
133
-
134
- function uniqueStringArray(
135
- value: unknown,
136
- allowed?: readonly string[],
137
- ): value is string[] {
138
- return (
139
- Array.isArray(value) &&
140
- value.length > 0 &&
141
- value.length <= 64 &&
142
- value.every(
143
- (entry) =>
144
- typeof entry === "string" &&
145
- ROUTE_ID.test(entry) &&
146
- (!allowed || allowed.includes(entry)),
147
- ) &&
148
- new Set(value).size === value.length
149
- );
150
- }
151
-
152
- function positiveInteger(value: unknown): value is number {
153
- return Number.isSafeInteger(value) && (value as number) > 0;
154
- }
155
-
156
- function httpUrl(value: unknown): value is string {
157
- if (typeof value !== "string" || value.length > 2048) return false;
158
- try {
159
- const parsed = new URL(value);
160
- return (
161
- (parsed.protocol === "http:" || parsed.protocol === "https:") &&
162
- parsed.username === "" &&
163
- parsed.password === ""
164
- );
165
- } catch {
166
- return false;
167
- }
168
- }
169
-
170
- function routeFingerprint(route: Record<string, unknown>): string {
171
- return createHash("sha256").update(canonicalJson(route)).digest("hex");
172
- }
173
-
174
- function parseCapabilities(value: unknown): RouteCapabilities | null {
175
- if (
176
- !isRecord(value) ||
177
- !hasExactKeys(value, ["roles", "dialects", "contextWindow", "maxTokens"]) ||
178
- !uniqueStringArray(value.roles, ROLES) ||
179
- !uniqueStringArray(value.dialects, ROUTE_DIALECTS) ||
180
- !positiveInteger(value.contextWindow) ||
181
- !positiveInteger(value.maxTokens) ||
182
- value.maxTokens > value.contextWindow
183
- ) {
184
- return null;
185
- }
186
- return {
187
- roles: [...value.roles] as WorkerRole[],
188
- dialects: [...value.dialects] as RouteDialect[],
189
- contextWindow: value.contextWindow,
190
- maxTokens: value.maxTokens,
191
- };
192
- }
193
-
194
- function parseRoute(
195
- id: string,
196
- value: unknown,
197
- ): WorkerRoutePolicy | RoutePolicyDiagnostic {
198
- if (!ROUTE_ID.test(id) || !isRecord(value)) {
199
- return {
200
- code: "route-invalid",
201
- routeId: ROUTE_ID.test(id) ? id : undefined,
202
- };
203
- }
204
- const capabilities = parseCapabilities(value.capabilities);
205
- if (!capabilities) {
206
- return { code: "route-invalid", routeId: id, field: "capabilities" };
207
- }
208
- if (value.kind === "inherited") {
209
- if (!hasExactKeys(value, ["kind", "capabilities"])) {
210
- return { code: "route-invalid", routeId: id };
211
- }
212
- const raw = { kind: "inherited" as const, capabilities };
213
- return {
214
- id,
215
- ...raw,
216
- fingerprint: routeFingerprint(raw),
217
- };
218
- }
219
- if (value.kind !== "custom") {
220
- return { code: "route-invalid", routeId: id, field: "kind" };
221
- }
222
- if (
223
- !hasExactKeys(
224
- value,
225
- ["kind", "url", "model", "dialect", "capabilities"],
226
- ["apiKeyEnv"],
227
- ) ||
228
- !httpUrl(value.url) ||
229
- typeof value.model !== "string" ||
230
- !MODEL_ID.test(value.model) ||
231
- !(ROUTE_DIALECTS as readonly unknown[]).includes(value.dialect) ||
232
- !capabilities.dialects.includes(value.dialect as RouteDialect) ||
233
- (value.apiKeyEnv !== undefined &&
234
- (typeof value.apiKeyEnv !== "string" || !ENV_NAME.test(value.apiKeyEnv)))
235
- ) {
236
- const field = !httpUrl(value.url)
237
- ? "url"
238
- : typeof value.model !== "string" || !MODEL_ID.test(value.model)
239
- ? "model"
240
- : !(ROUTE_DIALECTS as readonly unknown[]).includes(value.dialect)
241
- ? "dialect"
242
- : value.apiKeyEnv !== undefined &&
243
- (typeof value.apiKeyEnv !== "string" ||
244
- !ENV_NAME.test(value.apiKeyEnv))
245
- ? "apiKeyEnv"
246
- : undefined;
247
- return { code: "route-invalid", routeId: id, ...(field ? { field } : {}) };
248
- }
249
- const raw = {
250
- kind: "custom" as const,
251
- url: value.url,
252
- model: value.model,
253
- dialect: value.dialect as RouteDialect,
254
- ...(typeof value.apiKeyEnv === "string"
255
- ? { apiKeyEnv: value.apiKeyEnv }
256
- : {}),
257
- capabilities,
258
- };
259
- return { id, ...raw, fingerprint: routeFingerprint(raw) };
260
- }
261
-
262
- export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
263
- if (!isRecord(value)) {
264
- return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
265
- }
266
- const legacyVersion = Object.hasOwn(value, "version")
267
- ? value.version
268
- : undefined;
269
- if (legacyVersion !== undefined && legacyVersion !== 2) {
270
- return {
271
- ok: false,
272
- diagnostics: [{ code: "policy-version-unsupported", field: "version" }],
273
- };
274
- }
275
- if (!hasExactKeys(value, ["routes", "roles"], ["version"])) {
276
- return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
277
- }
278
- if (!isRecord(value.routes) || !isRecord(value.roles)) {
279
- return { ok: false, diagnostics: [{ code: "policy-invalid" }] };
280
- }
281
-
282
- const diagnostics: RoutePolicyDiagnostic[] = [];
283
- const routes: Record<string, WorkerRoutePolicy> = {};
284
- for (const [id, candidate] of Object.entries(value.routes)) {
285
- const parsed = parseRoute(id, candidate);
286
- if ("code" in parsed) diagnostics.push(parsed);
287
- else routes[id] = parsed;
288
- }
289
- if (Object.keys(routes).length !== Object.keys(value.routes).length) {
290
- return { ok: false, diagnostics };
291
- }
292
- const fingerprints = new Map<string, string>();
293
- for (const route of Object.values(routes)) {
294
- const prior = fingerprints.get(route.fingerprint);
295
- if (prior) {
296
- diagnostics.push({
297
- code: "route-duplicate",
298
- routeId: route.id,
299
- field: prior,
300
- });
301
- } else {
302
- fingerprints.set(route.fingerprint, route.id);
303
- }
304
- }
305
- const roleMap = value.roles as Record<string, unknown>;
306
- if (
307
- Object.keys(value.roles).length !== ROLES.length ||
308
- !ROLES.every((role) => Object.hasOwn(roleMap, role))
309
- ) {
310
- diagnostics.push({ code: "role-invalid" });
311
- } else {
312
- for (const role of ROLES) {
313
- const routeIds = roleMap[role];
314
- if (!uniqueStringArray(routeIds)) {
315
- diagnostics.push({ code: "role-invalid", role });
316
- continue;
317
- }
318
- for (const routeId of routeIds) {
319
- const route = routes[routeId];
320
- if (!route?.capabilities.roles.includes(role)) {
321
- diagnostics.push({
322
- code: "route-reference-invalid",
323
- role,
324
- routeId,
325
- });
326
- }
327
- }
328
- }
329
- }
330
- if (diagnostics.length > 0) return { ok: false, diagnostics };
331
- return {
332
- ok: true,
333
- policy: {
334
- routes: Object.freeze(structuredClone(routes)),
335
- roles: Object.freeze(
336
- Object.fromEntries(
337
- ROLES.map((role) => [
338
- role,
339
- Object.freeze([...(roleMap[role] as string[])]),
340
- ]),
341
- ),
342
- ) as unknown as RoutePolicy["roles"],
343
- },
344
- };
345
- }
346
-
347
- function constrainInheritedRoutes(
348
- policy: RoutePolicy,
349
- parentModel?: ParentModelCapabilities,
350
- ): RoutePolicy {
351
- if (!parentModel) return policy;
352
- const routes = Object.fromEntries(
353
- Object.entries(policy.routes).map(([routeId, route]) => {
354
- if (route.kind !== "inherited") return [routeId, structuredClone(route)];
355
- const capabilities = {
356
- ...structuredClone(route.capabilities),
357
- contextWindow: Math.min(
358
- route.capabilities.contextWindow,
359
- parentModel.contextWindow,
360
- ),
361
- maxTokens: Math.min(
362
- route.capabilities.maxTokens,
363
- parentModel.maxTokens,
364
- route.capabilities.contextWindow,
365
- parentModel.contextWindow,
366
- ),
367
- };
368
- const raw = { kind: "inherited" as const, capabilities };
369
- return [
370
- routeId,
371
- {
372
- id: route.id,
373
- ...raw,
374
- fingerprint: routeFingerprint(raw),
375
- },
376
- ];
377
- }),
378
- );
379
- return {
380
- routes: Object.freeze(routes),
381
- roles: Object.freeze(
382
- Object.fromEntries(
383
- ROLES.map((role) => [role, Object.freeze([...policy.roles[role]])]),
384
- ),
385
- ) as unknown as RoutePolicy["roles"],
386
- };
387
- }
388
-
389
- /** Default parent-model route used when no explicit policy file exists. */
390
- export function parentRoutePolicy(
391
- parentModel?: ParentModelCapabilities,
392
- ): RoutePolicy {
393
- const contextWindow = positiveInteger(parentModel?.contextWindow)
394
- ? parentModel.contextWindow
395
- : 1;
396
- const maxTokens = positiveInteger(parentModel?.maxTokens)
397
- ? Math.min(parentModel.maxTokens, contextWindow)
398
- : 1;
399
- const parsed = parseRoutePolicy({
400
- routes: {
401
- parent: {
402
- kind: "inherited",
403
- capabilities: {
404
- roles: [...ROLES],
405
- dialects: [...ROUTE_DIALECTS],
406
- contextWindow,
407
- maxTokens,
408
- },
409
- },
410
- },
411
- roles: Object.fromEntries(ROLES.map((role) => [role, ["parent"]])),
412
- });
413
- if (!parsed.ok) throw new Error("parent route policy is invalid");
414
- return parsed.policy;
415
- }
416
-
417
- function selectedSource(
418
- cwd: string,
419
- home: string,
420
- ): { kind: "project" | "user"; path: string } | { kind: "none" } {
421
- const mayExist = (candidate: string): boolean => {
422
- try {
423
- return lstatSync(candidate, { throwIfNoEntry: false }) !== undefined;
424
- } catch {
425
- // Permission and path-observation failures are explicit unreadable
426
- // policy candidates, not proof that the policy is absent.
427
- return true;
428
- }
429
- };
430
- const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
431
- if (mayExist(projectPath)) return { kind: "project", path: projectPath };
432
- const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
433
- if (mayExist(userPath)) return { kind: "user", path: userPath };
434
- return { kind: "none" };
435
- }
436
-
437
- export function loadRoutePolicy(
438
- options: {
439
- cwd?: string;
440
- home?: string;
441
- parentModel?: ParentModelCapabilities;
442
- } = {},
443
- ): RoutePolicyResolution {
444
- const cwd = path.resolve(options.cwd ?? process.cwd());
445
- const home = path.resolve(options.home ?? homedir());
446
- const source = selectedSource(cwd, home);
447
- if (source.kind === "none") {
448
- return {
449
- ok: true,
450
- source: { kind: "default" },
451
- policy: parentRoutePolicy(options.parentModel),
452
- };
453
- }
454
- let bytes: string;
455
- try {
456
- const stat = lstatSync(source.path);
457
- if (
458
- !stat.isFile() ||
459
- stat.isSymbolicLink() ||
460
- stat.size > MAX_POLICY_BYTES
461
- ) {
462
- throw new Error("unsafe policy file");
463
- }
464
- bytes = readFileSync(source.path, "utf8");
465
- } catch {
466
- return {
467
- ok: false,
468
- source,
469
- diagnostics: [{ code: "policy-unreadable" }],
470
- };
471
- }
472
- let value: unknown;
473
- try {
474
- value = JSON.parse(bytes);
475
- } catch {
476
- return {
477
- ok: false,
478
- source,
479
- diagnostics: [{ code: "policy-invalid-json" }],
480
- };
481
- }
482
- const parsed = parseRoutePolicy(value);
483
- return parsed.ok
484
- ? {
485
- ok: true,
486
- source,
487
- policy: constrainInheritedRoutes(parsed.policy, options.parentModel),
488
- }
489
- : { ok: false, source, diagnostics: parsed.diagnostics };
490
- }
491
-
492
- export function inspectRoutePolicy(
493
- resolution: RoutePolicyResolution,
494
- health: Readonly<Record<string, { state: string; retryAt?: number }>> = {},
495
- ): Record<string, unknown> {
496
- if (!resolution.ok) {
497
- return {
498
- ok: false,
499
- source: { kind: resolution.source.kind },
500
- diagnostics: structuredClone(resolution.diagnostics),
501
- };
502
- }
503
- return {
504
- ok: true,
505
- source: { kind: resolution.source.kind },
506
- routes: Object.values(resolution.policy.routes).map((route) => ({
507
- id: route.id,
508
- kind: route.kind,
509
- fingerprint: route.fingerprint,
510
- capabilities: structuredClone(route.capabilities),
511
- health: health[route.id]?.state ?? "healthy",
512
- ...(health[route.id]?.retryAt === undefined
513
- ? {}
514
- : { retryAt: health[route.id]?.retryAt }),
515
- })),
516
- roles: structuredClone(resolution.policy.roles),
517
- };
518
- }
519
-
520
- export interface ResolvedCustomRoute {
521
- url: string;
522
- model: string;
523
- dialect: RouteDialect;
524
- contextWindow: number;
525
- maxTokens: number;
526
- apiKey?: string;
527
- }
528
-
529
- export function resolveCustomRoute(
530
- route: CustomRoutePolicy,
531
- environment: Readonly<Record<string, string | undefined>> = process.env,
532
- ): ResolvedCustomRoute {
533
- const apiKey = route.apiKeyEnv ? environment[route.apiKeyEnv] : undefined;
534
- if (route.apiKeyEnv && !apiKey) {
535
- throw new Error("route-credential-unavailable:apiKeyEnv");
536
- }
537
- return {
538
- url: route.url,
539
- model: route.model,
540
- dialect: route.dialect,
541
- contextWindow: route.capabilities.contextWindow,
542
- maxTokens: route.capabilities.maxTokens,
543
- ...(apiKey ? { apiKey } : {}),
544
- };
545
- }