@caupulican/pi-adaptative 0.81.21 → 0.81.23

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 (63) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/agent-session.d.ts +3 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +28 -21
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/failure-corpus.d.ts +10 -1
  7. package/dist/core/failure-corpus.d.ts.map +1 -1
  8. package/dist/core/failure-corpus.js +32 -26
  9. package/dist/core/failure-corpus.js.map +1 -1
  10. package/dist/core/model-router/intent-classifier.d.ts.map +1 -1
  11. package/dist/core/model-router/intent-classifier.js +1 -1
  12. package/dist/core/model-router/intent-classifier.js.map +1 -1
  13. package/dist/core/model-router/route-judge.d.ts.map +1 -1
  14. package/dist/core/model-router/route-judge.js +10 -6
  15. package/dist/core/model-router/route-judge.js.map +1 -1
  16. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  17. package/dist/core/model-router/tool-escalation.js +2 -1
  18. package/dist/core/model-router/tool-escalation.js.map +1 -1
  19. package/dist/core/model-router-controller.d.ts.map +1 -1
  20. package/dist/core/model-router-controller.js +27 -2
  21. package/dist/core/model-router-controller.js.map +1 -1
  22. package/dist/core/session-analytics.d.ts +7 -1
  23. package/dist/core/session-analytics.d.ts.map +1 -1
  24. package/dist/core/session-analytics.js +42 -11
  25. package/dist/core/session-analytics.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +1 -1
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js.map +1 -1
  29. package/dist/core/tool-recovery-log-records.d.ts +24 -0
  30. package/dist/core/tool-recovery-log-records.d.ts.map +1 -0
  31. package/dist/core/tool-recovery-log-records.js +63 -0
  32. package/dist/core/tool-recovery-log-records.js.map +1 -0
  33. package/dist/core/tool-recovery-log-worker.d.ts +2 -0
  34. package/dist/core/tool-recovery-log-worker.d.ts.map +1 -0
  35. package/dist/core/tool-recovery-log-worker.js +44 -0
  36. package/dist/core/tool-recovery-log-worker.js.map +1 -0
  37. package/dist/core/tool-recovery-logger.d.ts +59 -0
  38. package/dist/core/tool-recovery-logger.d.ts.map +1 -0
  39. package/dist/core/tool-recovery-logger.js +197 -0
  40. package/dist/core/tool-recovery-logger.js.map +1 -0
  41. package/dist/core/tool-repair-health.d.ts +2 -1
  42. package/dist/core/tool-repair-health.d.ts.map +1 -1
  43. package/dist/core/tool-repair-health.js +13 -4
  44. package/dist/core/tool-repair-health.js.map +1 -1
  45. package/dist/core/tool-repair-settings.d.ts +1 -0
  46. package/dist/core/tool-repair-settings.d.ts.map +1 -1
  47. package/dist/core/tool-repair-settings.js +2 -1
  48. package/dist/core/tool-repair-settings.js.map +1 -1
  49. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +25 -1
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/docs/settings.md +6 -4
  54. package/docs/tool-repair.md +8 -7
  55. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  56. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  57. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  58. package/examples/extensions/sandbox/package-lock.json +2 -2
  59. package/examples/extensions/sandbox/package.json +1 -1
  60. package/examples/extensions/with-deps/package-lock.json +2 -2
  61. package/examples/extensions/with-deps/package.json +1 -1
  62. package/npm-shrinkwrap.json +12 -12
  63. package/package.json +4 -4
@@ -40,6 +40,16 @@ export interface FailureCorpusFs {
40
40
  size: number;
41
41
  };
42
42
  }
43
+ export declare function createToolValidationFailureCorpusRecord(args: {
44
+ provider?: string;
45
+ modelId?: string;
46
+ tool: string;
47
+ failureModes: readonly string[];
48
+ shape: readonly ToolValidationFailureShapeEntry[];
49
+ errorKeywords?: readonly string[];
50
+ ts: string;
51
+ }): ToolValidationFailureCorpusRecord;
52
+ export declare function writeFailureCorpusRecord(filePath: string, record: FailureCorpusRecord, fs?: FailureCorpusFs): void;
43
53
  export declare class FailureCorpusRecorder {
44
54
  private total;
45
55
  private unknown;
@@ -69,7 +79,6 @@ export declare class FailureCorpusRecorder {
69
79
  }): void;
70
80
  stats(): FailureCorpusStats;
71
81
  private appendRecord;
72
- private rotateIfNeeded;
73
82
  }
74
83
  export declare function redactSecrets(message: string): string;
75
84
  //# sourceMappingURL=failure-corpus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"failure-corpus.d.ts","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord({\n\t\t\tkind: \"tool_validation\",\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\ttool: args.tool,\n\t\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t\t});\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\tthis.fs.mkdirSync(dirname(this.filePath), { recursive: true });\n\t\t\tthis.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\t\t\tthis.rotateIfNeeded();\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(): void {\n\t\tif (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES) return;\n\t\tconst lines = this.fs\n\t\t\t.readFileSync(this.filePath, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().length > 0)\n\t\t\t.slice(-MAX_ROTATED_RECORDS);\n\t\tthis.fs.writeFileSync(this.filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[redacted]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [redacted]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[redacted]\");\n}\n"]}
1
+ {"version":3,"file":"failure-corpus.d.ts","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport function createToolValidationFailureCorpusRecord(args: {\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: readonly string[];\n\tshape: readonly ToolValidationFailureShapeEntry[];\n\terrorKeywords?: readonly string[];\n\tts: string;\n}): ToolValidationFailureCorpusRecord {\n\treturn {\n\t\tkind: \"tool_validation\",\n\t\tts: args.ts,\n\t\tprovider: args.provider,\n\t\tmodelId: args.modelId,\n\t\ttool: args.tool,\n\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t};\n}\n\nexport function writeFailureCorpusRecord(\n\tfilePath: string,\n\trecord: FailureCorpusRecord,\n\tfs: FailureCorpusFs = DEFAULT_FS,\n): void {\n\tfs.mkdirSync(dirname(filePath), { recursive: true });\n\tfs.appendFileSync(filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\trotateFailureCorpusIfNeeded(filePath, fs);\n}\n\nfunction rotateFailureCorpusIfNeeded(filePath: string, fs: FailureCorpusFs): void {\n\tif (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES) return;\n\tconst lines = fs\n\t\t.readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0)\n\t\t.slice(-MAX_ROTATED_RECORDS);\n\tfs.writeFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n}\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\twriteFailureCorpusRecord(this.filePath, record, this.fs);\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[REDACTED]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [REDACTED]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[REDACTED]\");\n}\n"]}
@@ -4,6 +4,33 @@ const DEFAULT_FS = { existsSync, mkdirSync, appendFileSync, readFileSync, writeF
4
4
  const MAX_MESSAGE_CHARS = 500;
5
5
  const MAX_FILE_BYTES = 512 * 1024;
6
6
  const MAX_ROTATED_RECORDS = 1000;
7
+ export function createToolValidationFailureCorpusRecord(args) {
8
+ return {
9
+ kind: "tool_validation",
10
+ ts: args.ts,
11
+ provider: args.provider,
12
+ modelId: args.modelId,
13
+ tool: args.tool,
14
+ failureModes: [...new Set(args.failureModes)].sort(),
15
+ shape: sanitizeToolValidationShape(args.shape),
16
+ errorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),
17
+ };
18
+ }
19
+ export function writeFailureCorpusRecord(filePath, record, fs = DEFAULT_FS) {
20
+ fs.mkdirSync(dirname(filePath), { recursive: true });
21
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, "utf-8");
22
+ rotateFailureCorpusIfNeeded(filePath, fs);
23
+ }
24
+ function rotateFailureCorpusIfNeeded(filePath, fs) {
25
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES)
26
+ return;
27
+ const lines = fs
28
+ .readFileSync(filePath, "utf-8")
29
+ .split("\n")
30
+ .filter((line) => line.trim().length > 0)
31
+ .slice(-MAX_ROTATED_RECORDS);
32
+ fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf-8");
33
+ }
7
34
  export class FailureCorpusRecorder {
8
35
  total = 0;
9
36
  unknown = 0;
@@ -31,40 +58,19 @@ export class FailureCorpusRecorder {
31
58
  });
32
59
  }
33
60
  recordToolValidation(args) {
34
- this.appendRecord({
35
- kind: "tool_validation",
36
- ts: this.now().toISOString(),
37
- provider: args.provider,
38
- modelId: args.modelId,
39
- tool: args.tool,
40
- failureModes: [...new Set(args.failureModes)].sort(),
41
- shape: sanitizeToolValidationShape(args.shape),
42
- errorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),
43
- });
61
+ this.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));
44
62
  }
45
63
  stats() {
46
64
  return { total: this.total, unknown: this.unknown };
47
65
  }
