@abelxiaoxing/cadence 1.8.2 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,476 @@
1
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ import type { Usage } from "@earendil-works/pi-ai";
5
+ import { loadAgentDefinitions } from "./agent-registry.ts";
6
+
7
+ export const SUBAGENT_LIMITS = Object.freeze({
8
+ timeoutMs: 20 * 60_000,
9
+ maxOutputBytes: 256 * 1024,
10
+ terminateGraceMs: 1_000,
11
+ maxPromptBytes: 128 * 1024,
12
+ });
13
+
14
+ export type SubagentRole =
15
+ | "design-explorer"
16
+ | "implementation-worker"
17
+ | "diagnosis-worker";
18
+
19
+ export interface SubagentCapabilities {
20
+ readonly tools: readonly string[];
21
+ readonly writes: boolean;
22
+ }
23
+
24
+ export const SUBAGENT_CAPABILITIES: Readonly<
25
+ Record<SubagentRole, SubagentCapabilities>
26
+ > = Object.freeze({
27
+ "design-explorer": { tools: ["read", "grep", "find", "ls"], writes: false },
28
+ "implementation-worker": {
29
+ tools: ["read", "grep", "find", "ls", "write", "edit", "bash"],
30
+ writes: true,
31
+ },
32
+ "diagnosis-worker": {
33
+ tools: ["read", "grep", "find", "ls"],
34
+ writes: false,
35
+ },
36
+ });
37
+
38
+ export type SubagentTerminalStatus =
39
+ | "completed"
40
+ | "failed"
41
+ | "cancelled"
42
+ | "timed-out"
43
+ | "output-limit";
44
+
45
+ export interface SubagentEvent {
46
+ readonly type: string;
47
+ readonly toolName?: string;
48
+ readonly text?: string;
49
+ }
50
+
51
+ export interface SubagentProcessResult {
52
+ readonly status: SubagentTerminalStatus;
53
+ readonly finalText: string;
54
+ readonly usage?: Usage;
55
+ readonly stderr: string;
56
+ readonly error?: string;
57
+ readonly events: readonly SubagentEvent[];
58
+ readonly outputBytes: number;
59
+ readonly outputTruncated: boolean;
60
+ readonly exitCode: number | null;
61
+ }
62
+
63
+ export interface SubagentProcessInput {
64
+ readonly role: SubagentRole;
65
+ readonly cwd: string;
66
+ readonly prompt: string;
67
+ readonly model?: { provider: string; id: string };
68
+ readonly signal?: AbortSignal;
69
+ readonly timeoutMs?: number;
70
+ readonly maxOutputBytes?: number;
71
+ readonly piExecutable?: string;
72
+ readonly onEvent?: (event: SubagentEvent) => void;
73
+ }
74
+
75
+ const TRUNCATION_MARKER = "…[truncated]";
76
+
77
+ function decodeUtf8(value: Buffer): string {
78
+ const decoder = new TextDecoder("utf-8", { fatal: true });
79
+ const maxBoundary = Math.min(3, value.length);
80
+ // A bounded head may end, and a bounded tail may begin, in the middle of
81
+ // one UTF-8 sequence. Try only those small boundary adjustments; valid
82
+ // interior output is never discarded or rendered as U+FFFD.
83
+ for (let start = 0; start <= maxBoundary; start += 1) {
84
+ for (let end = value.length; end >= start; end -= 1) {
85
+ if (value.length - end > 3) break;
86
+ try {
87
+ return decoder.decode(value.subarray(start, end));
88
+ } catch {
89
+ // Try the next possible boundary.
90
+ }
91
+ }
92
+ }
93
+ return value.toString("utf8");
94
+ }
95
+
96
+ function utf8Prefix(value: Buffer, limit: number): string {
97
+ if (limit <= 0) return "";
98
+ let end = Math.min(value.length, limit);
99
+ while (end > 0 && (value[end - 1] & 0xc0) === 0x80) end -= 1;
100
+ if (end < Math.min(value.length, limit) && end > 0) {
101
+ const lead = value[end - 1];
102
+ const expected = lead < 0x80 ? 1 : lead < 0xe0 ? 2 : lead < 0xf0 ? 3 : 4;
103
+ if (expected <= Math.min(value.length, limit) - (end - 1)) {
104
+ end = Math.min(value.length, limit);
105
+ } else {
106
+ end -= 1;
107
+ }
108
+ }
109
+ return decodeUtf8(value.subarray(0, end));
110
+ }
111
+
112
+ function boundedText(value: string, limit: number): string {
113
+ const bytes = Buffer.from(value, "utf8");
114
+ if (bytes.length <= limit) return value;
115
+ const markerBytes = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
116
+ if (limit <= markerBytes) return utf8Prefix(bytes, limit);
117
+ return `${utf8Prefix(bytes, limit - markerBytes)}${TRUNCATION_MARKER}`;
118
+ }
119
+
120
+ class BoundedBuffer {
121
+ #head = Buffer.alloc(0);
122
+ #tail = Buffer.alloc(0);
123
+ #bytes = 0;
124
+ constructor(readonly limit: number) {}
125
+ append(chunk: Buffer): void {
126
+ this.#bytes += chunk.byteLength;
127
+ if (this.limit <= 0) return;
128
+ const headLimit = Math.ceil(this.limit / 2);
129
+ if (this.#head.length < headLimit) {
130
+ const take = Math.min(headLimit - this.#head.length, chunk.length);
131
+ this.#head = Buffer.concat([this.#head, chunk.subarray(0, take)]);
132
+ chunk = chunk.subarray(take);
133
+ }
134
+ const tailLimit = this.limit - headLimit;
135
+ if (chunk.length > tailLimit) chunk = chunk.subarray(-tailLimit);
136
+ if (chunk.length > 0 && tailLimit > 0) {
137
+ this.#tail = Buffer.concat([this.#tail, chunk]).subarray(-tailLimit);
138
+ }
139
+ }
140
+ get bytes(): number {
141
+ return this.#bytes;
142
+ }
143
+ get truncated(): boolean {
144
+ return this.#bytes > this.limit;
145
+ }
146
+ text(): string {
147
+ return `${decodeUtf8(this.#head)}${decodeUtf8(this.#tail)}`;
148
+ }
149
+ }
150
+
151
+ function textFromMessage(message: unknown): string {
152
+ if (!message || typeof message !== "object") return "";
153
+ const content = (message as { content?: unknown }).content;
154
+ if (typeof content === "string") return content;
155
+ if (!Array.isArray(content)) return "";
156
+ return content
157
+ .map((part) => {
158
+ if (typeof part === "string") return part;
159
+ if (!part || typeof part !== "object") return "";
160
+ const value = part as {
161
+ type?: unknown;
162
+ text?: unknown;
163
+ thinking?: unknown;
164
+ };
165
+ if (value.type === "text" && typeof value.text === "string")
166
+ return value.text;
167
+ if (value.type === "thinking" && typeof value.thinking === "string")
168
+ return value.thinking;
169
+ return "";
170
+ })
171
+ .join("");
172
+ }
173
+
174
+ function usageFromMessage(message: unknown): Usage | undefined {
175
+ if (!message || typeof message !== "object") return undefined;
176
+ const usage = (message as { usage?: unknown }).usage;
177
+ return usage && typeof usage === "object" ? (usage as Usage) : undefined;
178
+ }
179
+
180
+ function executableFromEnvironment(env: NodeJS.ProcessEnv): string {
181
+ const explicit = env.CADENCE_PI_EXECUTABLE ?? env.PI_EXECUTABLE;
182
+ if (explicit) return explicit;
183
+ const current = process.argv[1];
184
+ if (
185
+ current &&
186
+ (path.basename(current) === "pi" ||
187
+ current
188
+ .replaceAll("\\", "/")
189
+ .endsWith("/pi-coding-agent/dist/bundle/cli.js") ||
190
+ current.endsWith("\\\\pi-coding-agent\\dist\\bundle\\cli.js"))
191
+ ) {
192
+ return current;
193
+ }
194
+ return "pi";
195
+ }
196
+
197
+ function modelArgument(model: SubagentProcessInput["model"]): string[] {
198
+ if (!model?.provider || !model.id) return [];
199
+ return ["--model", `${model.provider}/${model.id}`];
200
+ }
201
+
202
+ /** Parse the last JSON object in a fenced result block without trusting it as authority. */
203
+ export function parseSubagentJson<T = unknown>(text: string): T | undefined {
204
+ const matches = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/giu)];
205
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
206
+ try {
207
+ const value: unknown = JSON.parse(matches[index]?.[1]?.trim() ?? "");
208
+ return value as T;
209
+ } catch {
210
+ // A malformed final block is a result failure, not an identity.
211
+ }
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ export function buildSubagentPrompt(input: {
217
+ role: SubagentRole;
218
+ objective: string;
219
+ context?: string;
220
+ agentContent?: string;
221
+ finalConvention?: string;
222
+ }): string {
223
+ const definition =
224
+ input.agentContent ??
225
+ loadAgentDefinitions().find((agent) => agent.role === input.role)?.content;
226
+ if (!definition) throw new Error("subagent-role-unavailable");
227
+ return [
228
+ definition,
229
+ "You are running as an isolated one-shot Pi child. Do not invoke workflow control tools, submit tools, or another agent.",
230
+ `Your allowed tools are: ${SUBAGENT_CAPABILITIES[input.role].tools.join(", ")}.`,
231
+ SUBAGENT_CAPABILITIES[input.role].writes
232
+ ? "You may edit only the disposable workspace supplied as your current working directory. The parent will inspect all changes; never commit or claim verification success."
233
+ : "You are read-only and must not modify files or run commands.",
234
+ input.objective,
235
+ input.context ?? "",
236
+ input.finalConvention ??
237
+ "Finish with a concise summary. If structured evidence is required, put exactly one JSON object in a ```json code fence as the final answer.",
238
+ ]
239
+ .filter(Boolean)
240
+ .join("\n\n");
241
+ }
242
+
243
+ function terminateProcess(
244
+ child: ChildProcessWithoutNullStreams,
245
+ graceMs: number,
246
+ ): Promise<void> {
247
+ return new Promise((resolve) => {
248
+ let done = false;
249
+ const finish = () => {
250
+ if (done) return;
251
+ done = true;
252
+ clearTimeout(timer);
253
+ resolve();
254
+ };
255
+ const timer = setTimeout(() => {
256
+ if (process.platform !== "win32" && child.pid) {
257
+ try {
258
+ process.kill(-child.pid, "SIGKILL");
259
+ } catch {
260
+ // The process group may already have exited.
261
+ }
262
+ } else if (!child.killed) {
263
+ child.kill("SIGKILL");
264
+ }
265
+ finish();
266
+ }, graceMs);
267
+ timer.unref?.();
268
+ child.once("close", finish);
269
+ if (process.platform !== "win32" && child.pid) {
270
+ try {
271
+ process.kill(-child.pid, "SIGTERM");
272
+ } catch {
273
+ child.kill("SIGTERM");
274
+ }
275
+ } else {
276
+ child.kill("SIGTERM");
277
+ }
278
+ });
279
+ }
280
+
281
+ /** Run a fresh Pi CLI. No cwd or tool allowlist is treated as an OS sandbox. */
282
+ export async function runSubagentProcess(
283
+ input: SubagentProcessInput,
284
+ ): Promise<SubagentProcessResult> {
285
+ const timeoutMs = input.timeoutMs ?? SUBAGENT_LIMITS.timeoutMs;
286
+ const maxOutputBytes = input.maxOutputBytes ?? SUBAGENT_LIMITS.maxOutputBytes;
287
+ const promptBytes = Buffer.byteLength(input.prompt, "utf8");
288
+ if (promptBytes > SUBAGENT_LIMITS.maxPromptBytes) {
289
+ return {
290
+ status: "output-limit",
291
+ finalText: "",
292
+ stderr: "",
293
+ events: [],
294
+ outputBytes: promptBytes,
295
+ outputTruncated: true,
296
+ exitCode: null,
297
+ error: "subagent-prompt-limit",
298
+ };
299
+ }
300
+ if (input.signal?.aborted) {
301
+ return {
302
+ status: "cancelled",
303
+ finalText: "",
304
+ stderr: "",
305
+ events: [],
306
+ outputBytes: 0,
307
+ outputTruncated: false,
308
+ exitCode: null,
309
+ error: "cancelled",
310
+ };
311
+ }
312
+ const tools = SUBAGENT_CAPABILITIES[input.role].tools.join(",");
313
+ const args = [
314
+ "--mode",
315
+ "json",
316
+ "-p",
317
+ "--no-session",
318
+ "--no-extensions",
319
+ "--no-context-files",
320
+ "--no-approve",
321
+ "--tools",
322
+ tools,
323
+ ...modelArgument(input.model),
324
+ ];
325
+ const env = { ...process.env, CADENCE_SUBAGENT_CHILD: "1" };
326
+ const output = new BoundedBuffer(maxOutputBytes);
327
+ const stderr = new BoundedBuffer(Math.min(maxOutputBytes, 64 * 1024));
328
+ const events: SubagentEvent[] = [];
329
+ let child: ChildProcessWithoutNullStreams;
330
+ try {
331
+ child = spawn(
332
+ input.piExecutable ?? executableFromEnvironment(process.env),
333
+ args,
334
+ {
335
+ cwd: input.cwd,
336
+ env,
337
+ shell: false,
338
+ stdio: ["pipe", "pipe", "pipe"],
339
+ ...(process.platform === "win32" ? {} : { detached: true }),
340
+ },
341
+ );
342
+ } catch {
343
+ return {
344
+ status: "failed",
345
+ finalText: "",
346
+ stderr: "",
347
+ events,
348
+ outputBytes: 0,
349
+ outputTruncated: false,
350
+ exitCode: null,
351
+ error: "subagent-spawn-failed",
352
+ };
353
+ }
354
+ const decoder = new StringDecoder("utf8");
355
+ let finalText = "";
356
+ let usage: Usage | undefined;
357
+ let parseBuffer = "";
358
+ let terminal: SubagentTerminalStatus | undefined;
359
+ let terminalError: string | undefined;
360
+ const emit = (event: SubagentEvent) => {
361
+ events.push(event);
362
+ try {
363
+ input.onEvent?.(event);
364
+ } catch {
365
+ // Progress observers are presentation-only and cannot fail execution.
366
+ }
367
+ };
368
+ const processLine = (rawLine: string): void => {
369
+ const line = rawLine.replace(/\r$/u, "");
370
+ if (!line || terminal) return;
371
+ try {
372
+ const event = JSON.parse(line) as Record<string, unknown>;
373
+ const type = typeof event.type === "string" ? event.type : "unknown";
374
+ const message = event.message;
375
+ const text =
376
+ type === "message_end" ? textFromMessage(message) : undefined;
377
+ if (
378
+ text &&
379
+ message &&
380
+ typeof message === "object" &&
381
+ (message as { role?: unknown }).role === "assistant"
382
+ ) {
383
+ finalText = boundedText(text, maxOutputBytes);
384
+ usage = usageFromMessage(message) ?? usage;
385
+ }
386
+ if (
387
+ type === "message_update" &&
388
+ event.usage &&
389
+ typeof event.usage === "object"
390
+ )
391
+ usage = event.usage as Usage;
392
+ emit({
393
+ type,
394
+ ...(typeof event.toolName === "string"
395
+ ? { toolName: event.toolName }
396
+ : {}),
397
+ ...(text ? { text: boundedText(text, 4096) } : {}),
398
+ });
399
+ } catch {
400
+ emit({ type: "malformed-event" });
401
+ terminal = "failed";
402
+ terminalError = "subagent-jsonl-malformed";
403
+ void terminateProcess(child, SUBAGENT_LIMITS.terminateGraceMs);
404
+ }
405
+ };
406
+ const consumeDecoded = (decoded: string): void => {
407
+ if (!decoded || terminal) return;
408
+ if (
409
+ Buffer.byteLength(parseBuffer, "utf8") +
410
+ Buffer.byteLength(decoded, "utf8") >
411
+ maxOutputBytes
412
+ ) {
413
+ parseBuffer = "";
414
+ terminal = "output-limit";
415
+ void terminateProcess(child, SUBAGENT_LIMITS.terminateGraceMs);
416
+ return;
417
+ }
418
+ parseBuffer += decoded;
419
+ for (;;) {
420
+ const newline = parseBuffer.indexOf("\n");
421
+ if (newline < 0) break;
422
+ const line = parseBuffer.slice(0, newline);
423
+ parseBuffer = parseBuffer.slice(newline + 1);
424
+ processLine(line);
425
+ if (terminal) return;
426
+ }
427
+ };
428
+ const consume = (chunk: Buffer) => {
429
+ output.append(chunk);
430
+ consumeDecoded(decoder.write(chunk));
431
+ if (output.truncated && !terminal) {
432
+ terminal = "output-limit";
433
+ void terminateProcess(child, SUBAGENT_LIMITS.terminateGraceMs);
434
+ }
435
+ };
436
+ child.stdout.on("data", consume);
437
+ child.stderr.on("data", (chunk: Buffer) => stderr.append(chunk));
438
+ const termination = new Promise<number | null>((resolve) => {
439
+ // A spawn error is followed by close; keep the error listener only to
440
+ // prevent an uncaught event while waiting for the pipes to settle.
441
+ child.once("error", () => undefined);
442
+ child.once("close", (code) => resolve(code));
443
+ });
444
+ const cancel = (status: "cancelled" | "timed-out") => {
445
+ if (!terminal) terminal = status;
446
+ void terminateProcess(child, SUBAGENT_LIMITS.terminateGraceMs);
447
+ };
448
+ const abortHandler = () => cancel("cancelled");
449
+ input.signal?.addEventListener("abort", abortHandler, { once: true });
450
+ const timer = setTimeout(() => cancel("timed-out"), timeoutMs);
451
+ timer.unref?.();
452
+ child.stdin.end(input.prompt);
453
+ const exitCode = await termination;
454
+ if (timer) clearTimeout(timer);
455
+ input.signal?.removeEventListener("abort", abortHandler);
456
+ const trailing = decoder.end();
457
+ consumeDecoded(trailing);
458
+ if (parseBuffer && !terminal) {
459
+ processLine(parseBuffer);
460
+ parseBuffer = "";
461
+ }
462
+ if (!terminal) terminal = exitCode === 0 ? "completed" : "failed";
463
+ return {
464
+ status: terminal,
465
+ finalText,
466
+ usage,
467
+ stderr: boundedText(stderr.text(), 64 * 1024),
468
+ ...(terminal === "failed"
469
+ ? { error: terminalError ?? `subagent-exit-${exitCode ?? "unknown"}` }
470
+ : {}),
471
+ events,
472
+ outputBytes: output.bytes,
473
+ outputTruncated: output.truncated,
474
+ exitCode,
475
+ };
476
+ }
@@ -28,9 +28,7 @@ export function isTransportTimeoutCode(
28
28
  export function transportFailureError(code: string): Error {
29
29
  return isTransportTimeoutCode(code)
30
30
  ? new TransportTimeout(code)
31
- : new Error(
32
- code === "child-provider-rate-limited" ? code : "transport-failure",
33
- );
31
+ : new Error("transport-failure");
34
32
  }
35
33
 
36
34
  /** Snapshot at the start of each request; Workers cannot override these values. */
@@ -2,10 +2,15 @@ import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
5
+ import {
6
+ type CandidateContextBoundary,
7
+ type CandidateContextRef,
8
+ classifyCandidateContextRequest,
9
+ normalizeCandidateContextRefs,
10
+ } from "./candidate-context.ts";
5
11
  import { compareCanonicalStrings } from "./canonical.ts";
6
12
  import {
7
13
  type ApprovalBoundaryCode,
8
- CHILD_TRANSPORT_CODES,
9
14
  isValidRelativePath,
10
15
  type StructuredVerificationContract,
11
16
  verificationBoundInputPaths,
@@ -16,16 +21,9 @@ import type {
16
21
  ImplementPlan,
17
22
  PlanTaskDraft,
18
23
  } from "./delivery-compiler.ts";
19
- import type { RoutePolicy } from "./route-policy.ts";
20
24
  import { canonicalJson } from "./run-state.ts";
21
25
  import type { OperationLease } from "./run-store.ts";
22
26
  import type { ResolvedStateRoot } from "./state-root.ts";
23
- import {
24
- type CandidateContextBoundary,
25
- type CandidateContextRef,
26
- classifyCandidateContextRequest,
27
- normalizeCandidateContextRefs,
28
- } from "./submit-tool.ts";
29
27
  import type { WorkspaceEntry } from "./workspace-store.ts";
30
28
 
31
29
  export const SHA256 = /^[a-f0-9]{64}$/u;
@@ -242,13 +240,7 @@ export type ParentRecoveryStrategy =
242
240
  | "restore-runtime-environment"
243
241
  | "restore-verification-capability";
244
242
 
245
- const ROUTE_RECOVERY_CODES = new Set([
246
- "endpoint-unavailable",
247
- "route-capability-insufficient",
248
- "route-not-declared",
249
- "route-rebind-required",
250
- ...CHILD_TRANSPORT_CODES,
251
- ]);
243
+ const ROUTE_RECOVERY_CODES = new Set(["child-timeout", "transport-failure"]);
252
244
 
253
245
  const ENVIRONMENT_RECOVERY_CODES = new Set([
254
246
  "bubblewrap-launch-failed",
@@ -277,7 +269,8 @@ const ENVIRONMENT_RECOVERY_CODES = new Set([
277
269
  ]);
278
270
 
279
271
  const CORRECTION_RECOVERY_CODES = new Set([
280
- ...CHILD_TRANSPORT_CODES,
272
+ "child-timeout",
273
+ "transport-failure",
281
274
  "artifact-attempts-exhausted",
282
275
  "baseline-deletion-drift",
283
276
  "baseline-directory-drift",
@@ -655,8 +648,6 @@ export interface WorkflowWorker {
655
648
  baselineRevisionId: string;
656
649
  currentWorkspaceRevisionId: string;
657
650
  };
658
- updateRoutePolicy?(policy: RoutePolicy): void;
659
- routePolicyStatus?(): Record<string, unknown>;
660
651
  }
661
652
 
662
653
  export interface WorkflowDelivery {
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
5
+ import { permitsContextRead } from "./candidate-context.ts";
5
6
  import {
6
7
  assertPlanWithinChangeContract,
7
8
  normalizeChangeContract,
@@ -21,7 +22,6 @@ import {
21
22
  } from "./delivery-compiler.ts";
22
23
  import { compareDeliveryRevision } from "./delivery-revision.ts";
23
24
  import { snapshotFiles } from "./file-snapshot.ts";
24
- import type { RoutePolicy } from "./route-policy.ts";
25
25
  import {
26
26
  canonicalJson,
27
27
  type RunProjection,
@@ -40,7 +40,6 @@ import {
40
40
  RECOVERY_SCHEMA,
41
41
  REJECTED_DELIVERY_SCHEMA,
42
42
  } from "./storage-schema.ts";
43
- import { permitsContextRead } from "./submit-tool.ts";
44
43
  import {
45
44
  type ActiveOperation,
46
45
  assertDelivery,
@@ -206,19 +205,6 @@ export class WorkflowEngine {
206
205
  return new WorkflowEngine(options);
207
206
  }
208
207
 
209
- updateRoutePolicy(policy: RoutePolicy): void {
210
- this.#assertOpen();
211
- if (!this.#worker.updateRoutePolicy) {
212
- throw new Error("route-policy-update-unavailable");
213
- }
214
- this.#worker.updateRoutePolicy(policy);
215
- }
216
-
217
- routePolicyStatus(): Record<string, unknown> | undefined {
218
- this.#assertOpen();
219
- return this.#worker.routePolicyStatus?.();
220
- }
221
-
222
208
  #assertOpen(): void {
223
209
  if (this.#closed) throw new Error("workflow-engine-closed");
224
210
  }
@@ -1,39 +0,0 @@
1
- {
2
- "routes": {
3
- "parent": {
4
- "kind": "inherited",
5
- "capabilities": {
6
- "roles": [
7
- "design-explorer",
8
- "implementation-worker",
9
- "diagnosis-worker"
10
- ],
11
- "dialects": [
12
- "openai-completions",
13
- "openai-responses",
14
- "anthropic-messages"
15
- ],
16
- "contextWindow": 256000,
17
- "maxTokens": 128000
18
- }
19
- },
20
- "custom-worker": {
21
- "kind": "custom",
22
- "url": "https://example.invalid/v1",
23
- "model": "example-model",
24
- "dialect": "openai-responses",
25
- "apiKeyEnv": "CADENCE_CUSTOM_WORKER_API_KEY",
26
- "capabilities": {
27
- "roles": ["implementation-worker"],
28
- "dialects": ["openai-responses"],
29
- "contextWindow": 256000,
30
- "maxTokens": 128000
31
- }
32
- }
33
- },
34
- "roles": {
35
- "design-explorer": ["parent"],
36
- "implementation-worker": ["custom-worker", "parent"],
37
- "diagnosis-worker": ["parent"]
38
- }
39
- }
@@ -1,58 +0,0 @@
1
- import type { ChildFailure } from "./contracts.ts";
2
-
3
- /** Conservative admission estimate, not a tokenizer or a claim about provider billing. */
4
- export function childContextBudget(model: {
5
- contextWindow?: number;
6
- maxTokens?: number;
7
- }): number {
8
- if (
9
- !Number.isFinite(model.contextWindow) ||
10
- !model.contextWindow ||
11
- model.contextWindow < 1
12
- )
13
- return 4 * 1024 * 1024;
14
- const reserve = Math.min(
15
- model.maxTokens ?? 8192,
16
- Math.floor(model.contextWindow / 2),
17
- );
18
- return Math.min(
19
- 4 * 1024 * 1024,
20
- Math.max(1024, Math.floor((model.contextWindow - reserve - 1024) * 2)),
21
- );
22
- }
23
-
24
- export function classifyProviderFailure(error: unknown): ChildFailure {
25
- const text =
26
- typeof error === "string"
27
- ? error
28
- : error instanceof Error
29
- ? error.message
30
- : "";
31
- if (
32
- /context.{0,30}(?:length|window|limit|exceed)|too many tokens|input.{0,20}too long/iu.test(
33
- text,
34
- )
35
- )
36
- return { kind: "execution-limit", code: "child-context-limit" };
37
- if (
38
- /\b(?:401|403)\b|invalid.{0,10}api.?key|unauthorized|authentication/iu.test(
39
- text,
40
- )
41
- )
42
- return {
43
- kind: "environment",
44
- code: "child-provider-authentication-failed",
45
- stage: "child-provider-stream",
46
- };
47
- if (/\b429\b|rate.?limit|too many requests/iu.test(text))
48
- return {
49
- kind: "transport",
50
- code: "child-provider-rate-limited",
51
- stage: "child-provider-stream",
52
- };
53
- return {
54
- kind: "transport",
55
- code: "child-provider-stream-error",
56
- stage: "child-provider-stream",
57
- };
58
- }