@google/gemini-cli-core 0.5.0-preview.1 → 0.7.0-nightly.20250912.68035591

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 (231) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +12 -2
  3. package/dist/google-gemini-cli-core-0.6.0-nightly.tgz +0 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/config/config.d.ts +24 -1
  8. package/dist/src/config/config.js +58 -15
  9. package/dist/src/config/config.js.map +1 -1
  10. package/dist/src/config/config.test.js +11 -15
  11. package/dist/src/config/config.test.js.map +1 -1
  12. package/dist/src/config/models.d.ts +14 -0
  13. package/dist/src/config/models.js +26 -0
  14. package/dist/src/config/models.js.map +1 -1
  15. package/dist/src/config/models.test.d.ts +6 -0
  16. package/dist/src/config/models.test.js +55 -0
  17. package/dist/src/config/models.test.js.map +1 -0
  18. package/dist/src/confirmation-bus/index.d.ts +7 -0
  19. package/dist/src/confirmation-bus/index.js +8 -0
  20. package/dist/src/confirmation-bus/index.js.map +1 -0
  21. package/dist/src/confirmation-bus/message-bus.d.ts +17 -0
  22. package/dist/src/confirmation-bus/message-bus.js +81 -0
  23. package/dist/src/confirmation-bus/message-bus.js.map +1 -0
  24. package/dist/src/confirmation-bus/message-bus.test.d.ts +6 -0
  25. package/dist/src/confirmation-bus/message-bus.test.js +164 -0
  26. package/dist/src/confirmation-bus/message-bus.test.js.map +1 -0
  27. package/dist/src/confirmation-bus/types.d.ts +38 -0
  28. package/dist/src/confirmation-bus/types.js +15 -0
  29. package/dist/src/confirmation-bus/types.js.map +1 -0
  30. package/dist/src/core/client.d.ts +4 -1
  31. package/dist/src/core/client.js +46 -18
  32. package/dist/src/core/client.js.map +1 -1
  33. package/dist/src/core/client.test.js +156 -46
  34. package/dist/src/core/client.test.js.map +1 -1
  35. package/dist/src/core/contentGenerator.d.ts +0 -1
  36. package/dist/src/core/contentGenerator.js +0 -4
  37. package/dist/src/core/contentGenerator.js.map +1 -1
  38. package/dist/src/core/contentGenerator.test.js +0 -3
  39. package/dist/src/core/contentGenerator.test.js.map +1 -1
  40. package/dist/src/core/coreToolScheduler.d.ts +4 -3
  41. package/dist/src/core/coreToolScheduler.js +42 -5
  42. package/dist/src/core/coreToolScheduler.js.map +1 -1
  43. package/dist/src/core/coreToolScheduler.test.js +34 -0
  44. package/dist/src/core/coreToolScheduler.test.js.map +1 -1
  45. package/dist/src/core/geminiChat.d.ts +1 -22
  46. package/dist/src/core/geminiChat.js +15 -135
  47. package/dist/src/core/geminiChat.js.map +1 -1
  48. package/dist/src/core/geminiChat.test.js +59 -312
  49. package/dist/src/core/geminiChat.test.js.map +1 -1
  50. package/dist/src/core/nonInteractiveToolExecutor.test.js +48 -0
  51. package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
  52. package/dist/src/core/subagent.js +1 -1
  53. package/dist/src/core/subagent.js.map +1 -1
  54. package/dist/src/core/subagent.test.js +9 -8
  55. package/dist/src/core/subagent.test.js.map +1 -1
  56. package/dist/src/core/turn.d.ts +2 -1
  57. package/dist/src/core/turn.js +2 -2
  58. package/dist/src/core/turn.js.map +1 -1
  59. package/dist/src/core/turn.test.js +18 -18
  60. package/dist/src/core/turn.test.js.map +1 -1
  61. package/dist/src/generated/git-commit.d.ts +2 -2
  62. package/dist/src/generated/git-commit.js +2 -2
  63. package/dist/src/generated/git-commit.js.map +1 -1
  64. package/dist/src/ide/ide-client.d.ts +27 -0
  65. package/dist/src/ide/ide-client.js +85 -5
  66. package/dist/src/ide/ide-client.js.map +1 -1
  67. package/dist/src/ide/ide-client.test.js +53 -0
  68. package/dist/src/ide/ide-client.test.js.map +1 -1
  69. package/dist/src/ide/ideContext.d.ts +34 -20
  70. package/dist/src/ide/ideContext.js +20 -33
  71. package/dist/src/ide/ideContext.js.map +1 -1
  72. package/dist/src/ide/ideContext.test.js +37 -39
  73. package/dist/src/ide/ideContext.test.js.map +1 -1
  74. package/dist/src/index.d.ts +3 -1
  75. package/dist/src/index.js +3 -1
  76. package/dist/src/index.js.map +1 -1
  77. package/dist/src/output/json-formatter.d.ts +11 -0
  78. package/dist/src/output/json-formatter.js +30 -0
  79. package/dist/src/output/json-formatter.js.map +1 -0
  80. package/dist/src/output/json-formatter.test.d.ts +6 -0
  81. package/dist/src/output/json-formatter.test.js +266 -0
  82. package/dist/src/output/json-formatter.test.js.map +1 -0
  83. package/dist/src/output/types.d.ts +20 -0
  84. package/dist/src/output/types.js +11 -0
  85. package/dist/src/output/types.js.map +1 -0
  86. package/dist/src/policy/index.d.ts +7 -0
  87. package/dist/src/policy/index.js +8 -0
  88. package/dist/src/policy/index.js.map +1 -0
  89. package/dist/src/policy/policy-engine.d.ts +30 -0
  90. package/dist/src/policy/policy-engine.js +83 -0
  91. package/dist/src/policy/policy-engine.js.map +1 -0
  92. package/dist/src/policy/policy-engine.test.d.ts +6 -0
  93. package/dist/src/policy/policy-engine.test.js +470 -0
  94. package/dist/src/policy/policy-engine.test.js.map +1 -0
  95. package/dist/src/policy/stable-stringify.d.ts +58 -0
  96. package/dist/src/policy/stable-stringify.js +122 -0
  97. package/dist/src/policy/stable-stringify.js.map +1 -0
  98. package/dist/src/policy/types.d.ts +47 -0
  99. package/dist/src/policy/types.js +12 -0
  100. package/dist/src/policy/types.js.map +1 -0
  101. package/dist/src/routing/modelRouterService.d.ts +23 -0
  102. package/dist/src/routing/modelRouterService.js +36 -0
  103. package/dist/src/routing/modelRouterService.js.map +1 -0
  104. package/dist/src/routing/modelRouterService.test.d.ts +6 -0
  105. package/dist/src/routing/modelRouterService.test.js +72 -0
  106. package/dist/src/routing/modelRouterService.test.js.map +1 -0
  107. package/dist/src/routing/routingStrategy.d.ts +62 -0
  108. package/dist/src/routing/routingStrategy.js +7 -0
  109. package/dist/src/routing/routingStrategy.js.map +1 -0
  110. package/dist/src/routing/strategies/compositeStrategy.d.ts +26 -0
  111. package/dist/src/routing/strategies/compositeStrategy.js +67 -0
  112. package/dist/src/routing/strategies/compositeStrategy.js.map +1 -0
  113. package/dist/src/routing/strategies/compositeStrategy.test.d.ts +6 -0
  114. package/dist/src/routing/strategies/compositeStrategy.test.js +123 -0
  115. package/dist/src/routing/strategies/compositeStrategy.test.js.map +1 -0
  116. package/dist/src/routing/strategies/defaultStrategy.d.ts +12 -0
  117. package/dist/src/routing/strategies/defaultStrategy.js +20 -0
  118. package/dist/src/routing/strategies/defaultStrategy.js.map +1 -0
  119. package/dist/src/routing/strategies/defaultStrategy.test.d.ts +6 -0
  120. package/dist/src/routing/strategies/defaultStrategy.test.js +26 -0
  121. package/dist/src/routing/strategies/defaultStrategy.test.js.map +1 -0
  122. package/dist/src/routing/strategies/fallbackStrategy.d.ts +12 -0
  123. package/dist/src/routing/strategies/fallbackStrategy.js +25 -0
  124. package/dist/src/routing/strategies/fallbackStrategy.js.map +1 -0
  125. package/dist/src/routing/strategies/fallbackStrategy.test.d.ts +6 -0
  126. package/dist/src/routing/strategies/fallbackStrategy.test.js +55 -0
  127. package/dist/src/routing/strategies/fallbackStrategy.test.js.map +1 -0
  128. package/dist/src/routing/strategies/overrideStrategy.d.ts +15 -0
  129. package/dist/src/routing/strategies/overrideStrategy.js +27 -0
  130. package/dist/src/routing/strategies/overrideStrategy.js.map +1 -0
  131. package/dist/src/routing/strategies/overrideStrategy.test.d.ts +6 -0
  132. package/dist/src/routing/strategies/overrideStrategy.test.js +41 -0
  133. package/dist/src/routing/strategies/overrideStrategy.test.js.map +1 -0
  134. package/dist/src/services/fileDiscoveryService.d.ts +10 -0
  135. package/dist/src/services/fileDiscoveryService.js +31 -17
  136. package/dist/src/services/fileDiscoveryService.js.map +1 -1
  137. package/dist/src/services/loopDetectionService.d.ts +5 -0
  138. package/dist/src/services/loopDetectionService.js +13 -2
  139. package/dist/src/services/loopDetectionService.js.map +1 -1
  140. package/dist/src/services/loopDetectionService.test.js +15 -0
  141. package/dist/src/services/loopDetectionService.test.js.map +1 -1
  142. package/dist/src/services/shellExecutionService.d.ts +34 -2
  143. package/dist/src/services/shellExecutionService.js +177 -43
  144. package/dist/src/services/shellExecutionService.js.map +1 -1
  145. package/dist/src/services/shellExecutionService.test.js +153 -56
  146. package/dist/src/services/shellExecutionService.test.js.map +1 -1
  147. package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +4 -2
  148. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +31 -4
  149. package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
  150. package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +6 -2
  151. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +14 -2
  152. package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
  153. package/dist/src/telemetry/index.d.ts +2 -2
  154. package/dist/src/telemetry/index.js +2 -2
  155. package/dist/src/telemetry/index.js.map +1 -1
  156. package/dist/src/telemetry/loggers.d.ts +2 -1
  157. package/dist/src/telemetry/loggers.js +20 -5
  158. package/dist/src/telemetry/loggers.js.map +1 -1
  159. package/dist/src/telemetry/loggers.test.js +50 -35
  160. package/dist/src/telemetry/loggers.test.js.map +1 -1
  161. package/dist/src/telemetry/metrics.d.ts +1 -1
  162. package/dist/src/telemetry/metrics.js +2 -2
  163. package/dist/src/telemetry/metrics.js.map +1 -1
  164. package/dist/src/telemetry/types.d.ts +23 -3
  165. package/dist/src/telemetry/types.js +25 -3
  166. package/dist/src/telemetry/types.js.map +1 -1
  167. package/dist/src/tools/edit.js +2 -3
  168. package/dist/src/tools/edit.js.map +1 -1
  169. package/dist/src/tools/edit.test.js +2 -8
  170. package/dist/src/tools/edit.test.js.map +1 -1
  171. package/dist/src/tools/glob.d.ts +5 -1
  172. package/dist/src/tools/glob.js +24 -17
  173. package/dist/src/tools/glob.js.map +1 -1
  174. package/dist/src/tools/glob.test.js +51 -0
  175. package/dist/src/tools/glob.test.js.map +1 -1
  176. package/dist/src/tools/ls.js +19 -32
  177. package/dist/src/tools/ls.js.map +1 -1
  178. package/dist/src/tools/ls.test.js +140 -280
  179. package/dist/src/tools/ls.test.js.map +1 -1
  180. package/dist/src/tools/read-many-files.d.ts +1 -1
  181. package/dist/src/tools/read-many-files.js +17 -49
  182. package/dist/src/tools/read-many-files.js.map +1 -1
  183. package/dist/src/tools/ripGrep.d.ts +4 -0
  184. package/dist/src/tools/ripGrep.js +11 -1
  185. package/dist/src/tools/ripGrep.js.map +1 -1
  186. package/dist/src/tools/ripGrep.test.js +51 -1
  187. package/dist/src/tools/ripGrep.test.js.map +1 -1
  188. package/dist/src/tools/shell.d.ts +12 -2
  189. package/dist/src/tools/shell.js +12 -16
  190. package/dist/src/tools/shell.js.map +1 -1
  191. package/dist/src/tools/shell.test.js +2 -33
  192. package/dist/src/tools/shell.test.js.map +1 -1
  193. package/dist/src/tools/smart-edit.js +2 -3
  194. package/dist/src/tools/smart-edit.js.map +1 -1
  195. package/dist/src/tools/smart-edit.test.js +2 -8
  196. package/dist/src/tools/smart-edit.test.js.map +1 -1
  197. package/dist/src/tools/tools.d.ts +6 -4
  198. package/dist/src/tools/tools.js +2 -2
  199. package/dist/src/tools/tools.js.map +1 -1
  200. package/dist/src/tools/write-file.js +2 -3
  201. package/dist/src/tools/write-file.js.map +1 -1
  202. package/dist/src/tools/write-file.test.js +78 -0
  203. package/dist/src/tools/write-file.test.js.map +1 -1
  204. package/dist/src/utils/bfsFileSearch.js +11 -5
  205. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  206. package/dist/src/utils/errors.d.ts +6 -0
  207. package/dist/src/utils/errors.js +10 -0
  208. package/dist/src/utils/errors.js.map +1 -1
  209. package/dist/src/utils/geminiIgnoreParser.d.ts +18 -0
  210. package/dist/src/utils/geminiIgnoreParser.js +61 -0
  211. package/dist/src/utils/geminiIgnoreParser.js.map +1 -0
  212. package/dist/src/utils/geminiIgnoreParser.test.d.ts +6 -0
  213. package/dist/src/utils/geminiIgnoreParser.test.js +50 -0
  214. package/dist/src/utils/geminiIgnoreParser.test.js.map +1 -0
  215. package/dist/src/utils/gitIgnoreParser.d.ts +3 -9
  216. package/dist/src/utils/gitIgnoreParser.js +60 -69
  217. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  218. package/dist/src/utils/gitIgnoreParser.test.js +18 -53
  219. package/dist/src/utils/gitIgnoreParser.test.js.map +1 -1
  220. package/dist/src/utils/terminalSerializer.d.ts +28 -0
  221. package/dist/src/utils/terminalSerializer.js +432 -0
  222. package/dist/src/utils/terminalSerializer.js.map +1 -0
  223. package/dist/src/utils/terminalSerializer.test.d.ts +6 -0
  224. package/dist/src/utils/terminalSerializer.test.js +176 -0
  225. package/dist/src/utils/terminalSerializer.test.js.map +1 -0
  226. package/dist/tsconfig.tsbuildinfo +1 -1
  227. package/package.json +1 -1
  228. package/dist/google-gemini-cli-core-0.5.0-preview.tgz +0 -0
  229. package/dist/src/utils/ide-trust.d.ts +0 -10
  230. package/dist/src/utils/ide-trust.js +0 -14
  231. package/dist/src/utils/ide-trust.js.map +0 -1
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Produces a stable, deterministic JSON string representation with sorted keys.
8
+ *
9
+ * This method is critical for security policy matching. It ensures that the same
10
+ * object always produces the same string representation, regardless of property
11
+ * insertion order, which could vary across different JavaScript engines or
12
+ * runtime conditions.
13
+ *
14
+ * Key behaviors:
15
+ * 1. **Sorted Keys**: Object properties are always serialized in alphabetical order,
16
+ * ensuring deterministic output for pattern matching.
17
+ *
18
+ * 2. **Circular Reference Protection**: Uses ancestor chain tracking (not just
19
+ * object identity) to detect true circular references while correctly handling
20
+ * repeated non-circular object references. Circular references are replaced
21
+ * with "[Circular]" to prevent stack overflow attacks.
22
+ *
23
+ * 3. **JSON Spec Compliance**:
24
+ * - undefined values: Omitted from objects, converted to null in arrays
25
+ * - Functions: Omitted from objects, converted to null in arrays
26
+ * - toJSON methods: Respected and called when present (per JSON.stringify spec)
27
+ *
28
+ * 4. **Security Considerations**:
29
+ * - Prevents DoS via circular references that would cause infinite recursion
30
+ * - Ensures consistent policy rule matching by normalizing property order
31
+ * - Respects toJSON for objects that sanitize their output
32
+ * - Handles toJSON methods that throw errors gracefully
33
+ *
34
+ * @param obj - The object to stringify (typically toolCall.args)
35
+ * @returns A deterministic JSON string representation
36
+ *
37
+ * @example
38
+ * // Different property orders produce the same output:
39
+ * stableStringify({b: 2, a: 1}) === stableStringify({a: 1, b: 2})
40
+ * // Returns: '{"a":1,"b":2}'
41
+ *
42
+ * @example
43
+ * // Circular references are handled safely:
44
+ * const obj = {a: 1};
45
+ * obj.self = obj;
46
+ * stableStringify(obj)
47
+ * // Returns: '{"a":1,"self":"[Circular]"}'
48
+ *
49
+ * @example
50
+ * // toJSON methods are respected:
51
+ * const obj = {
52
+ * sensitive: 'secret',
53
+ * toJSON: () => ({ safe: 'data' })
54
+ * };
55
+ * stableStringify(obj)
56
+ * // Returns: '{"safe":"data"}'
57
+ */
58
+ export function stableStringify(obj) {
59
+ const stringify = (currentObj, ancestors) => {
60
+ // Handle primitives and null
61
+ if (currentObj === undefined) {
62
+ return 'null'; // undefined in arrays becomes null in JSON
63
+ }
64
+ if (currentObj === null) {
65
+ return 'null';
66
+ }
67
+ if (typeof currentObj === 'function') {
68
+ return 'null'; // functions in arrays become null in JSON
69
+ }
70
+ if (typeof currentObj !== 'object') {
71
+ return JSON.stringify(currentObj);
72
+ }
73
+ // Check for circular reference (object is in ancestor chain)
74
+ if (ancestors.has(currentObj)) {
75
+ return '"[Circular]"';
76
+ }
77
+ ancestors.add(currentObj);
78
+ try {
79
+ // Check for toJSON method and use it if present
80
+ const objWithToJSON = currentObj;
81
+ if (typeof objWithToJSON.toJSON === 'function') {
82
+ try {
83
+ const jsonValue = objWithToJSON.toJSON();
84
+ // The result of toJSON needs to be stringified recursively
85
+ if (jsonValue === null) {
86
+ return 'null';
87
+ }
88
+ return stringify(jsonValue, ancestors);
89
+ }
90
+ catch {
91
+ // If toJSON throws, treat as a regular object
92
+ }
93
+ }
94
+ if (Array.isArray(currentObj)) {
95
+ const items = currentObj.map((item) => {
96
+ // undefined and functions in arrays become null
97
+ if (item === undefined || typeof item === 'function') {
98
+ return 'null';
99
+ }
100
+ return stringify(item, ancestors);
101
+ });
102
+ return '[' + items.join(',') + ']';
103
+ }
104
+ // Handle objects - sort keys and filter out undefined/function values
105
+ const sortedKeys = Object.keys(currentObj).sort();
106
+ const pairs = [];
107
+ for (const key of sortedKeys) {
108
+ const value = currentObj[key];
109
+ // Skip undefined and function values in objects (per JSON spec)
110
+ if (value !== undefined && typeof value !== 'function') {
111
+ pairs.push(JSON.stringify(key) + ':' + stringify(value, ancestors));
112
+ }
113
+ }
114
+ return '{' + pairs.join(',') + '}';
115
+ }
116
+ finally {
117
+ ancestors.delete(currentObj);
118
+ }
119
+ };
120
+ return stringify(obj, new Set());
121
+ }
122
+ //# sourceMappingURL=stable-stringify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stable-stringify.js","sourceRoot":"","sources":["../../../src/policy/stable-stringify.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,MAAM,SAAS,GAAG,CAAC,UAAmB,EAAE,SAAuB,EAAU,EAAE;QACzE,6BAA6B;QAC7B,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,MAAM,CAAC,CAAC,2CAA2C;QAC5D,CAAC;QACD,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACxB,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;YACrC,OAAO,MAAM,CAAC,CAAC,0CAA0C;QAC3D,CAAC;QACD,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;QAED,6DAA6D;QAC7D,IAAI,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAE1B,IAAI,CAAC;YACH,gDAAgD;YAChD,MAAM,aAAa,GAAG,UAAwC,CAAC;YAC/D,IAAI,OAAO,aAAa,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBAC/C,IAAI,CAAC;oBACH,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC;oBACzC,2DAA2D;oBAC3D,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;wBACvB,OAAO,MAAM,CAAC;oBAChB,CAAC;oBACD,OAAO,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACzC,CAAC;gBAAC,MAAM,CAAC;oBACP,8CAA8C;gBAChD,CAAC;YACH,CAAC;YAED,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;oBACpC,gDAAgD;oBAChD,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;wBACrD,OAAO,MAAM,CAAC;oBAChB,CAAC;oBACD,OAAO,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;gBACpC,CAAC,CAAC,CAAC;gBACH,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;YACrC,CAAC;YAED,sEAAsE;YACtE,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;YAClD,MAAM,KAAK,GAAa,EAAE,CAAC;YAE3B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,MAAM,KAAK,GAAI,UAAsC,CAAC,GAAG,CAAC,CAAC;gBAC3D,gEAAgE;gBAChE,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;oBACvD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC;YAED,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACrC,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export declare enum PolicyDecision {
7
+ ALLOW = "allow",
8
+ DENY = "deny",
9
+ ASK_USER = "ask_user"
10
+ }
11
+ export interface PolicyRule {
12
+ /**
13
+ * The name of the tool this rule applies to.
14
+ * If undefined, the rule applies to all tools.
15
+ */
16
+ toolName?: string;
17
+ /**
18
+ * Pattern to match against tool arguments.
19
+ * Can be used for more fine-grained control.
20
+ */
21
+ argsPattern?: RegExp;
22
+ /**
23
+ * The decision to make when this rule matches.
24
+ */
25
+ decision: PolicyDecision;
26
+ /**
27
+ * Priority of this rule. Higher numbers take precedence.
28
+ * Default is 0.
29
+ */
30
+ priority?: number;
31
+ }
32
+ export interface PolicyEngineConfig {
33
+ /**
34
+ * List of policy rules to apply.
35
+ */
36
+ rules?: PolicyRule[];
37
+ /**
38
+ * Default decision when no rules match.
39
+ * Defaults to ASK_USER.
40
+ */
41
+ defaultDecision?: PolicyDecision;
42
+ /**
43
+ * Whether to allow tools in non-interactive mode.
44
+ * When true, ASK_USER decisions become DENY.
45
+ */
46
+ nonInteractive?: boolean;
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export var PolicyDecision;
7
+ (function (PolicyDecision) {
8
+ PolicyDecision["ALLOW"] = "allow";
9
+ PolicyDecision["DENY"] = "deny";
10
+ PolicyDecision["ASK_USER"] = "ask_user";
11
+ })(PolicyDecision || (PolicyDecision = {}));
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/policy/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAN,IAAY,cAIX;AAJD,WAAY,cAAc;IACxB,iCAAe,CAAA;IACf,+BAAa,CAAA;IACb,uCAAqB,CAAA;AACvB,CAAC,EAJW,cAAc,KAAd,cAAc,QAIzB"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { Config } from '../config/config.js';
7
+ import type { RoutingContext, RoutingDecision } from './routingStrategy.js';
8
+ /**
9
+ * A centralized service for making model routing decisions.
10
+ */
11
+ export declare class ModelRouterService {
12
+ private config;
13
+ private strategy;
14
+ constructor(config: Config);
15
+ private initializeDefaultStrategy;
16
+ /**
17
+ * Determines which model to use for a given request context.
18
+ *
19
+ * @param context The full context of the request.
20
+ * @returns A promise that resolves to a RoutingDecision.
21
+ */
22
+ route(context: RoutingContext): Promise<RoutingDecision>;
23
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { DefaultStrategy } from './strategies/defaultStrategy.js';
7
+ import { CompositeStrategy } from './strategies/compositeStrategy.js';
8
+ import { FallbackStrategy } from './strategies/fallbackStrategy.js';
9
+ import { OverrideStrategy } from './strategies/overrideStrategy.js';
10
+ /**
11
+ * A centralized service for making model routing decisions.
12
+ */
13
+ export class ModelRouterService {
14
+ config;
15
+ strategy;
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.strategy = this.initializeDefaultStrategy();
19
+ }
20
+ initializeDefaultStrategy() {
21
+ // Initialize the composite strategy with the desired priority order.
22
+ // The strategies are ordered in order of highest priority.
23
+ return new CompositeStrategy([new FallbackStrategy(), new OverrideStrategy(), new DefaultStrategy()], 'agent-router');
24
+ }
25
+ /**
26
+ * Determines which model to use for a given request context.
27
+ *
28
+ * @param context The full context of the request.
29
+ * @returns A promise that resolves to a RoutingDecision.
30
+ */
31
+ async route(context) {
32
+ const decision = await this.strategy.route(context, this.config, this.config.getBaseLlmClient());
33
+ return decision;
34
+ }
35
+ }
36
+ //# sourceMappingURL=modelRouterService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelRouterService.js","sourceRoot":"","sources":["../../../src/routing/modelRouterService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEpE;;GAEG;AACH,MAAM,OAAO,kBAAkB;IACrB,MAAM,CAAS;IACf,QAAQ,CAAmB;IAEnC,YAAY,MAAc;QACxB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,yBAAyB,EAAE,CAAC;IACnD,CAAC;IAEO,yBAAyB;QAC/B,qEAAqE;QACrE,2DAA2D;QAC3D,OAAO,IAAI,iBAAiB,CAC1B,CAAC,IAAI,gBAAgB,EAAE,EAAE,IAAI,gBAAgB,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC,EACvE,cAAc,CACf,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,OAAuB;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CACxC,OAAO,EACP,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAC/B,CAAC;QAEF,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { ModelRouterService } from './modelRouterService.js';
8
+ import { Config } from '../config/config.js';
9
+ import { DefaultStrategy } from './strategies/defaultStrategy.js';
10
+ import { CompositeStrategy } from './strategies/compositeStrategy.js';
11
+ import { FallbackStrategy } from './strategies/fallbackStrategy.js';
12
+ import { OverrideStrategy } from './strategies/overrideStrategy.js';
13
+ vi.mock('../config/config.js');
14
+ vi.mock('../core/baseLlmClient.js');
15
+ vi.mock('./strategies/defaultStrategy.js');
16
+ vi.mock('./strategies/compositeStrategy.js');
17
+ vi.mock('./strategies/fallbackStrategy.js');
18
+ vi.mock('./strategies/overrideStrategy.js');
19
+ describe('ModelRouterService', () => {
20
+ let service;
21
+ let mockConfig;
22
+ let mockBaseLlmClient;
23
+ let mockContext;
24
+ let mockCompositeStrategy;
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ mockConfig = new Config({});
28
+ mockBaseLlmClient = {};
29
+ vi.spyOn(mockConfig, 'getBaseLlmClient').mockReturnValue(mockBaseLlmClient);
30
+ mockCompositeStrategy = new CompositeStrategy([new FallbackStrategy(), new OverrideStrategy(), new DefaultStrategy()], 'agent-router');
31
+ vi.mocked(CompositeStrategy).mockImplementation(() => mockCompositeStrategy);
32
+ service = new ModelRouterService(mockConfig);
33
+ mockContext = {
34
+ history: [],
35
+ request: [{ text: 'test prompt' }],
36
+ signal: new AbortController().signal,
37
+ };
38
+ });
39
+ it('should initialize with a CompositeStrategy', () => {
40
+ expect(CompositeStrategy).toHaveBeenCalled();
41
+ expect(service['strategy']).toBeInstanceOf(CompositeStrategy);
42
+ });
43
+ it('should initialize the CompositeStrategy with the correct child strategies in order', () => {
44
+ // This test relies on the mock implementation detail of the constructor
45
+ const compositeStrategyArgs = vi.mocked(CompositeStrategy).mock.calls[0];
46
+ const childStrategies = compositeStrategyArgs[0];
47
+ expect(childStrategies.length).toBe(3);
48
+ expect(childStrategies[0]).toBeInstanceOf(FallbackStrategy);
49
+ expect(childStrategies[1]).toBeInstanceOf(OverrideStrategy);
50
+ expect(childStrategies[2]).toBeInstanceOf(DefaultStrategy);
51
+ expect(compositeStrategyArgs[1]).toBe('agent-router');
52
+ });
53
+ describe('route()', () => {
54
+ it('should delegate routing to the composite strategy', async () => {
55
+ const strategyDecision = {
56
+ model: 'strategy-chosen-model',
57
+ metadata: {
58
+ source: 'test-router/fallback',
59
+ latencyMs: 10,
60
+ reasoning: 'Strategy reasoning',
61
+ },
62
+ };
63
+ const strategySpy = vi
64
+ .spyOn(mockCompositeStrategy, 'route')
65
+ .mockResolvedValue(strategyDecision);
66
+ const decision = await service.route(mockContext);
67
+ expect(strategySpy).toHaveBeenCalledWith(mockContext, mockConfig, mockBaseLlmClient);
68
+ expect(decision).toEqual(strategyDecision);
69
+ });
70
+ });
71
+ });
72
+ //# sourceMappingURL=modelRouterService.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelRouterService.test.js","sourceRoot":"","sources":["../../../src/routing/modelRouterService.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { Content, PartListUnion } from '@google/genai';
7
+ import type { BaseLlmClient } from '../core/baseLlmClient.js';
8
+ import type { Config } from '../config/config.js';
9
+ /**
10
+ * The output of a routing decision. It specifies which model to use and why.
11
+ */
12
+ export interface RoutingDecision {
13
+ /** The model identifier string to use for the next API call (e.g., 'gemini-2.5-pro'). */
14
+ model: string;
15
+ /**
16
+ * Metadata about the routing decision for logging purposes.
17
+ */
18
+ metadata: {
19
+ source: string;
20
+ latencyMs: number;
21
+ reasoning: string;
22
+ error?: string;
23
+ };
24
+ }
25
+ /**
26
+ * The context provided to the router for making a decision.
27
+ */
28
+ export interface RoutingContext {
29
+ /** The full history of the conversation. */
30
+ history: Content[];
31
+ /** The immediate request parts to be processed. */
32
+ request: PartListUnion;
33
+ /** An abort signal to cancel an LLM call during routing. */
34
+ signal: AbortSignal;
35
+ }
36
+ /**
37
+ * The core interface that all routing strategies must implement.
38
+ * Strategies implementing this interface may decline a request by returning null.
39
+ */
40
+ export interface RoutingStrategy {
41
+ /** The name of the strategy (e.g., 'fallback', 'override', 'composite'). */
42
+ readonly name: string;
43
+ /**
44
+ * Determines which model to use for a given request context.
45
+ * @param context The full context of the request.
46
+ * @param config The current configuration.
47
+ * @param client A reference to the GeminiClient, allowing the strategy to make its own API calls if needed.
48
+ * @returns A promise that resolves to a RoutingDecision, or null if the strategy is not applicable.
49
+ */
50
+ route(context: RoutingContext, config: Config, baseLlmClient: BaseLlmClient): Promise<RoutingDecision | null>;
51
+ }
52
+ /**
53
+ * A strategy that is guaranteed to return a decision. It must not return null.
54
+ * This is used to ensure that a composite chain always terminates.
55
+ */
56
+ export interface TerminalStrategy extends RoutingStrategy {
57
+ /**
58
+ * Determines which model to use for a given request context.
59
+ * @returns A promise that resolves to a RoutingDecision.
60
+ */
61
+ route(context: RoutingContext, config: Config, baseLlmClient: BaseLlmClient): Promise<RoutingDecision>;
62
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=routingStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routingStrategy.js","sourceRoot":"","sources":["../../../src/routing/routingStrategy.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { Config } from '../../config/config.js';
7
+ import type { BaseLlmClient } from '../../core/baseLlmClient.js';
8
+ import type { RoutingContext, RoutingDecision, RoutingStrategy, TerminalStrategy } from '../routingStrategy.js';
9
+ /**
10
+ * A strategy that attempts a list of child strategies in order (Chain of Responsibility).
11
+ */
12
+ export declare class CompositeStrategy implements TerminalStrategy {
13
+ readonly name: string;
14
+ private strategies;
15
+ /**
16
+ * Initializes the CompositeStrategy.
17
+ * @param strategies The strategies to try, in order of priority. The last strategy must be terminal.
18
+ * @param name The name of this composite configuration (e.g., 'router' or 'composite').
19
+ */
20
+ constructor(strategies: [...RoutingStrategy[], TerminalStrategy], name?: string);
21
+ route(context: RoutingContext, config: Config, baseLlmClient: BaseLlmClient): Promise<RoutingDecision>;
22
+ /**
23
+ * Helper function to enhance the decision metadata with composite information.
24
+ */
25
+ private finalizeDecision;
26
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * A strategy that attempts a list of child strategies in order (Chain of Responsibility).
8
+ */
9
+ export class CompositeStrategy {
10
+ name;
11
+ strategies;
12
+ /**
13
+ * Initializes the CompositeStrategy.
14
+ * @param strategies The strategies to try, in order of priority. The last strategy must be terminal.
15
+ * @param name The name of this composite configuration (e.g., 'router' or 'composite').
16
+ */
17
+ constructor(strategies, name = 'composite') {
18
+ this.strategies = strategies;
19
+ this.name = name;
20
+ }
21
+ async route(context, config, baseLlmClient) {
22
+ const startTime = performance.now();
23
+ // Separate non-terminal strategies from the terminal one.
24
+ // This separation allows TypeScript to understand the control flow guarantees.
25
+ const nonTerminalStrategies = this.strategies.slice(0, -1);
26
+ const terminalStrategy = this.strategies[this.strategies.length - 1];
27
+ // Try non-terminal strategies, allowing them to fail gracefully.
28
+ for (const strategy of nonTerminalStrategies) {
29
+ try {
30
+ const decision = await strategy.route(context, config, baseLlmClient);
31
+ if (decision) {
32
+ return this.finalizeDecision(decision, startTime);
33
+ }
34
+ }
35
+ catch (error) {
36
+ console.error(`[Routing] Strategy '${strategy.name}' failed. Continuing to next strategy. Error:`, error);
37
+ }
38
+ }
39
+ // If no other strategy matched, execute the terminal strategy.
40
+ try {
41
+ const decision = await terminalStrategy.route(context, config, baseLlmClient);
42
+ return this.finalizeDecision(decision, startTime);
43
+ }
44
+ catch (error) {
45
+ console.error(`[Routing] Critical Error: Terminal strategy '${terminalStrategy.name}' failed. Routing cannot proceed. Error:`, error);
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Helper function to enhance the decision metadata with composite information.
51
+ */
52
+ finalizeDecision(decision, startTime) {
53
+ const endTime = performance.now();
54
+ const totalLatency = endTime - startTime;
55
+ // Combine the source paths: composite_name/child_source (e.g. 'router/default')
56
+ const compositeSource = `${this.name}/${decision.metadata.source}`;
57
+ return {
58
+ ...decision,
59
+ metadata: {
60
+ ...decision.metadata,
61
+ source: compositeSource,
62
+ latencyMs: decision.metadata.latencyMs || totalLatency,
63
+ },
64
+ };
65
+ }
66
+ }
67
+ //# sourceMappingURL=compositeStrategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compositeStrategy.js","sourceRoot":"","sources":["../../../../src/routing/strategies/compositeStrategy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;GAEG;AACH,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAS;IAEd,UAAU,CAA2C;IAE7D;;;;OAIG;IACH,YACE,UAAoD,EACpD,OAAe,WAAW;QAE1B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,KAAK,CACT,OAAuB,EACvB,MAAc,EACd,aAA4B;QAE5B,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAEpC,0DAA0D;QAC1D,+EAA+E;QAC/E,MAAM,qBAAqB,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CACjD,CAAC,EACD,CAAC,CAAC,CACkB,CAAC;QACvB,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,CACtC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CACP,CAAC;QAEtB,iEAAiE;QACjE,KAAK,MAAM,QAAQ,IAAI,qBAAqB,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;gBACtE,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;gBACpD,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CACX,uBAAuB,QAAQ,CAAC,IAAI,+CAA+C,EACnF,KAAK,CACN,CAAC;YACJ,CAAC;QACH,CAAC;QAED,+DAA+D;QAC/D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,KAAK,CAC3C,OAAO,EACP,MAAM,EACN,aAAa,CACd,CAAC;YAEF,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CACX,gDAAgD,gBAAgB,CAAC,IAAI,0CAA0C,EAC/G,KAAK,CACN,CAAC;YACF,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACK,gBAAgB,CACtB,QAAyB,EACzB,SAAiB;QAEjB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAClC,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;QAEzC,gFAAgF;QAChF,MAAM,eAAe,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAEnE,OAAO;YACL,GAAG,QAAQ;YACX,QAAQ,EAAE;gBACR,GAAG,QAAQ,CAAC,QAAQ;gBACpB,MAAM,EAAE,eAAe;gBACvB,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,SAAS,IAAI,YAAY;aACvD;SACF,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export {};
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { CompositeStrategy } from './compositeStrategy.js';
8
+ describe('CompositeStrategy', () => {
9
+ let mockContext;
10
+ let mockConfig;
11
+ let mockBaseLlmClient;
12
+ let mockStrategy1;
13
+ let mockStrategy2;
14
+ let mockTerminalStrategy;
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ mockContext = {};
18
+ mockConfig = {};
19
+ mockBaseLlmClient = {};
20
+ mockStrategy1 = {
21
+ name: 'strategy1',
22
+ route: vi.fn().mockResolvedValue(null),
23
+ };
24
+ mockStrategy2 = {
25
+ name: 'strategy2',
26
+ route: vi.fn().mockResolvedValue(null),
27
+ };
28
+ mockTerminalStrategy = {
29
+ name: 'terminal',
30
+ route: vi.fn().mockResolvedValue({
31
+ model: 'terminal-model',
32
+ metadata: {
33
+ source: 'terminal',
34
+ latencyMs: 10,
35
+ reasoning: 'Terminal decision',
36
+ },
37
+ }),
38
+ };
39
+ });
40
+ it('should try strategies in order and return the first successful decision', async () => {
41
+ const decision = {
42
+ model: 'strategy2-model',
43
+ metadata: {
44
+ source: 'strategy2',
45
+ latencyMs: 20,
46
+ reasoning: 'Strategy 2 decided',
47
+ },
48
+ };
49
+ vi.spyOn(mockStrategy2, 'route').mockResolvedValue(decision);
50
+ const composite = new CompositeStrategy([mockStrategy1, mockStrategy2, mockTerminalStrategy], 'test-router');
51
+ const result = await composite.route(mockContext, mockConfig, mockBaseLlmClient);
52
+ expect(mockStrategy1.route).toHaveBeenCalledWith(mockContext, mockConfig, mockBaseLlmClient);
53
+ expect(mockStrategy2.route).toHaveBeenCalledWith(mockContext, mockConfig, mockBaseLlmClient);
54
+ expect(mockTerminalStrategy.route).not.toHaveBeenCalled();
55
+ expect(result.model).toBe('strategy2-model');
56
+ expect(result.metadata.source).toBe('test-router/strategy2');
57
+ });
58
+ it('should fall back to the terminal strategy if no other strategy provides a decision', async () => {
59
+ const composite = new CompositeStrategy([mockStrategy1, mockStrategy2, mockTerminalStrategy], 'test-router');
60
+ const result = await composite.route(mockContext, mockConfig, mockBaseLlmClient);
61
+ expect(mockStrategy1.route).toHaveBeenCalledTimes(1);
62
+ expect(mockStrategy2.route).toHaveBeenCalledTimes(1);
63
+ expect(mockTerminalStrategy.route).toHaveBeenCalledTimes(1);
64
+ expect(result.model).toBe('terminal-model');
65
+ expect(result.metadata.source).toBe('test-router/terminal');
66
+ });
67
+ it('should handle errors in non-terminal strategies and continue', async () => {
68
+ const consoleErrorSpy = vi
69
+ .spyOn(console, 'error')
70
+ .mockImplementation(() => { });
71
+ vi.spyOn(mockStrategy1, 'route').mockRejectedValue(new Error('Strategy 1 failed'));
72
+ const composite = new CompositeStrategy([mockStrategy1, mockTerminalStrategy], 'test-router');
73
+ const result = await composite.route(mockContext, mockConfig, mockBaseLlmClient);
74
+ expect(consoleErrorSpy).toHaveBeenCalledWith("[Routing] Strategy 'strategy1' failed. Continuing to next strategy. Error:", expect.any(Error));
75
+ expect(result.model).toBe('terminal-model');
76
+ consoleErrorSpy.mockRestore();
77
+ });
78
+ it('should re-throw an error from the terminal strategy', async () => {
79
+ const consoleErrorSpy = vi
80
+ .spyOn(console, 'error')
81
+ .mockImplementation(() => { });
82
+ const terminalError = new Error('Terminal strategy failed');
83
+ vi.spyOn(mockTerminalStrategy, 'route').mockRejectedValue(terminalError);
84
+ const composite = new CompositeStrategy([mockTerminalStrategy]);
85
+ await expect(composite.route(mockContext, mockConfig, mockBaseLlmClient)).rejects.toThrow(terminalError);
86
+ expect(consoleErrorSpy).toHaveBeenCalledWith("[Routing] Critical Error: Terminal strategy 'terminal' failed. Routing cannot proceed. Error:", terminalError);
87
+ consoleErrorSpy.mockRestore();
88
+ });
89
+ it('should correctly finalize the decision metadata', async () => {
90
+ const decision = {
91
+ model: 'some-model',
92
+ metadata: {
93
+ source: 'child-source',
94
+ latencyMs: 50,
95
+ reasoning: 'Child reasoning',
96
+ },
97
+ };
98
+ vi.spyOn(mockStrategy1, 'route').mockResolvedValue(decision);
99
+ const composite = new CompositeStrategy([mockStrategy1, mockTerminalStrategy], 'my-composite');
100
+ const result = await composite.route(mockContext, mockConfig, mockBaseLlmClient);
101
+ expect(result.model).toBe('some-model');
102
+ expect(result.metadata.source).toBe('my-composite/child-source');
103
+ expect(result.metadata.reasoning).toBe('Child reasoning');
104
+ // It should keep the child's latency
105
+ expect(result.metadata.latencyMs).toBe(50);
106
+ });
107
+ it('should calculate total latency if child latency is not provided', async () => {
108
+ const decision = {
109
+ model: 'some-model',
110
+ metadata: {
111
+ source: 'child-source',
112
+ // No latencyMs here
113
+ latencyMs: 0,
114
+ reasoning: 'Child reasoning',
115
+ },
116
+ };
117
+ vi.spyOn(mockStrategy1, 'route').mockResolvedValue(decision);
118
+ const composite = new CompositeStrategy([mockStrategy1, mockTerminalStrategy], 'my-composite');
119
+ const result = await composite.route(mockContext, mockConfig, mockBaseLlmClient);
120
+ expect(result.metadata.latencyMs).toBeGreaterThanOrEqual(0);
121
+ });
122
+ });
123
+ //# sourceMappingURL=compositeStrategy.test.js.map