48
66
  appendRecord(record) {
49
67
  try {
50
- this.fs.mkdirSync(dirname(this.filePath), { recursive: true });
51
- this.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\n`, "utf-8");
52
- this.rotateIfNeeded();
68
+ writeFailureCorpusRecord(this.filePath, record, this.fs);
53
69
  }
54
70
  catch (error) {
55
71
  this.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);
56
72
  }
57
73
  }
58
- rotateIfNeeded() {
59
- if (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES)
60
- return;
61
- const lines = this.fs
62
- .readFileSync(this.filePath, "utf-8")
63
- .split("\n")
64
- .filter((line) => line.trim().length > 0)
65
- .slice(-MAX_ROTATED_RECORDS);
66
- this.fs.writeFileSync(this.filePath, `${lines.join("\n")}\n`, "utf-8");
67
- }
68
74
  }
69
75
  function sanitizeToolValidationShape(shape) {
70
76
  return shape.map((entry) => ({
@@ -76,8 +82,8 @@ function sanitizeToolValidationShape(shape) {
76
82
  }
77
83
  export function redactSecrets(message) {
78
84
  return message
79
- .replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, "[redacted]")
80
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
81
- .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "[redacted]");
85
+ .replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, "[REDACTED]")
86
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
87
+ .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "[REDACTED]");
82
88
  }
83
89
  //# sourceMappingURL=failure-corpus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"failure-corpus.js","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord({\n\t\t\tkind: \"tool_validation\",\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\ttool: args.tool,\n\t\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t\t});\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\tthis.fs.mkdirSync(dirname(this.filePath), { recursive: true });\n\t\t\tthis.fs.appendFileSync(this.filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\t\t\tthis.rotateIfNeeded();\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\n\tprivate rotateIfNeeded(): void {\n\t\tif (!this.fs.existsSync(this.filePath) || this.fs.statSync(this.filePath).size <= MAX_FILE_BYTES) return;\n\t\tconst lines = this.fs\n\t\t\t.readFileSync(this.filePath, \"utf-8\")\n\t\t\t.split(\"\\n\")\n\t\t\t.filter((line) => line.trim().length > 0)\n\t\t\t.slice(-MAX_ROTATED_RECORDS);\n\t\tthis.fs.writeFileSync(this.filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[redacted]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [redacted]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[redacted]\");\n}\n"]}
1
+ {"version":3,"file":"failure-corpus.js","sourceRoot":"","sources":["../../src/core/failure-corpus.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ClassifiedError } from \"@caupulican/pi-agent-core\";\n\nexport interface ProviderFailureCorpusRecord {\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\treason: ClassifiedError[\"reason\"];\n\tretryable: boolean;\n\tmessage: string;\n}\n\nexport interface ToolValidationFailureShapeEntry {\n\tpath: string;\n\texpectedType: string;\n\treceivedType: string;\n\tkeyword?: string;\n}\n\nexport interface ToolValidationFailureCorpusRecord {\n\tkind: \"tool_validation\";\n\tts: string;\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: string[];\n\tshape: ToolValidationFailureShapeEntry[];\n\terrorKeywords: string[];\n}\n\nexport type FailureCorpusRecord = ProviderFailureCorpusRecord | ToolValidationFailureCorpusRecord;\n\nexport interface FailureCorpusStats {\n\ttotal: number;\n\tunknown: number;\n}\n\nexport interface FailureCorpusFs {\n\texistsSync(path: string): boolean;\n\tmkdirSync(path: string, options: { recursive: true }): void;\n\tappendFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\treadFileSync(path: string, encoding: \"utf-8\"): string;\n\twriteFileSync(path: string, data: string, encoding: \"utf-8\"): void;\n\tstatSync(path: string): { size: number };\n}\n\nconst DEFAULT_FS: FailureCorpusFs = { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, statSync };\nconst MAX_MESSAGE_CHARS = 500;\nconst MAX_FILE_BYTES = 512 * 1024;\nconst MAX_ROTATED_RECORDS = 1000;\n\nexport function createToolValidationFailureCorpusRecord(args: {\n\tprovider?: string;\n\tmodelId?: string;\n\ttool: string;\n\tfailureModes: readonly string[];\n\tshape: readonly ToolValidationFailureShapeEntry[];\n\terrorKeywords?: readonly string[];\n\tts: string;\n}): ToolValidationFailureCorpusRecord {\n\treturn {\n\t\tkind: \"tool_validation\",\n\t\tts: args.ts,\n\t\tprovider: args.provider,\n\t\tmodelId: args.modelId,\n\t\ttool: args.tool,\n\t\tfailureModes: [...new Set(args.failureModes)].sort(),\n\t\tshape: sanitizeToolValidationShape(args.shape),\n\t\terrorKeywords: [...new Set(args.errorKeywords ?? [])].sort(),\n\t};\n}\n\nexport function writeFailureCorpusRecord(\n\tfilePath: string,\n\trecord: FailureCorpusRecord,\n\tfs: FailureCorpusFs = DEFAULT_FS,\n): void {\n\tfs.mkdirSync(dirname(filePath), { recursive: true });\n\tfs.appendFileSync(filePath, `${JSON.stringify(record)}\\n`, \"utf-8\");\n\trotateFailureCorpusIfNeeded(filePath, fs);\n}\n\nfunction rotateFailureCorpusIfNeeded(filePath: string, fs: FailureCorpusFs): void {\n\tif (!fs.existsSync(filePath) || fs.statSync(filePath).size <= MAX_FILE_BYTES) return;\n\tconst lines = fs\n\t\t.readFileSync(filePath, \"utf-8\")\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.trim().length > 0)\n\t\t.slice(-MAX_ROTATED_RECORDS);\n\tfs.writeFileSync(filePath, `${lines.join(\"\\n\")}\\n`, \"utf-8\");\n}\n\nexport class FailureCorpusRecorder {\n\tprivate total = 0;\n\tprivate unknown = 0;\n\tprivate readonly filePath: string;\n\tprivate readonly fs: FailureCorpusFs;\n\tprivate readonly now: () => Date;\n\tprivate readonly debug: (message: string) => void;\n\n\tconstructor(args: { filePath: string; fs?: FailureCorpusFs; now?: () => Date; debug?: (message: string) => void }) {\n\t\tthis.filePath = args.filePath;\n\t\tthis.fs = args.fs ?? DEFAULT_FS;\n\t\tthis.now = args.now ?? (() => new Date());\n\t\tthis.debug = args.debug ?? (() => {});\n\t}\n\n\trecord(args: { provider?: string; modelId?: string; message: string; classified: ClassifiedError }): void {\n\t\tthis.total += 1;\n\t\tif (args.classified.reason === \"unknown\") this.unknown += 1;\n\t\tthis.appendRecord({\n\t\t\tts: this.now().toISOString(),\n\t\t\tprovider: args.provider,\n\t\t\tmodelId: args.modelId,\n\t\t\treason: args.classified.reason,\n\t\t\tretryable: args.classified.retryable,\n\t\t\tmessage: redactSecrets(args.message).slice(0, MAX_MESSAGE_CHARS),\n\t\t});\n\t}\n\n\trecordToolValidation(args: {\n\t\tprovider?: string;\n\t\tmodelId?: string;\n\t\ttool: string;\n\t\tfailureModes: readonly string[];\n\t\tshape: readonly ToolValidationFailureShapeEntry[];\n\t\terrorKeywords?: readonly string[];\n\t}): void {\n\t\tthis.appendRecord(createToolValidationFailureCorpusRecord({ ...args, ts: this.now().toISOString() }));\n\t}\n\n\tstats(): FailureCorpusStats {\n\t\treturn { total: this.total, unknown: this.unknown };\n\t}\n\n\tprivate appendRecord(record: FailureCorpusRecord): void {\n\t\ttry {\n\t\t\twriteFailureCorpusRecord(this.filePath, record, this.fs);\n\t\t} catch (error) {\n\t\t\tthis.debug(`failure corpus write skipped: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n}\n\nfunction sanitizeToolValidationShape(\n\tshape: readonly ToolValidationFailureShapeEntry[],\n): ToolValidationFailureShapeEntry[] {\n\treturn shape.map((entry) => ({\n\t\tpath: entry.path,\n\t\texpectedType: entry.expectedType,\n\t\treceivedType: entry.receivedType,\n\t\t...(entry.keyword ? { keyword: entry.keyword } : {}),\n\t}));\n}\n\nexport function redactSecrets(message: string): string {\n\treturn message\n\t\t.replace(/sk-(?:proj-|ant-)?[A-Za-z0-9._-]{8,}/g, \"[REDACTED]\")\n\t\t.replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, \"Bearer [REDACTED]\")\n\t\t.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, \"[REDACTED]\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"intent-classifier.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/intent-classifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAiCtD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CA0ItE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAG3E","sourcesContent":["import type { RouteDecision } from \"../autonomy/contracts.ts\";\n\nexport type ModelRouterIntent = \"research\" | \"modify\";\n\nconst EXPLICIT_MODIFY_REQUEST_RE =\n\t/^(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+.*\\b(add|apply|build|change|commit|create|delete|edit|fix|generate|implement|install|modify|patch|refactor|remove|rename|replace|run|test|update|write|publish|release|push|deploy|tag|reset|clean|rewrite)\\b/i;\n\nconst READ_ONLY_QUESTION_RE =\n\t/^(?:(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+)?(?:how|what|why|when|where|which|who|explain|summarize|compare|describe|list|show|search|find|view|read|locate)\\b/i;\n\nconst RELEASE_PUBLISH_RE = /\\b(publish|release|push|deploy|tag)\\b/i;\nconst SECURITY_AUTH_RE = /\\b(auth|token|credential|credentials|secret|api[-_]key)\\b/i;\nconst DESTRUCTIVE_RE = /\\b(delete|reset|rm\\s+-rf|clean)\\b/i;\n\nconst SELF_MOD_MUTATE_RE =\n\t/\\b(modify|change|write|update|edit|delete|add|remove)\\s+.*\\b(skills|prompts|settings|tools|behavior)\\b|self[-_]modification/i;\nconst ARCHITECTURE_MUTATE_RE = /\\b(rewrite|redesign|change|modify|rearchitect)\\s+.*\\b(architecture|architect)\\b/i;\n\n// Planning floor: plans steer all downstream work, so planning never routes cheap by default.\n// Only the route judge may downgrade a planning prompt back to cheap (explicit trivial verdict).\n// Core terms are always planning; design/architecture words count only with prospective phrasing,\n// so lookups like \"show me the architecture\" stay cheap.\nconst PLANNING_CORE_RE = /\\b(plan|planning|roadmap|strategy)\\b/i;\nconst PLANNING_DESIGN_WORD_RE = /\\b(design|architect\\w*|structure|approach)\\b/i;\nconst PLANNING_PROSPECTIVE_RE =\n\t/\\b(how (?:should|would|do we|can we)|what(?:'s| is) the best|propose|draft|come up with|figure out|decide (?:on|how))\\b/i;\n\nfunction isPlanningPrompt(text: string): boolean {\n\treturn PLANNING_CORE_RE.test(text) || (PLANNING_DESIGN_WORD_RE.test(text) && PLANNING_PROSPECTIVE_RE.test(text));\n}\n\nconst REFACTOR_RE = /\\b(refactor|refactoring)\\b/i;\nconst TEST_VALIDATION_RE = /\\b(test|testing|validation|lint|vitest|jest|run)\\b/i;\nconst IMPLEMENT_RE = /\\b(implement|fix|apply|change|update|create|write|generate|modify|edit|patch|add)\\b/i;\n\nexport function classifyModelRouterRoute(prompt: string): RouteDecision {\n\tconst text = prompt.trim();\n\n\tif (text.length === 0) {\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.1,\n\t\t\treasonCode: \"empty_prompt\",\n\t\t\treasons: [\"Empty or whitespace prompt\"],\n\t\t};\n\t}\n\n\t// 1. Explicit read-only questions/lookups dominate (unless prefixed by explicit mutation verb).\n\t// Planning-shaped questions are the exception: a plan steers expensive downstream work, so the\n\t// floor is medium even when phrased as a question.\n\tif (READ_ONLY_QUESTION_RE.test(text) && !EXPLICIT_MODIFY_REQUEST_RE.test(text)) {\n\t\tif (isPlanningPrompt(text)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.9,\n\t\t\treasonCode: \"read_only_question\",\n\t\t\treasons: [\"Prompt asks a question or requests an explanation, search, or lookup\"],\n\t\t};\n\t}\n\n\t// Helper function to match patterns and return appropriate decision\n\tfunction matchKeywords(input: string): RouteDecision | null {\n\t\t// A. High-risk / approval-required/expensive signals\n\t\tif (RELEASE_PUBLISH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"release_or_publish\",\n\t\t\t\treasons: [\"Prompt mentions publishing, releasing, pushing, or deploying\"],\n\t\t\t};\n\t\t}\n\t\tif (SECURITY_AUTH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.95,\n\t\t\t\treasonCode: \"security_or_auth\",\n\t\t\t\treasons: [\"Prompt mentions credentials, authentication, tokens, or secrets\"],\n\t\t\t};\n\t\t}\n\t\tif (DESTRUCTIVE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"destructive_or_git_history\",\n\t\t\t\treasons: [\"Prompt mentions deleting, resetting, cleaning, or destructive operations\"],\n\t\t\t};\n\t\t}\n\t\tif (SELF_MOD_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"settings_or_self_modification\",\n\t\t\t\treasons: [\"Prompt mentions modifying skills, prompts, settings, tools, or self-modification\"],\n\t\t\t};\n\t\t}\n\t\tif (ARCHITECTURE_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"architecture_or_ambiguous\",\n\t\t\t\treasons: [\"Prompt mentions core architecture or rewrite\"],\n\t\t\t};\n\t\t}\n\n\t\t// B. Explicit implementation/scoped-write signals route medium\n\t\tif (isPlanningPrompt(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\tif (REFACTOR_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"mechanical_refactor\",\n\t\t\t\treasons: [\"Prompt mentions refactoring code structure\"],\n\t\t\t};\n\t\t}\n\t\tif (TEST_VALIDATION_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"test_or_validation\",\n\t\t\t\treasons: [\"Prompt mentions testing, validation, or linting\"],\n\t\t\t};\n\t\t}\n\t\tif (IMPLEMENT_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"normal_implementation\",\n\t\t\t\treasons: [\"Prompt mentions implementing, updating, creating, or modifying code\"],\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tconst match = matchKeywords(text);\n\tif (match) {\n\t\treturn match;\n\t}\n\n\t// 4. Default fallbacks\n\treturn {\n\t\ttier: \"cheap\",\n\t\trisk: \"read-only\",\n\t\tconfidence: 0.5,\n\t\treasonCode: \"default_read_only\",\n\t\treasons: [\"No explicit implementation, destructive, or release patterns detected\"],\n\t};\n}\n\nexport function classifyModelRouterIntent(prompt: string): ModelRouterIntent {\n\tconst decision = classifyModelRouterRoute(prompt);\n\treturn decision.tier === \"cheap\" ? \"research\" : \"modify\";\n}\n"]}
1
+ {"version":3,"file":"intent-classifier.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/intent-classifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAiCtD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CA0ItE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAG3E","sourcesContent":["import type { RouteDecision } from \"../autonomy/contracts.ts\";\n\nexport type ModelRouterIntent = \"research\" | \"modify\";\n\nconst EXPLICIT_MODIFY_REQUEST_RE =\n\t/^(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+.*\\b(add|apply|build|change|commit|create|delete|edit|fix|generate|implement|install|modify|patch|refactor|remove|rename|replace|run|test|update|write|publish|release|push|deploy|tag|reset|clean|rewrite)\\b/i;\n\nconst READ_ONLY_QUESTION_RE =\n\t/^(?:(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+)?(?:how|what|why|when|where|which|who|explain|summarize|compare|describe|list|show|search|find|view|read|locate)\\b/i;\n\nconst RELEASE_PUBLISH_RE = /\\b(publish|release|push|deploy|tag)\\b/i;\nconst SECURITY_AUTH_RE = /\\b(auth|token|credential|credentials|secret|api[-_]key)\\b/i;\nconst DESTRUCTIVE_RE = /\\b(delete|reset|rm\\s+-rf|clean)\\b/i;\n\nconst SELF_MOD_MUTATE_RE =\n\t/\\b(modify|change|write|update|edit|delete|add|remove)\\s+.*\\b(skills|prompts|settings|tools|behavior)\\b|self[-_]modification/i;\nconst ARCHITECTURE_MUTATE_RE = /\\b(rewrite|redesign|change|modify|rearchitect)\\s+.*\\b(architecture|architect)\\b/i;\n\n// Planning floor: plans steer all downstream work, so planning never routes cheap by default.\n// Only the route judge may downgrade a planning prompt back to cheap (explicit trivial verdict).\n// Core terms are always planning; design/architecture words count only with prospective phrasing,\n// so lookups like \"show me the architecture\" stay cheap.\nconst PLANNING_CORE_RE = /\\b(plan|planning|roadmap|strategy)\\b/i;\nconst PLANNING_DESIGN_WORD_RE = /\\b(design|architect\\w*|structure|approach)\\b/i;\nconst PLANNING_PROSPECTIVE_RE =\n\t/\\b(how (?:should|would|do we|can we)|what(?:'s| is) the (?:best|cleanest|right)|propose|draft|come up with|figure out|decide (?:on|how))\\b/i;\n\nfunction isPlanningPrompt(text: string): boolean {\n\treturn PLANNING_CORE_RE.test(text) || (PLANNING_DESIGN_WORD_RE.test(text) && PLANNING_PROSPECTIVE_RE.test(text));\n}\n\nconst REFACTOR_RE = /\\b(refactor|refactoring)\\b/i;\nconst TEST_VALIDATION_RE = /\\b(test|testing|validation|lint|vitest|jest|run)\\b/i;\nconst IMPLEMENT_RE = /\\b(implement|fix|apply|change|update|create|write|generate|modify|edit|patch|add)\\b/i;\n\nexport function classifyModelRouterRoute(prompt: string): RouteDecision {\n\tconst text = prompt.trim();\n\n\tif (text.length === 0) {\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.1,\n\t\t\treasonCode: \"empty_prompt\",\n\t\t\treasons: [\"Empty or whitespace prompt\"],\n\t\t};\n\t}\n\n\t// 1. Explicit read-only questions/lookups dominate (unless prefixed by explicit mutation verb).\n\t// Planning-shaped questions are the exception: a plan steers expensive downstream work, so the\n\t// floor is medium even when phrased as a question.\n\tif (READ_ONLY_QUESTION_RE.test(text) && !EXPLICIT_MODIFY_REQUEST_RE.test(text)) {\n\t\tif (isPlanningPrompt(text)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.9,\n\t\t\treasonCode: \"read_only_question\",\n\t\t\treasons: [\"Prompt asks a question or requests an explanation, search, or lookup\"],\n\t\t};\n\t}\n\n\t// Helper function to match patterns and return appropriate decision\n\tfunction matchKeywords(input: string): RouteDecision | null {\n\t\t// A. High-risk / approval-required/expensive signals\n\t\tif (RELEASE_PUBLISH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"release_or_publish\",\n\t\t\t\treasons: [\"Prompt mentions publishing, releasing, pushing, or deploying\"],\n\t\t\t};\n\t\t}\n\t\tif (SECURITY_AUTH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.95,\n\t\t\t\treasonCode: \"security_or_auth\",\n\t\t\t\treasons: [\"Prompt mentions credentials, authentication, tokens, or secrets\"],\n\t\t\t};\n\t\t}\n\t\tif (DESTRUCTIVE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"destructive_or_git_history\",\n\t\t\t\treasons: [\"Prompt mentions deleting, resetting, cleaning, or destructive operations\"],\n\t\t\t};\n\t\t}\n\t\tif (SELF_MOD_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"settings_or_self_modification\",\n\t\t\t\treasons: [\"Prompt mentions modifying skills, prompts, settings, tools, or self-modification\"],\n\t\t\t};\n\t\t}\n\t\tif (ARCHITECTURE_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"architecture_or_ambiguous\",\n\t\t\t\treasons: [\"Prompt mentions core architecture or rewrite\"],\n\t\t\t};\n\t\t}\n\n\t\t// B. Explicit implementation/scoped-write signals route medium\n\t\tif (isPlanningPrompt(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\tif (REFACTOR_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"mechanical_refactor\",\n\t\t\t\treasons: [\"Prompt mentions refactoring code structure\"],\n\t\t\t};\n\t\t}\n\t\tif (TEST_VALIDATION_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"test_or_validation\",\n\t\t\t\treasons: [\"Prompt mentions testing, validation, or linting\"],\n\t\t\t};\n\t\t}\n\t\tif (IMPLEMENT_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"normal_implementation\",\n\t\t\t\treasons: [\"Prompt mentions implementing, updating, creating, or modifying code\"],\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tconst match = matchKeywords(text);\n\tif (match) {\n\t\treturn match;\n\t}\n\n\t// 4. Default fallbacks\n\treturn {\n\t\ttier: \"cheap\",\n\t\trisk: \"read-only\",\n\t\tconfidence: 0.5,\n\t\treasonCode: \"default_read_only\",\n\t\treasons: [\"No explicit implementation, destructive, or release patterns detected\"],\n\t};\n}\n\nexport function classifyModelRouterIntent(prompt: string): ModelRouterIntent {\n\tconst decision = classifyModelRouterRoute(prompt);\n\treturn decision.tier === \"cheap\" ? \"research\" : \"modify\";\n}\n"]}
@@ -11,7 +11,7 @@ const ARCHITECTURE_MUTATE_RE = /\b(rewrite|redesign|change|modify|rearchitect)\s
11
11
  // so lookups like "show me the architecture" stay cheap.
12
12
  const PLANNING_CORE_RE = /\b(plan|planning|roadmap|strategy)\b/i;
13
13
  const PLANNING_DESIGN_WORD_RE = /\b(design|architect\w*|structure|approach)\b/i;
14
- const PLANNING_PROSPECTIVE_RE = /\b(how (?:should|would|do we|can we)|what(?:'s| is) the best|propose|draft|come up with|figure out|decide (?:on|how))\b/i;
14
+ const PLANNING_PROSPECTIVE_RE = /\b(how (?:should|would|do we|can we)|what(?:'s| is) the (?:best|cleanest|right)|propose|draft|come up with|figure out|decide (?:on|how))\b/i;
15
15
  function isPlanningPrompt(text) {
16
16
  return PLANNING_CORE_RE.test(text) || (PLANNING_DESIGN_WORD_RE.test(text) && PLANNING_PROSPECTIVE_RE.test(text));
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"intent-classifier.js","sourceRoot":"","sources":["../../../src/core/model-router/intent-classifier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RouteDecision } from \"../autonomy/contracts.ts\";\n\nexport type ModelRouterIntent = \"research\" | \"modify\";\n\nconst EXPLICIT_MODIFY_REQUEST_RE =\n\t/^(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+.*\\b(add|apply|build|change|commit|create|delete|edit|fix|generate|implement|install|modify|patch|refactor|remove|rename|replace|run|test|update|write|publish|release|push|deploy|tag|reset|clean|rewrite)\\b/i;\n\nconst READ_ONLY_QUESTION_RE =\n\t/^(?:(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+)?(?:how|what|why|when|where|which|who|explain|summarize|compare|describe|list|show|search|find|view|read|locate)\\b/i;\n\nconst RELEASE_PUBLISH_RE = /\\b(publish|release|push|deploy|tag)\\b/i;\nconst SECURITY_AUTH_RE = /\\b(auth|token|credential|credentials|secret|api[-_]key)\\b/i;\nconst DESTRUCTIVE_RE = /\\b(delete|reset|rm\\s+-rf|clean)\\b/i;\n\nconst SELF_MOD_MUTATE_RE =\n\t/\\b(modify|change|write|update|edit|delete|add|remove)\\s+.*\\b(skills|prompts|settings|tools|behavior)\\b|self[-_]modification/i;\nconst ARCHITECTURE_MUTATE_RE = /\\b(rewrite|redesign|change|modify|rearchitect)\\s+.*\\b(architecture|architect)\\b/i;\n\n// Planning floor: plans steer all downstream work, so planning never routes cheap by default.\n// Only the route judge may downgrade a planning prompt back to cheap (explicit trivial verdict).\n// Core terms are always planning; design/architecture words count only with prospective phrasing,\n// so lookups like \"show me the architecture\" stay cheap.\nconst PLANNING_CORE_RE = /\\b(plan|planning|roadmap|strategy)\\b/i;\nconst PLANNING_DESIGN_WORD_RE = /\\b(design|architect\\w*|structure|approach)\\b/i;\nconst PLANNING_PROSPECTIVE_RE =\n\t/\\b(how (?:should|would|do we|can we)|what(?:'s| is) the best|propose|draft|come up with|figure out|decide (?:on|how))\\b/i;\n\nfunction isPlanningPrompt(text: string): boolean {\n\treturn PLANNING_CORE_RE.test(text) || (PLANNING_DESIGN_WORD_RE.test(text) && PLANNING_PROSPECTIVE_RE.test(text));\n}\n\nconst REFACTOR_RE = /\\b(refactor|refactoring)\\b/i;\nconst TEST_VALIDATION_RE = /\\b(test|testing|validation|lint|vitest|jest|run)\\b/i;\nconst IMPLEMENT_RE = /\\b(implement|fix|apply|change|update|create|write|generate|modify|edit|patch|add)\\b/i;\n\nexport function classifyModelRouterRoute(prompt: string): RouteDecision {\n\tconst text = prompt.trim();\n\n\tif (text.length === 0) {\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.1,\n\t\t\treasonCode: \"empty_prompt\",\n\t\t\treasons: [\"Empty or whitespace prompt\"],\n\t\t};\n\t}\n\n\t// 1. Explicit read-only questions/lookups dominate (unless prefixed by explicit mutation verb).\n\t// Planning-shaped questions are the exception: a plan steers expensive downstream work, so the\n\t// floor is medium even when phrased as a question.\n\tif (READ_ONLY_QUESTION_RE.test(text) && !EXPLICIT_MODIFY_REQUEST_RE.test(text)) {\n\t\tif (isPlanningPrompt(text)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.9,\n\t\t\treasonCode: \"read_only_question\",\n\t\t\treasons: [\"Prompt asks a question or requests an explanation, search, or lookup\"],\n\t\t};\n\t}\n\n\t// Helper function to match patterns and return appropriate decision\n\tfunction matchKeywords(input: string): RouteDecision | null {\n\t\t// A. High-risk / approval-required/expensive signals\n\t\tif (RELEASE_PUBLISH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"release_or_publish\",\n\t\t\t\treasons: [\"Prompt mentions publishing, releasing, pushing, or deploying\"],\n\t\t\t};\n\t\t}\n\t\tif (SECURITY_AUTH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.95,\n\t\t\t\treasonCode: \"security_or_auth\",\n\t\t\t\treasons: [\"Prompt mentions credentials, authentication, tokens, or secrets\"],\n\t\t\t};\n\t\t}\n\t\tif (DESTRUCTIVE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"destructive_or_git_history\",\n\t\t\t\treasons: [\"Prompt mentions deleting, resetting, cleaning, or destructive operations\"],\n\t\t\t};\n\t\t}\n\t\tif (SELF_MOD_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"settings_or_self_modification\",\n\t\t\t\treasons: [\"Prompt mentions modifying skills, prompts, settings, tools, or self-modification\"],\n\t\t\t};\n\t\t}\n\t\tif (ARCHITECTURE_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"architecture_or_ambiguous\",\n\t\t\t\treasons: [\"Prompt mentions core architecture or rewrite\"],\n\t\t\t};\n\t\t}\n\n\t\t// B. Explicit implementation/scoped-write signals route medium\n\t\tif (isPlanningPrompt(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\tif (REFACTOR_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"mechanical_refactor\",\n\t\t\t\treasons: [\"Prompt mentions refactoring code structure\"],\n\t\t\t};\n\t\t}\n\t\tif (TEST_VALIDATION_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"test_or_validation\",\n\t\t\t\treasons: [\"Prompt mentions testing, validation, or linting\"],\n\t\t\t};\n\t\t}\n\t\tif (IMPLEMENT_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"normal_implementation\",\n\t\t\t\treasons: [\"Prompt mentions implementing, updating, creating, or modifying code\"],\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tconst match = matchKeywords(text);\n\tif (match) {\n\t\treturn match;\n\t}\n\n\t// 4. Default fallbacks\n\treturn {\n\t\ttier: \"cheap\",\n\t\trisk: \"read-only\",\n\t\tconfidence: 0.5,\n\t\treasonCode: \"default_read_only\",\n\t\treasons: [\"No explicit implementation, destructive, or release patterns detected\"],\n\t};\n}\n\nexport function classifyModelRouterIntent(prompt: string): ModelRouterIntent {\n\tconst decision = classifyModelRouterRoute(prompt);\n\treturn decision.tier === \"cheap\" ? \"research\" : \"modify\";\n}\n"]}
1
+ {"version":3,"file":"intent-classifier.js","sourceRoot":"","sources":["../../../src/core/model-router/intent-classifier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RouteDecision } from \"../autonomy/contracts.ts\";\n\nexport type ModelRouterIntent = \"research\" | \"modify\";\n\nconst EXPLICIT_MODIFY_REQUEST_RE =\n\t/^(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+.*\\b(add|apply|build|change|commit|create|delete|edit|fix|generate|implement|install|modify|patch|refactor|remove|rename|replace|run|test|update|write|publish|release|push|deploy|tag|reset|clean|rewrite)\\b/i;\n\nconst READ_ONLY_QUESTION_RE =\n\t/^(?:(?:can you|could you|please|pls|go ahead and|let'?s|i need you to|we need to|you should)\\s+)?(?:how|what|why|when|where|which|who|explain|summarize|compare|describe|list|show|search|find|view|read|locate)\\b/i;\n\nconst RELEASE_PUBLISH_RE = /\\b(publish|release|push|deploy|tag)\\b/i;\nconst SECURITY_AUTH_RE = /\\b(auth|token|credential|credentials|secret|api[-_]key)\\b/i;\nconst DESTRUCTIVE_RE = /\\b(delete|reset|rm\\s+-rf|clean)\\b/i;\n\nconst SELF_MOD_MUTATE_RE =\n\t/\\b(modify|change|write|update|edit|delete|add|remove)\\s+.*\\b(skills|prompts|settings|tools|behavior)\\b|self[-_]modification/i;\nconst ARCHITECTURE_MUTATE_RE = /\\b(rewrite|redesign|change|modify|rearchitect)\\s+.*\\b(architecture|architect)\\b/i;\n\n// Planning floor: plans steer all downstream work, so planning never routes cheap by default.\n// Only the route judge may downgrade a planning prompt back to cheap (explicit trivial verdict).\n// Core terms are always planning; design/architecture words count only with prospective phrasing,\n// so lookups like \"show me the architecture\" stay cheap.\nconst PLANNING_CORE_RE = /\\b(plan|planning|roadmap|strategy)\\b/i;\nconst PLANNING_DESIGN_WORD_RE = /\\b(design|architect\\w*|structure|approach)\\b/i;\nconst PLANNING_PROSPECTIVE_RE =\n\t/\\b(how (?:should|would|do we|can we)|what(?:'s| is) the (?:best|cleanest|right)|propose|draft|come up with|figure out|decide (?:on|how))\\b/i;\n\nfunction isPlanningPrompt(text: string): boolean {\n\treturn PLANNING_CORE_RE.test(text) || (PLANNING_DESIGN_WORD_RE.test(text) && PLANNING_PROSPECTIVE_RE.test(text));\n}\n\nconst REFACTOR_RE = /\\b(refactor|refactoring)\\b/i;\nconst TEST_VALIDATION_RE = /\\b(test|testing|validation|lint|vitest|jest|run)\\b/i;\nconst IMPLEMENT_RE = /\\b(implement|fix|apply|change|update|create|write|generate|modify|edit|patch|add)\\b/i;\n\nexport function classifyModelRouterRoute(prompt: string): RouteDecision {\n\tconst text = prompt.trim();\n\n\tif (text.length === 0) {\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.1,\n\t\t\treasonCode: \"empty_prompt\",\n\t\t\treasons: [\"Empty or whitespace prompt\"],\n\t\t};\n\t}\n\n\t// 1. Explicit read-only questions/lookups dominate (unless prefixed by explicit mutation verb).\n\t// Planning-shaped questions are the exception: a plan steers expensive downstream work, so the\n\t// floor is medium even when phrased as a question.\n\tif (READ_ONLY_QUESTION_RE.test(text) && !EXPLICIT_MODIFY_REQUEST_RE.test(text)) {\n\t\tif (isPlanningPrompt(text)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttier: \"cheap\",\n\t\t\trisk: \"read-only\",\n\t\t\tconfidence: 0.9,\n\t\t\treasonCode: \"read_only_question\",\n\t\t\treasons: [\"Prompt asks a question or requests an explanation, search, or lookup\"],\n\t\t};\n\t}\n\n\t// Helper function to match patterns and return appropriate decision\n\tfunction matchKeywords(input: string): RouteDecision | null {\n\t\t// A. High-risk / approval-required/expensive signals\n\t\tif (RELEASE_PUBLISH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"release_or_publish\",\n\t\t\t\treasons: [\"Prompt mentions publishing, releasing, pushing, or deploying\"],\n\t\t\t};\n\t\t}\n\t\tif (SECURITY_AUTH_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.95,\n\t\t\t\treasonCode: \"security_or_auth\",\n\t\t\t\treasons: [\"Prompt mentions credentials, authentication, tokens, or secrets\"],\n\t\t\t};\n\t\t}\n\t\tif (DESTRUCTIVE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"destructive_or_git_history\",\n\t\t\t\treasons: [\"Prompt mentions deleting, resetting, cleaning, or destructive operations\"],\n\t\t\t};\n\t\t}\n\t\tif (SELF_MOD_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"approval-required\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"settings_or_self_modification\",\n\t\t\t\treasons: [\"Prompt mentions modifying skills, prompts, settings, tools, or self-modification\"],\n\t\t\t};\n\t\t}\n\t\tif (ARCHITECTURE_MUTATE_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"expensive\",\n\t\t\t\trisk: \"high-impact\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treasonCode: \"architecture_or_ambiguous\",\n\t\t\t\treasons: [\"Prompt mentions core architecture or rewrite\"],\n\t\t\t};\n\t\t}\n\n\t\t// B. Explicit implementation/scoped-write signals route medium\n\t\tif (isPlanningPrompt(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"read-only\",\n\t\t\t\tconfidence: 0.75,\n\t\t\t\treasonCode: \"planning_min_medium\",\n\t\t\t\treasons: [\"Planning/design prompts never route cheap by default; a judge may deem them trivial\"],\n\t\t\t};\n\t\t}\n\t\tif (REFACTOR_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"mechanical_refactor\",\n\t\t\t\treasons: [\"Prompt mentions refactoring code structure\"],\n\t\t\t};\n\t\t}\n\t\tif (TEST_VALIDATION_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.8,\n\t\t\t\treasonCode: \"test_or_validation\",\n\t\t\t\treasons: [\"Prompt mentions testing, validation, or linting\"],\n\t\t\t};\n\t\t}\n\t\tif (IMPLEMENT_RE.test(input)) {\n\t\t\treturn {\n\t\t\t\ttier: \"medium\",\n\t\t\t\trisk: \"scoped-write\",\n\t\t\t\tconfidence: 0.85,\n\t\t\t\treasonCode: \"normal_implementation\",\n\t\t\t\treasons: [\"Prompt mentions implementing, updating, creating, or modifying code\"],\n\t\t\t};\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tconst match = matchKeywords(text);\n\tif (match) {\n\t\treturn match;\n\t}\n\n\t// 4. Default fallbacks\n\treturn {\n\t\ttier: \"cheap\",\n\t\trisk: \"read-only\",\n\t\tconfidence: 0.5,\n\t\treasonCode: \"default_read_only\",\n\t\treasons: [\"No explicit implementation, destructive, or release patterns detected\"],\n\t};\n}\n\nexport function classifyModelRouterIntent(prompt: string): ModelRouterIntent {\n\tconst decision = classifyModelRouterRoute(prompt);\n\treturn decision.tier === \"cheap\" ? \"research\" : \"modify\";\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"route-judge.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/route-judge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;;;;;GAOG;AAEH,uGAAqG;AACrG,eAAO,MAAM,yBAAyB,QAS1B,CAAC;AAEb,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;IACvC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GAAG,MAAM,CAMnG;AAKD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAiClF;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAczG;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC;QAChG,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAyC/B","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { RouteDecision } from \"../autonomy/contracts.ts\";\n\n/**\n * Routing-only judge: a bounded, tool-less completion (default: the medium model) that decides the\n * final cheap/medium/expensive tier for a user prompt, refining the regex classifier's baseline.\n * Core rule: planning is never cheap unless the judge explicitly deems the task trivial. The judge\n * proposes; the existing pipeline (model resolution, auth, escalation, gates) still decides.\n * Failure is honest: unparseable/timeout/unavailable falls back to the baseline with a visible\n * reasonCode, never silently.\n */\n\n/** Static across calls — callers pass cacheRetention \"short\" so only the variable tail is billed. */\nexport const ROUTE_JUDGE_SYSTEM_PROMPT = [\n\t\"You are a routing judge for a coding agent. You only route; you never answer the task.\",\n\t\"Pick which model tier should handle the user prompt:\",\n\t'- \"cheap\": trivial, mechanical, read-only lookups only.',\n\t'- \"medium\": normal implementation, scoped edits, tests, and NON-trivial planning/design.',\n\t'- \"expensive\": architecture, ambiguity, security/auth, destructive or release operations, high-impact changes.',\n\t\"Planning, design, and strategy prompts are NEVER cheap unless the task is genuinely trivial.\",\n\t\"Respond with STRICT JSON only - no prose:\",\n\t'{\"tier\":\"cheap\"|\"medium\"|\"expensive\",\"risk\":\"read-only\"|\"scoped-write\"|\"high-impact\"|\"approval-required\",\"trivial\":true|false,\"reason\":\"<short reason>\"}',\n].join(\"\\n\");\n\nexport const ROUTE_JUDGE_MAX_OUTPUT_TOKENS = 128;\nexport const ROUTE_JUDGE_MAX_WALL_CLOCK_MS = 10_000;\n\nexport interface RouteJudgeVerdict {\n\ttier: \"cheap\" | \"medium\" | \"expensive\";\n\trisk: RouteDecision[\"risk\"];\n\ttrivial: boolean;\n\treason: string;\n}\n\nexport function buildRouteJudgeUserPrompt(args: { prompt: string; baseline: RouteDecision }): string {\n\treturn [\n\t\t`Baseline (regex) verdict: tier=${args.baseline.tier}, risk=${args.baseline.risk}, reason=${args.baseline.reasonCode}.`,\n\t\t\"User prompt:\",\n\t\targs.prompt.slice(0, 4000),\n\t].join(\"\\n\");\n}\n\nconst JUDGE_TIERS: readonly string[] = [\"cheap\", \"medium\", \"expensive\"];\nconst JUDGE_RISKS: readonly string[] = [\"read-only\", \"scoped-write\", \"high-impact\", \"approval-required\"];\n\nexport function parseRouteJudgeVerdict(text: string): RouteJudgeVerdict | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\t// The judge may never select the learning tier or anything outside the three foreground tiers.\n\t\tif (typeof record.tier !== \"string\" || !JUDGE_TIERS.includes(record.tier)) continue;\n\t\tconst risk =\n\t\t\ttypeof record.risk === \"string\" && JUDGE_RISKS.includes(record.risk)\n\t\t\t\t? (record.risk as RouteDecision[\"risk\"])\n\t\t\t\t: undefined;\n\t\tif (!risk) continue;\n\t\treturn {\n\t\t\ttier: record.tier as RouteJudgeVerdict[\"tier\"],\n\t\t\trisk,\n\t\t\ttrivial: record.trivial === true,\n\t\t\treason: typeof record.reason === \"string\" ? record.reason.slice(0, 200) : \"\",\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Merge a judge verdict into the baseline decision (pure; never returns learning). */\nexport function applyRouteJudgeVerdict(baseline: RouteDecision, verdict: RouteJudgeVerdict): RouteDecision {\n\t// Enforce the core rule in code, not just in the judge prompt: downgrading a non-cheap\n\t// baseline to cheap requires an EXPLICIT trivial verdict. An untrusted judge saying\n\t// {tier:\"cheap\", trivial:false} for an elevated prompt keeps the baseline tier.\n\tconst tier =\n\t\tverdict.tier === \"cheap\" && baseline.tier !== \"cheap\" && !verdict.trivial ? baseline.tier : verdict.tier;\n\treturn {\n\t\t...baseline,\n\t\ttier,\n\t\trisk: verdict.risk,\n\t\tconfidence: Math.max(baseline.confidence, 0.75),\n\t\treasonCode: `judge_${tier}${verdict.trivial ? \"_trivial\" : \"\"}`,\n\t\treasons: [...baseline.reasons, `Route judge: ${verdict.reason || \"no reason given\"}`],\n\t};\n}\n\nexport interface RouteJudgeRunResult {\n\tdecision: RouteDecision;\n\tverdict?: RouteJudgeVerdict;\n\t/** Set when the judge could not decide and the baseline was kept. */\n\tfallbackReason?: string;\n\tcostUsd: number;\n}\n\n/**\n * Run the judge over a baseline decision. The completion executor is injected (production:\n * AgentSession.runIsolatedCompletion on the judge model). Never throws; every failure keeps the\n * baseline with a visible fallbackReason.\n */\nexport async function runRouteJudge(args: {\n\tprompt: string;\n\tbaseline: RouteDecision;\n\tcomplete: (input: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<{\n\t\ttext: string;\n\t\tcostUsd: number;\n\t\tstopReason: string;\n\t}>;\n\tsignal?: AbortSignal;\n\tmaxWallClockMs?: number;\n}): Promise<RouteJudgeRunResult> {\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: args.maxWallClockMs ?? ROUTE_JUDGE_MAX_WALL_CLOCK_MS,\n\t\tsignal: args.signal,\n\t\texecute: (signal) =>\n\t\t\targs.complete({\n\t\t\t\tsystemPrompt: ROUTE_JUDGE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildRouteJudgeUserPrompt({ prompt: args.prompt, baseline: args.baseline }),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure || !bounded.completion) {\n\t\treturn {\n\t\t\tdecision: {\n\t\t\t\t...args.baseline,\n\t\t\t\treasons: [...args.baseline.reasons, \"Route judge unavailable; baseline kept\"],\n\t\t\t},\n\t\t\tfallbackReason: bounded.failure ? `judge_${bounded.failure.reasonCode}` : \"judge_unavailable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\tif (bounded.completion.stopReason === \"error\" || bounded.completion.stopReason === \"aborted\") {\n\t\treturn {\n\t\t\tdecision: { ...args.baseline, reasons: [...args.baseline.reasons, \"Route judge errored; baseline kept\"] },\n\t\t\tfallbackReason: \"judge_model_error\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\tconst verdict = parseRouteJudgeVerdict(bounded.completion.text);\n\tif (!verdict) {\n\t\treturn {\n\t\t\tdecision: { ...args.baseline, reasons: [...args.baseline.reasons, \"Route judge unparseable; baseline kept\"] },\n\t\t\tfallbackReason: \"judge_unparseable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\treturn { decision: applyRouteJudgeVerdict(args.baseline, verdict), verdict, costUsd };\n}\n"]}
1
+ {"version":3,"file":"route-judge.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/route-judge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;;;;;GAOG;AAEH,uGAAqG;AACrG,eAAO,MAAM,yBAAyB,QAS1B,CAAC;AAEb,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;IACvC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GAAG,MAAM,CAMnG;AAKD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAiClF;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAczG;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CAChB;AAUD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC;QAChG,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsC/B","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { RouteDecision } from \"../autonomy/contracts.ts\";\n\n/**\n * Routing-only judge: a bounded, tool-less completion (default: the medium model) that decides the\n * final cheap/medium/expensive tier for a user prompt, refining the regex classifier's baseline.\n * Core rule: planning is never cheap unless the judge explicitly deems the task trivial. The judge\n * proposes; the existing pipeline (model resolution, auth, escalation, gates) still decides.\n * Failure is honest: unparseable/timeout/unavailable falls back to the baseline with a visible\n * reasonCode, never silently.\n */\n\n/** Static across calls — callers pass cacheRetention \"short\" so only the variable tail is billed. */\nexport const ROUTE_JUDGE_SYSTEM_PROMPT = [\n\t\"You are a routing judge for a coding agent. You only route; you never answer the task.\",\n\t\"Pick which model tier should handle the user prompt:\",\n\t'- \"cheap\": trivial, mechanical, read-only lookups only.',\n\t'- \"medium\": normal implementation, scoped edits, tests, and NON-trivial planning/design.',\n\t'- \"expensive\": architecture, ambiguity, security/auth, destructive or release operations, high-impact changes.',\n\t\"Planning, design, and strategy prompts are NEVER cheap unless the task is genuinely trivial.\",\n\t\"Respond with STRICT JSON only - no prose:\",\n\t'{\"tier\":\"cheap\"|\"medium\"|\"expensive\",\"risk\":\"read-only\"|\"scoped-write\"|\"high-impact\"|\"approval-required\",\"trivial\":true|false,\"reason\":\"<short reason>\"}',\n].join(\"\\n\");\n\nexport const ROUTE_JUDGE_MAX_OUTPUT_TOKENS = 128;\nexport const ROUTE_JUDGE_MAX_WALL_CLOCK_MS = 10_000;\n\nexport interface RouteJudgeVerdict {\n\ttier: \"cheap\" | \"medium\" | \"expensive\";\n\trisk: RouteDecision[\"risk\"];\n\ttrivial: boolean;\n\treason: string;\n}\n\nexport function buildRouteJudgeUserPrompt(args: { prompt: string; baseline: RouteDecision }): string {\n\treturn [\n\t\t`Baseline (regex) verdict: tier=${args.baseline.tier}, risk=${args.baseline.risk}, reason=${args.baseline.reasonCode}.`,\n\t\t\"User prompt:\",\n\t\targs.prompt.slice(0, 4000),\n\t].join(\"\\n\");\n}\n\nconst JUDGE_TIERS: readonly string[] = [\"cheap\", \"medium\", \"expensive\"];\nconst JUDGE_RISKS: readonly string[] = [\"read-only\", \"scoped-write\", \"high-impact\", \"approval-required\"];\n\nexport function parseRouteJudgeVerdict(text: string): RouteJudgeVerdict | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\t// The judge may never select the learning tier or anything outside the three foreground tiers.\n\t\tif (typeof record.tier !== \"string\" || !JUDGE_TIERS.includes(record.tier)) continue;\n\t\tconst risk =\n\t\t\ttypeof record.risk === \"string\" && JUDGE_RISKS.includes(record.risk)\n\t\t\t\t? (record.risk as RouteDecision[\"risk\"])\n\t\t\t\t: undefined;\n\t\tif (!risk) continue;\n\t\treturn {\n\t\t\ttier: record.tier as RouteJudgeVerdict[\"tier\"],\n\t\t\trisk,\n\t\t\ttrivial: record.trivial === true,\n\t\t\treason: typeof record.reason === \"string\" ? record.reason.slice(0, 200) : \"\",\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Merge a judge verdict into the baseline decision (pure; never returns learning). */\nexport function applyRouteJudgeVerdict(baseline: RouteDecision, verdict: RouteJudgeVerdict): RouteDecision {\n\t// Enforce the core rule in code, not just in the judge prompt: downgrading a non-cheap\n\t// baseline to cheap requires an EXPLICIT trivial verdict. An untrusted judge saying\n\t// {tier:\"cheap\", trivial:false} for an elevated prompt keeps the baseline tier.\n\tconst tier =\n\t\tverdict.tier === \"cheap\" && baseline.tier !== \"cheap\" && !verdict.trivial ? baseline.tier : verdict.tier;\n\treturn {\n\t\t...baseline,\n\t\ttier,\n\t\trisk: verdict.risk,\n\t\tconfidence: Math.max(baseline.confidence, 0.75),\n\t\treasonCode: `judge_${tier}${verdict.trivial ? \"_trivial\" : \"\"}`,\n\t\treasons: [...baseline.reasons, `Route judge: ${verdict.reason || \"no reason given\"}`],\n\t};\n}\n\nexport interface RouteJudgeRunResult {\n\tdecision: RouteDecision;\n\tverdict?: RouteJudgeVerdict;\n\t/** Set when the judge could not decide and the baseline was kept. */\n\tfallbackReason?: string;\n\tcostUsd: number;\n}\n\nfunction judgeFallbackDecision(baseline: RouteDecision, reason: string): RouteDecision {\n\treturn {\n\t\t...baseline,\n\t\treasonCode: \"judge_unavailable_fallback\",\n\t\treasons: [...baseline.reasons, reason],\n\t};\n}\n\n/**\n * Run the judge over a baseline decision. The completion executor is injected (production:\n * AgentSession.runIsolatedCompletion on the judge model). Never throws; every failure keeps the\n * baseline with a visible fallbackReason.\n */\nexport async function runRouteJudge(args: {\n\tprompt: string;\n\tbaseline: RouteDecision;\n\tcomplete: (input: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<{\n\t\ttext: string;\n\t\tcostUsd: number;\n\t\tstopReason: string;\n\t}>;\n\tsignal?: AbortSignal;\n\tmaxWallClockMs?: number;\n}): Promise<RouteJudgeRunResult> {\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: args.maxWallClockMs ?? ROUTE_JUDGE_MAX_WALL_CLOCK_MS,\n\t\tsignal: args.signal,\n\t\texecute: (signal) =>\n\t\t\targs.complete({\n\t\t\t\tsystemPrompt: ROUTE_JUDGE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildRouteJudgeUserPrompt({ prompt: args.prompt, baseline: args.baseline }),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure || !bounded.completion) {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge unavailable; baseline kept\"),\n\t\t\tfallbackReason: bounded.failure ? `judge_${bounded.failure.reasonCode}` : \"judge_unavailable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\tif (bounded.completion.stopReason === \"error\" || bounded.completion.stopReason === \"aborted\") {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge errored; baseline kept\"),\n\t\t\tfallbackReason: \"judge_model_error\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\tconst verdict = parseRouteJudgeVerdict(bounded.completion.text);\n\tif (!verdict) {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge unparseable; baseline kept\"),\n\t\t\tfallbackReason: \"judge_unparseable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\treturn { decision: applyRouteJudgeVerdict(args.baseline, verdict), verdict, costUsd };\n}\n"]}
@@ -82,6 +82,13 @@ export function applyRouteJudgeVerdict(baseline, verdict) {
82
82
  reasons: [...baseline.reasons, `Route judge: ${verdict.reason || "no reason given"}`],
83
83
  };
84
84
  }
85
+ function judgeFallbackDecision(baseline, reason) {
86
+ return {
87
+ ...baseline,
88
+ reasonCode: "judge_unavailable_fallback",
89
+ reasons: [...baseline.reasons, reason],
90
+ };
91
+ }
85
92
  /**
86
93
  * Run the judge over a baseline decision. The completion executor is injected (production:
87
94
  * AgentSession.runIsolatedCompletion on the judge model). Never throws; every failure keeps the
@@ -100,17 +107,14 @@ export async function runRouteJudge(args) {
100
107
  const costUsd = bounded.completion?.costUsd ?? 0;
101
108
  if (bounded.failure || !bounded.completion) {
102
109
  return {
103
- decision: {
104
- ...args.baseline,
105
- reasons: [...args.baseline.reasons, "Route judge unavailable; baseline kept"],
106
- },
110
+ decision: judgeFallbackDecision(args.baseline, "Route judge unavailable; baseline kept"),
107
111
  fallbackReason: bounded.failure ? `judge_${bounded.failure.reasonCode}` : "judge_unavailable_fallback",
108
112
  costUsd,
109
113
  };
110
114
  }
111
115
  if (bounded.completion.stopReason === "error" || bounded.completion.stopReason === "aborted") {
112
116
  return {
113
- decision: { ...args.baseline, reasons: [...args.baseline.reasons, "Route judge errored; baseline kept"] },
117
+ decision: judgeFallbackDecision(args.baseline, "Route judge errored; baseline kept"),
114
118
  fallbackReason: "judge_model_error",
115
119
  costUsd,
116
120
  };
@@ -118,7 +122,7 @@ export async function runRouteJudge(args) {
118
122
  const verdict = parseRouteJudgeVerdict(bounded.completion.text);
119
123
  if (!verdict) {
120
124
  return {
121
- decision: { ...args.baseline, reasons: [...args.baseline.reasons, "Route judge unparseable; baseline kept"] },
125
+ decision: judgeFallbackDecision(args.baseline, "Route judge unparseable; baseline kept"),
122
126
  fallbackReason: "judge_unparseable_fallback",
123
127
  costUsd,
124
128
  };
@@ -1 +1 @@
1
- {"version":3,"file":"route-judge.js","sourceRoot":"","sources":["../../../src/core/model-router/route-judge.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { RouteDecision } from \"../autonomy/contracts.ts\";\n\n/**\n * Routing-only judge: a bounded, tool-less completion (default: the medium model) that decides the\n * final cheap/medium/expensive tier for a user prompt, refining the regex classifier's baseline.\n * Core rule: planning is never cheap unless the judge explicitly deems the task trivial. The judge\n * proposes; the existing pipeline (model resolution, auth, escalation, gates) still decides.\n * Failure is honest: unparseable/timeout/unavailable falls back to the baseline with a visible\n * reasonCode, never silently.\n */\n\n/** Static across calls — callers pass cacheRetention \"short\" so only the variable tail is billed. */\nexport const ROUTE_JUDGE_SYSTEM_PROMPT = [\n\t\"You are a routing judge for a coding agent. You only route; you never answer the task.\",\n\t\"Pick which model tier should handle the user prompt:\",\n\t'- \"cheap\": trivial, mechanical, read-only lookups only.',\n\t'- \"medium\": normal implementation, scoped edits, tests, and NON-trivial planning/design.',\n\t'- \"expensive\": architecture, ambiguity, security/auth, destructive or release operations, high-impact changes.',\n\t\"Planning, design, and strategy prompts are NEVER cheap unless the task is genuinely trivial.\",\n\t\"Respond with STRICT JSON only - no prose:\",\n\t'{\"tier\":\"cheap\"|\"medium\"|\"expensive\",\"risk\":\"read-only\"|\"scoped-write\"|\"high-impact\"|\"approval-required\",\"trivial\":true|false,\"reason\":\"<short reason>\"}',\n].join(\"\\n\");\n\nexport const ROUTE_JUDGE_MAX_OUTPUT_TOKENS = 128;\nexport const ROUTE_JUDGE_MAX_WALL_CLOCK_MS = 10_000;\n\nexport interface RouteJudgeVerdict {\n\ttier: \"cheap\" | \"medium\" | \"expensive\";\n\trisk: RouteDecision[\"risk\"];\n\ttrivial: boolean;\n\treason: string;\n}\n\nexport function buildRouteJudgeUserPrompt(args: { prompt: string; baseline: RouteDecision }): string {\n\treturn [\n\t\t`Baseline (regex) verdict: tier=${args.baseline.tier}, risk=${args.baseline.risk}, reason=${args.baseline.reasonCode}.`,\n\t\t\"User prompt:\",\n\t\targs.prompt.slice(0, 4000),\n\t].join(\"\\n\");\n}\n\nconst JUDGE_TIERS: readonly string[] = [\"cheap\", \"medium\", \"expensive\"];\nconst JUDGE_RISKS: readonly string[] = [\"read-only\", \"scoped-write\", \"high-impact\", \"approval-required\"];\n\nexport function parseRouteJudgeVerdict(text: string): RouteJudgeVerdict | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\t// The judge may never select the learning tier or anything outside the three foreground tiers.\n\t\tif (typeof record.tier !== \"string\" || !JUDGE_TIERS.includes(record.tier)) continue;\n\t\tconst risk =\n\t\t\ttypeof record.risk === \"string\" && JUDGE_RISKS.includes(record.risk)\n\t\t\t\t? (record.risk as RouteDecision[\"risk\"])\n\t\t\t\t: undefined;\n\t\tif (!risk) continue;\n\t\treturn {\n\t\t\ttier: record.tier as RouteJudgeVerdict[\"tier\"],\n\t\t\trisk,\n\t\t\ttrivial: record.trivial === true,\n\t\t\treason: typeof record.reason === \"string\" ? record.reason.slice(0, 200) : \"\",\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Merge a judge verdict into the baseline decision (pure; never returns learning). */\nexport function applyRouteJudgeVerdict(baseline: RouteDecision, verdict: RouteJudgeVerdict): RouteDecision {\n\t// Enforce the core rule in code, not just in the judge prompt: downgrading a non-cheap\n\t// baseline to cheap requires an EXPLICIT trivial verdict. An untrusted judge saying\n\t// {tier:\"cheap\", trivial:false} for an elevated prompt keeps the baseline tier.\n\tconst tier =\n\t\tverdict.tier === \"cheap\" && baseline.tier !== \"cheap\" && !verdict.trivial ? baseline.tier : verdict.tier;\n\treturn {\n\t\t...baseline,\n\t\ttier,\n\t\trisk: verdict.risk,\n\t\tconfidence: Math.max(baseline.confidence, 0.75),\n\t\treasonCode: `judge_${tier}${verdict.trivial ? \"_trivial\" : \"\"}`,\n\t\treasons: [...baseline.reasons, `Route judge: ${verdict.reason || \"no reason given\"}`],\n\t};\n}\n\nexport interface RouteJudgeRunResult {\n\tdecision: RouteDecision;\n\tverdict?: RouteJudgeVerdict;\n\t/** Set when the judge could not decide and the baseline was kept. */\n\tfallbackReason?: string;\n\tcostUsd: number;\n}\n\n/**\n * Run the judge over a baseline decision. The completion executor is injected (production:\n * AgentSession.runIsolatedCompletion on the judge model). Never throws; every failure keeps the\n * baseline with a visible fallbackReason.\n */\nexport async function runRouteJudge(args: {\n\tprompt: string;\n\tbaseline: RouteDecision;\n\tcomplete: (input: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<{\n\t\ttext: string;\n\t\tcostUsd: number;\n\t\tstopReason: string;\n\t}>;\n\tsignal?: AbortSignal;\n\tmaxWallClockMs?: number;\n}): Promise<RouteJudgeRunResult> {\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: args.maxWallClockMs ?? ROUTE_JUDGE_MAX_WALL_CLOCK_MS,\n\t\tsignal: args.signal,\n\t\texecute: (signal) =>\n\t\t\targs.complete({\n\t\t\t\tsystemPrompt: ROUTE_JUDGE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildRouteJudgeUserPrompt({ prompt: args.prompt, baseline: args.baseline }),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure || !bounded.completion) {\n\t\treturn {\n\t\t\tdecision: {\n\t\t\t\t...args.baseline,\n\t\t\t\treasons: [...args.baseline.reasons, \"Route judge unavailable; baseline kept\"],\n\t\t\t},\n\t\t\tfallbackReason: bounded.failure ? `judge_${bounded.failure.reasonCode}` : \"judge_unavailable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\tif (bounded.completion.stopReason === \"error\" || bounded.completion.stopReason === \"aborted\") {\n\t\treturn {\n\t\t\tdecision: { ...args.baseline, reasons: [...args.baseline.reasons, \"Route judge errored; baseline kept\"] },\n\t\t\tfallbackReason: \"judge_model_error\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\tconst verdict = parseRouteJudgeVerdict(bounded.completion.text);\n\tif (!verdict) {\n\t\treturn {\n\t\t\tdecision: { ...args.baseline, reasons: [...args.baseline.reasons, \"Route judge unparseable; baseline kept\"] },\n\t\t\tfallbackReason: \"judge_unparseable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\treturn { decision: applyRouteJudgeVerdict(args.baseline, verdict), verdict, costUsd };\n}\n"]}
1
+ {"version":3,"file":"route-judge.js","sourceRoot":"","sources":["../../../src/core/model-router/route-judge.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { RouteDecision } from \"../autonomy/contracts.ts\";\n\n/**\n * Routing-only judge: a bounded, tool-less completion (default: the medium model) that decides the\n * final cheap/medium/expensive tier for a user prompt, refining the regex classifier's baseline.\n * Core rule: planning is never cheap unless the judge explicitly deems the task trivial. The judge\n * proposes; the existing pipeline (model resolution, auth, escalation, gates) still decides.\n * Failure is honest: unparseable/timeout/unavailable falls back to the baseline with a visible\n * reasonCode, never silently.\n */\n\n/** Static across calls — callers pass cacheRetention \"short\" so only the variable tail is billed. */\nexport const ROUTE_JUDGE_SYSTEM_PROMPT = [\n\t\"You are a routing judge for a coding agent. You only route; you never answer the task.\",\n\t\"Pick which model tier should handle the user prompt:\",\n\t'- \"cheap\": trivial, mechanical, read-only lookups only.',\n\t'- \"medium\": normal implementation, scoped edits, tests, and NON-trivial planning/design.',\n\t'- \"expensive\": architecture, ambiguity, security/auth, destructive or release operations, high-impact changes.',\n\t\"Planning, design, and strategy prompts are NEVER cheap unless the task is genuinely trivial.\",\n\t\"Respond with STRICT JSON only - no prose:\",\n\t'{\"tier\":\"cheap\"|\"medium\"|\"expensive\",\"risk\":\"read-only\"|\"scoped-write\"|\"high-impact\"|\"approval-required\",\"trivial\":true|false,\"reason\":\"<short reason>\"}',\n].join(\"\\n\");\n\nexport const ROUTE_JUDGE_MAX_OUTPUT_TOKENS = 128;\nexport const ROUTE_JUDGE_MAX_WALL_CLOCK_MS = 10_000;\n\nexport interface RouteJudgeVerdict {\n\ttier: \"cheap\" | \"medium\" | \"expensive\";\n\trisk: RouteDecision[\"risk\"];\n\ttrivial: boolean;\n\treason: string;\n}\n\nexport function buildRouteJudgeUserPrompt(args: { prompt: string; baseline: RouteDecision }): string {\n\treturn [\n\t\t`Baseline (regex) verdict: tier=${args.baseline.tier}, risk=${args.baseline.risk}, reason=${args.baseline.reasonCode}.`,\n\t\t\"User prompt:\",\n\t\targs.prompt.slice(0, 4000),\n\t].join(\"\\n\");\n}\n\nconst JUDGE_TIERS: readonly string[] = [\"cheap\", \"medium\", \"expensive\"];\nconst JUDGE_RISKS: readonly string[] = [\"read-only\", \"scoped-write\", \"high-impact\", \"approval-required\"];\n\nexport function parseRouteJudgeVerdict(text: string): RouteJudgeVerdict | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\t// The judge may never select the learning tier or anything outside the three foreground tiers.\n\t\tif (typeof record.tier !== \"string\" || !JUDGE_TIERS.includes(record.tier)) continue;\n\t\tconst risk =\n\t\t\ttypeof record.risk === \"string\" && JUDGE_RISKS.includes(record.risk)\n\t\t\t\t? (record.risk as RouteDecision[\"risk\"])\n\t\t\t\t: undefined;\n\t\tif (!risk) continue;\n\t\treturn {\n\t\t\ttier: record.tier as RouteJudgeVerdict[\"tier\"],\n\t\t\trisk,\n\t\t\ttrivial: record.trivial === true,\n\t\t\treason: typeof record.reason === \"string\" ? record.reason.slice(0, 200) : \"\",\n\t\t};\n\t}\n\treturn undefined;\n}\n\n/** Merge a judge verdict into the baseline decision (pure; never returns learning). */\nexport function applyRouteJudgeVerdict(baseline: RouteDecision, verdict: RouteJudgeVerdict): RouteDecision {\n\t// Enforce the core rule in code, not just in the judge prompt: downgrading a non-cheap\n\t// baseline to cheap requires an EXPLICIT trivial verdict. An untrusted judge saying\n\t// {tier:\"cheap\", trivial:false} for an elevated prompt keeps the baseline tier.\n\tconst tier =\n\t\tverdict.tier === \"cheap\" && baseline.tier !== \"cheap\" && !verdict.trivial ? baseline.tier : verdict.tier;\n\treturn {\n\t\t...baseline,\n\t\ttier,\n\t\trisk: verdict.risk,\n\t\tconfidence: Math.max(baseline.confidence, 0.75),\n\t\treasonCode: `judge_${tier}${verdict.trivial ? \"_trivial\" : \"\"}`,\n\t\treasons: [...baseline.reasons, `Route judge: ${verdict.reason || \"no reason given\"}`],\n\t};\n}\n\nexport interface RouteJudgeRunResult {\n\tdecision: RouteDecision;\n\tverdict?: RouteJudgeVerdict;\n\t/** Set when the judge could not decide and the baseline was kept. */\n\tfallbackReason?: string;\n\tcostUsd: number;\n}\n\nfunction judgeFallbackDecision(baseline: RouteDecision, reason: string): RouteDecision {\n\treturn {\n\t\t...baseline,\n\t\treasonCode: \"judge_unavailable_fallback\",\n\t\treasons: [...baseline.reasons, reason],\n\t};\n}\n\n/**\n * Run the judge over a baseline decision. The completion executor is injected (production:\n * AgentSession.runIsolatedCompletion on the judge model). Never throws; every failure keeps the\n * baseline with a visible fallbackReason.\n */\nexport async function runRouteJudge(args: {\n\tprompt: string;\n\tbaseline: RouteDecision;\n\tcomplete: (input: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<{\n\t\ttext: string;\n\t\tcostUsd: number;\n\t\tstopReason: string;\n\t}>;\n\tsignal?: AbortSignal;\n\tmaxWallClockMs?: number;\n}): Promise<RouteJudgeRunResult> {\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: args.maxWallClockMs ?? ROUTE_JUDGE_MAX_WALL_CLOCK_MS,\n\t\tsignal: args.signal,\n\t\texecute: (signal) =>\n\t\t\targs.complete({\n\t\t\t\tsystemPrompt: ROUTE_JUDGE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildRouteJudgeUserPrompt({ prompt: args.prompt, baseline: args.baseline }),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure || !bounded.completion) {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge unavailable; baseline kept\"),\n\t\t\tfallbackReason: bounded.failure ? `judge_${bounded.failure.reasonCode}` : \"judge_unavailable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\tif (bounded.completion.stopReason === \"error\" || bounded.completion.stopReason === \"aborted\") {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge errored; baseline kept\"),\n\t\t\tfallbackReason: \"judge_model_error\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\tconst verdict = parseRouteJudgeVerdict(bounded.completion.text);\n\tif (!verdict) {\n\t\treturn {\n\t\t\tdecision: judgeFallbackDecision(args.baseline, \"Route judge unparseable; baseline kept\"),\n\t\t\tfallbackReason: \"judge_unparseable_fallback\",\n\t\t\tcostUsd,\n\t\t};\n\t}\n\n\treturn { decision: applyRouteJudgeVerdict(args.baseline, verdict), verdict, costUsd };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-escalation.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA2F1D,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAeV","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command)) return false;\n\tconst segments = command.split(/\\s*(?:&&|\\|\\||[;|\\r\\n])\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every((segment) => segment.length > 0 && isReadOnlyShellSegment(segment));\n}\n\nexport function shouldEscalateModelRouterTool(options: {\n\ttier: ModelTier;\n\ttoolName: string;\n\targs?: unknown;\n\t/** The route's reasonCode; executor-lane turns carry \"executor_direct\". */\n\treasonCode?: string;\n}): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\t// Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces\n\t// its own safety (danger confirmation, structural exit-code contract). Escalating on it would\n\t// abort every executor turn at the moment it does its job. Any OTHER mutating tool still\n\t// escalates to the expensive model as usual.\n\tif (options.reasonCode === \"executor_direct\" && toolName === \"run_toolkit_script\") return false;\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}
1
+ {"version":3,"file":"tool-escalation.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA6F1D,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAeV","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst UNSAFE_NESTED_SHELL_EXECUTION_RE = /(`|\\$\\(|\\bfind\\b[\\s\\S]*\\s-exec(?:dir)?\\b|\\bxargs\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command) || UNSAFE_NESTED_SHELL_EXECUTION_RE.test(command))\n\t\treturn false;\n\tconst segments = command.split(/\\s*(?:&&|\\|\\||[;|\\r\\n])\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every((segment) => segment.length > 0 && isReadOnlyShellSegment(segment));\n}\n\nexport function shouldEscalateModelRouterTool(options: {\n\ttier: ModelTier;\n\ttoolName: string;\n\targs?: unknown;\n\t/** The route's reasonCode; executor-lane turns carry \"executor_direct\". */\n\treasonCode?: string;\n}): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\t// Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces\n\t// its own safety (danger confirmation, structural exit-code contract). Escalating on it would\n\t// abort every executor turn at the moment it does its job. Any OTHER mutating tool still\n\t// escalates to the expensive model as usual.\n\tif (options.reasonCode === \"executor_direct\" && toolName === \"run_toolkit_script\") return false;\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}
@@ -42,6 +42,7 @@ const READ_ONLY_COMMANDS = new Set([
42
42
  const READ_ONLY_GIT_SUBCOMMANDS = new Set(["branch", "diff", "log", "rev-parse", "show", "status", "tag"]);
43
43
  const READ_ONLY_NPM_SUBCOMMANDS = new Set(["info", "list", "ls", "outdated", "view", "whoami"]);
44
44
  const MUTATING_SHELL_TOKEN_RE = /(^|\s)(>|>>|2>|&>|tee\b|rm\b|mv\b|cp\b|mkdir\b|touch\b|chmod\b|chown\b|install\b|commit\b|push\b|publish\b|deploy\b|apply\b|add\b|checkout\b|switch\b|reset\b|clean\b|stash\b|merge\b|rebase\b|npm\s+(?:i|install|ci|update|publish|run)\b|pnpm\s+(?:i|install|update|publish|run)\b|yarn\s+(?:add|install|upgrade|publish|run)\b)/i;
45
+ const UNSAFE_NESTED_SHELL_EXECUTION_RE = /(`|\$\(|\bfind\b[\s\S]*\s-exec(?:dir)?\b|\bxargs\b)/i;
45
46
  const MUTATING_TOOL_NAME_RE = /(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;
46
47
  function getShellCommand(args) {
47
48
  if (!args || typeof args !== "object")
@@ -75,7 +76,7 @@ function isReadOnlyShellSegment(segment) {
75
76
  return true;
76
77
  }
77
78
  function isReadOnlyShellCommand(command) {
78
- if (!command || MUTATING_SHELL_TOKEN_RE.test(command))
79
+ if (!command || MUTATING_SHELL_TOKEN_RE.test(command) || UNSAFE_NESTED_SHELL_EXECUTION_RE.test(command))
79
80
  return false;
80
81
  const segments = command.split(/\s*(?:&&|\|\||[;|\r\n])\s*/).map((segment) => segment.trim());
81
82
  return segments.length > 0 && segments.every((segment) => segment.length > 0 && isReadOnlyShellSegment(segment));
@@ -1 +1 @@
1
- {"version":3,"file":"tool-escalation.js","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command)) return false;\n\tconst segments = command.split(/\\s*(?:&&|\\|\\||[;|\\r\\n])\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every((segment) => segment.length > 0 && isReadOnlyShellSegment(segment));\n}\n\nexport function shouldEscalateModelRouterTool(options: {\n\ttier: ModelTier;\n\ttoolName: string;\n\targs?: unknown;\n\t/** The route's reasonCode; executor-lane turns carry \"executor_direct\". */\n\treasonCode?: string;\n}): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\t// Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces\n\t// its own safety (danger confirmation, structural exit-code contract). Escalating on it would\n\t// abort every executor turn at the moment it does its job. Any OTHER mutating tool still\n\t// escalates to the expensive model as usual.\n\tif (options.reasonCode === \"executor_direct\" && toolName === \"run_toolkit_script\") return false;\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}
1
+ {"version":3,"file":"tool-escalation.js","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst UNSAFE_NESTED_SHELL_EXECUTION_RE = /(`|\\$\\(|\\bfind\\b[\\s\\S]*\\s-exec(?:dir)?\\b|\\bxargs\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command) || UNSAFE_NESTED_SHELL_EXECUTION_RE.test(command))\n\t\treturn false;\n\tconst segments = command.split(/\\s*(?:&&|\\|\\||[;|\\r\\n])\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every((segment) => segment.length > 0 && isReadOnlyShellSegment(segment));\n}\n\nexport function shouldEscalateModelRouterTool(options: {\n\ttier: ModelTier;\n\ttoolName: string;\n\targs?: unknown;\n\t/** The route's reasonCode; executor-lane turns carry \"executor_direct\". */\n\treasonCode?: string;\n}): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\t// Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces\n\t// its own safety (danger confirmation, structural exit-code contract). Escalating on it would\n\t// abort every executor turn at the moment it does its job. Any OTHER mutating tool still\n\t// escalates to the expensive model as usual.\n\tif (options.reasonCode === \"executor_direct\" && toolName === \"run_toolkit_script\") return false;\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}