pi-smart-router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +167 -0
  2. package/.pi/extensions/smart-router/commands.ts +242 -0
  3. package/.pi/extensions/smart-router/dataset-export.ts +142 -0
  4. package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
  5. package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
  6. package/.pi/extensions/smart-router/extension-setup.ts +115 -0
  7. package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
  8. package/.pi/extensions/smart-router/index.ts +115 -0
  9. package/.pi/extensions/smart-router/package.json +3 -0
  10. package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
  11. package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
  12. package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
  13. package/.pi/extensions/smart-router/routing-context.ts +171 -0
  14. package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
  15. package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
  16. package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
  17. package/.pi/extensions/smart-router/types.ts +90 -0
  18. package/.pi/extensions/smart-router/utils.ts +58 -0
  19. package/README.md +593 -0
  20. package/bin/pi-smart-router.mjs +79 -0
  21. package/config/.gitkeep +0 -0
  22. package/config/hydra-projection-weights.json.example +1169 -0
  23. package/config/models.yaml.example +58 -0
  24. package/config/p-success-weights.json.example +19 -0
  25. package/config/routing-calibration.json.example +1605 -0
  26. package/config/routing-centroids.json.example +1570 -0
  27. package/config/routing-clusters.yaml.example +41 -0
  28. package/data/contrib/example.json +25 -0
  29. package/dist/api/explain/router-explain.d.ts +47 -0
  30. package/dist/api/explain/router-explain.d.ts.map +1 -0
  31. package/dist/api/explain/router-explain.js +101 -0
  32. package/dist/api/explain/router-explain.js.map +1 -0
  33. package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
  34. package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
  35. package/dist/api/middleware/pi-router-middleware.js +83 -0
  36. package/dist/api/middleware/pi-router-middleware.js.map +1 -0
  37. package/dist/cli/smart-router-cli.d.ts +85 -0
  38. package/dist/cli/smart-router-cli.d.ts.map +1 -0
  39. package/dist/cli/smart-router-cli.js +293 -0
  40. package/dist/cli/smart-router-cli.js.map +1 -0
  41. package/dist/config/defaults.d.ts +7 -0
  42. package/dist/config/defaults.d.ts.map +1 -0
  43. package/dist/config/defaults.js +33 -0
  44. package/dist/config/defaults.js.map +1 -0
  45. package/dist/config/models-loader.d.ts +22 -0
  46. package/dist/config/models-loader.d.ts.map +1 -0
  47. package/dist/config/models-loader.js +51 -0
  48. package/dist/config/models-loader.js.map +1 -0
  49. package/dist/config/pi-model-mapper.d.ts +40 -0
  50. package/dist/config/pi-model-mapper.d.ts.map +1 -0
  51. package/dist/config/pi-model-mapper.js +216 -0
  52. package/dist/config/pi-model-mapper.js.map +1 -0
  53. package/dist/config/routing-clusters-loader.d.ts +35 -0
  54. package/dist/config/routing-clusters-loader.d.ts.map +1 -0
  55. package/dist/config/routing-clusters-loader.js +100 -0
  56. package/dist/config/routing-clusters-loader.js.map +1 -0
  57. package/dist/domain/delegation/delegation-context.d.ts +23 -0
  58. package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
  59. package/dist/domain/delegation/delegation-context.js +73 -0
  60. package/dist/domain/delegation/delegation-context.js.map +1 -0
  61. package/dist/domain/delegation/execution-ledger.d.ts +19 -0
  62. package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
  63. package/dist/domain/delegation/execution-ledger.js +23 -0
  64. package/dist/domain/delegation/execution-ledger.js.map +1 -0
  65. package/dist/domain/delegation/output-headroom.d.ts +39 -0
  66. package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
  67. package/dist/domain/delegation/output-headroom.js +78 -0
  68. package/dist/domain/delegation/output-headroom.js.map +1 -0
  69. package/dist/domain/matching/cluster-matcher.d.ts +69 -0
  70. package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
  71. package/dist/domain/matching/cluster-matcher.js +294 -0
  72. package/dist/domain/matching/cluster-matcher.js.map +1 -0
  73. package/dist/domain/matching/embedding-provider.d.ts +21 -0
  74. package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
  75. package/dist/domain/matching/embedding-provider.js +42 -0
  76. package/dist/domain/matching/embedding-provider.js.map +1 -0
  77. package/dist/domain/matching/hydra-input.d.ts +18 -0
  78. package/dist/domain/matching/hydra-input.d.ts.map +1 -0
  79. package/dist/domain/matching/hydra-input.js +40 -0
  80. package/dist/domain/matching/hydra-input.js.map +1 -0
  81. package/dist/domain/matching/hydra-matcher.d.ts +103 -0
  82. package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
  83. package/dist/domain/matching/hydra-matcher.js +275 -0
  84. package/dist/domain/matching/hydra-matcher.js.map +1 -0
  85. package/dist/domain/pinning/cache-economics.d.ts +53 -0
  86. package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
  87. package/dist/domain/pinning/cache-economics.js +72 -0
  88. package/dist/domain/pinning/cache-economics.js.map +1 -0
  89. package/dist/domain/pinning/loop-escalation.d.ts +41 -0
  90. package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
  91. package/dist/domain/pinning/loop-escalation.js +137 -0
  92. package/dist/domain/pinning/loop-escalation.js.map +1 -0
  93. package/dist/domain/pinning/session-pinner.d.ts +81 -0
  94. package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
  95. package/dist/domain/pinning/session-pinner.js +237 -0
  96. package/dist/domain/pinning/session-pinner.js.map +1 -0
  97. package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
  98. package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
  99. package/dist/domain/pinning/sub-route-policy.js +72 -0
  100. package/dist/domain/pinning/sub-route-policy.js.map +1 -0
  101. package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
  102. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
  103. package/dist/domain/pipeline/router-pipeline.js +807 -0
  104. package/dist/domain/pipeline/router-pipeline.js.map +1 -0
  105. package/dist/domain/pipeline/safe-default.d.ts +25 -0
  106. package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
  107. package/dist/domain/pipeline/safe-default.js +41 -0
  108. package/dist/domain/pipeline/safe-default.js.map +1 -0
  109. package/dist/domain/refactor-guardrails.d.ts +48 -0
  110. package/dist/domain/refactor-guardrails.d.ts.map +1 -0
  111. package/dist/domain/refactor-guardrails.js +129 -0
  112. package/dist/domain/refactor-guardrails.js.map +1 -0
  113. package/dist/domain/routing/context-fit.d.ts +53 -0
  114. package/dist/domain/routing/context-fit.d.ts.map +1 -0
  115. package/dist/domain/routing/context-fit.js +142 -0
  116. package/dist/domain/routing/context-fit.js.map +1 -0
  117. package/dist/domain/routing/expected-cost.d.ts +63 -0
  118. package/dist/domain/routing/expected-cost.d.ts.map +1 -0
  119. package/dist/domain/routing/expected-cost.js +177 -0
  120. package/dist/domain/routing/expected-cost.js.map +1 -0
  121. package/dist/domain/routing/p-success-classifier.d.ts +130 -0
  122. package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
  123. package/dist/domain/routing/p-success-classifier.js +350 -0
  124. package/dist/domain/routing/p-success-classifier.js.map +1 -0
  125. package/dist/domain/routing/tier-features.d.ts +70 -0
  126. package/dist/domain/routing/tier-features.d.ts.map +1 -0
  127. package/dist/domain/routing/tier-features.js +174 -0
  128. package/dist/domain/routing/tier-features.js.map +1 -0
  129. package/dist/domain/routing/tool-history-guard.d.ts +32 -0
  130. package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
  131. package/dist/domain/routing/tool-history-guard.js +110 -0
  132. package/dist/domain/routing/tool-history-guard.js.map +1 -0
  133. package/dist/domain/scoring/multi-objective.d.ts +48 -0
  134. package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
  135. package/dist/domain/scoring/multi-objective.js +116 -0
  136. package/dist/domain/scoring/multi-objective.js.map +1 -0
  137. package/dist/domain/triage/triage-engine.d.ts +43 -0
  138. package/dist/domain/triage/triage-engine.d.ts.map +1 -0
  139. package/dist/domain/triage/triage-engine.js +317 -0
  140. package/dist/domain/triage/triage-engine.js.map +1 -0
  141. package/dist/domain/triage/turn-envelope.d.ts +17 -0
  142. package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
  143. package/dist/domain/triage/turn-envelope.js +81 -0
  144. package/dist/domain/triage/turn-envelope.js.map +1 -0
  145. package/dist/domain/types/entities.d.ts +303 -0
  146. package/dist/domain/types/entities.d.ts.map +1 -0
  147. package/dist/domain/types/entities.js +6 -0
  148. package/dist/domain/types/entities.js.map +1 -0
  149. package/dist/domain/types/index.d.ts +4 -0
  150. package/dist/domain/types/index.d.ts.map +1 -0
  151. package/dist/domain/types/index.js +2 -0
  152. package/dist/domain/types/index.js.map +1 -0
  153. package/dist/domain/types/schemas.d.ts +346 -0
  154. package/dist/domain/types/schemas.d.ts.map +1 -0
  155. package/dist/domain/types/schemas.js +241 -0
  156. package/dist/domain/types/schemas.js.map +1 -0
  157. package/dist/domain/types/store-port.d.ts +44 -0
  158. package/dist/domain/types/store-port.d.ts.map +1 -0
  159. package/dist/domain/types/store-port.js +6 -0
  160. package/dist/domain/types/store-port.js.map +1 -0
  161. package/dist/index.d.ts +39 -0
  162. package/dist/index.d.ts.map +1 -0
  163. package/dist/index.js +36 -0
  164. package/dist/index.js.map +1 -0
  165. package/dist/infra/gemini-provider.d.ts +28 -0
  166. package/dist/infra/gemini-provider.d.ts.map +1 -0
  167. package/dist/infra/gemini-provider.js +112 -0
  168. package/dist/infra/gemini-provider.js.map +1 -0
  169. package/dist/infra/telemetry.d.ts +51 -0
  170. package/dist/infra/telemetry.d.ts.map +1 -0
  171. package/dist/infra/telemetry.js +100 -0
  172. package/dist/infra/telemetry.js.map +1 -0
  173. package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
  174. package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
  175. package/dist/infrastructure/delegation/provider-error.js +179 -0
  176. package/dist/infrastructure/delegation/provider-error.js.map +1 -0
  177. package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
  178. package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
  179. package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
  180. package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
  181. package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
  182. package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
  183. package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
  184. package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
  185. package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
  186. package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
  187. package/dist/infrastructure/hardware/hardware-probe.js +199 -0
  188. package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
  189. package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
  190. package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
  191. package/dist/infrastructure/local/local-zero-tier.js +92 -0
  192. package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
  193. package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
  194. package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
  195. package/dist/infrastructure/persistence/memory-store.js +87 -0
  196. package/dist/infrastructure/persistence/memory-store.js.map +1 -0
  197. package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
  198. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
  199. package/dist/infrastructure/persistence/sqlite-store.js +658 -0
  200. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
  201. package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
  202. package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
  203. package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
  204. package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
  205. package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
  206. package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
  207. package/dist/infrastructure/pricing/price-broker.js +133 -0
  208. package/dist/infrastructure/pricing/price-broker.js.map +1 -0
  209. package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
  210. package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
  211. package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
  212. package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
  213. package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
  214. package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
  215. package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
  216. package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
  217. package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
  218. package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
  219. package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
  220. package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
  221. package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
  222. package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
  223. package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
  224. package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
  225. package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
  226. package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
  227. package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
  228. package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
  229. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
  230. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
  231. package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
  232. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
  233. package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
  234. package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
  235. package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
  236. package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
  237. package/package.json +110 -0
  238. package/skills/router-backlog-orchestrator/SKILL.md +183 -0
  239. package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
  240. package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
  241. package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
  242. package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
  243. package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
  244. package/src/api/explain/.gitkeep +0 -0
  245. package/src/api/explain/router-explain.ts +146 -0
  246. package/src/api/middleware/.gitkeep +0 -0
  247. package/src/api/middleware/pi-router-middleware.ts +169 -0
  248. package/src/cli/smart-router-cli.ts +436 -0
  249. package/src/config/.gitkeep +0 -0
  250. package/src/config/defaults.ts +35 -0
  251. package/src/config/models-loader.ts +68 -0
  252. package/src/config/pi-model-mapper.ts +285 -0
  253. package/src/config/routing-clusters-loader.ts +148 -0
  254. package/src/domain/delegation/delegation-context.ts +111 -0
  255. package/src/domain/delegation/execution-ledger.ts +34 -0
  256. package/src/domain/delegation/output-headroom.ts +131 -0
  257. package/src/domain/matching/.gitkeep +0 -0
  258. package/src/domain/matching/cluster-matcher.ts +474 -0
  259. package/src/domain/matching/embedding-provider.ts +79 -0
  260. package/src/domain/matching/hydra-input.ts +53 -0
  261. package/src/domain/matching/hydra-matcher.ts +454 -0
  262. package/src/domain/pinning/.gitkeep +0 -0
  263. package/src/domain/pinning/cache-economics.ts +128 -0
  264. package/src/domain/pinning/loop-escalation.ts +181 -0
  265. package/src/domain/pinning/session-pinner.ts +388 -0
  266. package/src/domain/pinning/sub-route-policy.ts +119 -0
  267. package/src/domain/pipeline/.gitkeep +0 -0
  268. package/src/domain/pipeline/router-pipeline.ts +1139 -0
  269. package/src/domain/pipeline/safe-default.ts +70 -0
  270. package/src/domain/refactor-guardrails.ts +202 -0
  271. package/src/domain/routing/context-fit.ts +221 -0
  272. package/src/domain/routing/expected-cost.ts +320 -0
  273. package/src/domain/routing/p-success-classifier.ts +511 -0
  274. package/src/domain/routing/tier-features.ts +290 -0
  275. package/src/domain/routing/tool-history-guard.ts +159 -0
  276. package/src/domain/scoring/.gitkeep +0 -0
  277. package/src/domain/scoring/multi-objective.ts +175 -0
  278. package/src/domain/triage/.gitkeep +0 -0
  279. package/src/domain/triage/triage-engine.ts +373 -0
  280. package/src/domain/triage/turn-envelope.ts +104 -0
  281. package/src/domain/types/.gitkeep +0 -0
  282. package/src/domain/types/entities.ts +384 -0
  283. package/src/domain/types/index.ts +41 -0
  284. package/src/domain/types/schemas.ts +289 -0
  285. package/src/domain/types/store-port.ts +59 -0
  286. package/src/index.ts +103 -0
  287. package/src/infra/gemini-provider.ts +142 -0
  288. package/src/infra/telemetry.ts +169 -0
  289. package/src/infrastructure/delegation/provider-error.ts +251 -0
  290. package/src/infrastructure/gateway/.gitkeep +0 -0
  291. package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
  292. package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
  293. package/src/infrastructure/hardware/.gitkeep +0 -0
  294. package/src/infrastructure/hardware/hardware-probe.ts +276 -0
  295. package/src/infrastructure/local/.gitkeep +0 -0
  296. package/src/infrastructure/local/local-zero-tier.ts +151 -0
  297. package/src/infrastructure/persistence/.gitkeep +0 -0
  298. package/src/infrastructure/persistence/memory-store.ts +122 -0
  299. package/src/infrastructure/persistence/sqlite-store.ts +900 -0
  300. package/src/infrastructure/pricing/.gitkeep +0 -0
  301. package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
  302. package/src/infrastructure/pricing/price-broker.ts +183 -0
  303. package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
  304. package/src/infrastructure/telemetry/.gitkeep +0 -0
  305. package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
  306. package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
  307. package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
  308. package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
  309. package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
  310. package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
@@ -0,0 +1,171 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import {
4
+ type Context,
5
+ type Message,
6
+ type SimpleStreamOptions,
7
+ type TextContent,
8
+ } from '@earendil-works/pi-ai/compat';
9
+
10
+ import type {
11
+ Message as RoutingMessage,
12
+ RoutingRequest,
13
+ TurnType,
14
+ } from '../../../src/domain/types/index.js';
15
+ import { LifecycleHookState } from '../../../src/index.js';
16
+
17
+ const TOKEN_ESTIMATE_KEYS = [
18
+ 'estimatedInputTokens',
19
+ 'estimated_input_tokens',
20
+ 'contextTokens',
21
+ 'tokenCount',
22
+ 'tokens',
23
+ ] as const;
24
+
25
+ function readOptionalTokenEstimate(source: unknown): number | undefined {
26
+ if (source === null || typeof source !== 'object') {
27
+ return undefined;
28
+ }
29
+
30
+ const record = source as Record<string, unknown>;
31
+ for (const key of TOKEN_ESTIMATE_KEYS) {
32
+ const value = record[key];
33
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
34
+ return Math.floor(value);
35
+ }
36
+ }
37
+
38
+ return undefined;
39
+ }
40
+
41
+ function estimateInputTokens(
42
+ context: Context,
43
+ options?: SimpleStreamOptions,
44
+ ): number {
45
+ const fromOptions = readOptionalTokenEstimate(options);
46
+ if (fromOptions !== undefined) {
47
+ return fromOptions;
48
+ }
49
+
50
+ const fromContext = readOptionalTokenEstimate(context);
51
+ if (fromContext !== undefined) {
52
+ return fromContext;
53
+ }
54
+
55
+ const mapped = mapContextMessages(context.messages);
56
+ let charCount = mapped.reduce((sum, message) => sum + message.content.length, 0);
57
+ if (context.systemPrompt) {
58
+ charCount += context.systemPrompt.length;
59
+ }
60
+
61
+ if (context.messages.length === 0 && !context.systemPrompt) {
62
+ return 0;
63
+ }
64
+
65
+ return Math.max(1, Math.ceil(charCount / 4));
66
+ }
67
+
68
+ function messageContentToString(content: string | readonly (TextContent | { type: string })[]): string {
69
+ if (typeof content === 'string') {
70
+ return content;
71
+ }
72
+
73
+ return content
74
+ .filter((block): block is TextContent => block.type === 'text')
75
+ .map((block) => block.text)
76
+ .join('\n');
77
+ }
78
+
79
+ export function extractPromptText(messages: readonly Message[]): string {
80
+ for (let i = messages.length - 1; i >= 0; i--) {
81
+ const message = messages[i];
82
+ if (message?.role === 'user') {
83
+ const text = messageContentToString(message.content);
84
+ if (text.trim()) {
85
+ return text;
86
+ }
87
+ }
88
+ }
89
+ return '';
90
+ }
91
+
92
+ export function deriveTurnType(messages: readonly Message[]): TurnType {
93
+ if (messages.length === 0) {
94
+ return 'unknown';
95
+ }
96
+
97
+ const lastMessage = messages[messages.length - 1];
98
+ if (!lastMessage) {
99
+ return 'unknown';
100
+ }
101
+
102
+ if (lastMessage.role === 'toolResult') {
103
+ return 'tool_result';
104
+ }
105
+
106
+ if (lastMessage.role === 'user') {
107
+ const text = messageContentToString(lastMessage.content).toLowerCase();
108
+ if (
109
+ text.includes('plan') ||
110
+ text.includes('architect') ||
111
+ text.includes('design')
112
+ ) {
113
+ return 'planning';
114
+ }
115
+ }
116
+
117
+ return 'main_loop';
118
+ }
119
+
120
+ export function mapContextMessages(messages: readonly Message[]): RoutingMessage[] {
121
+ return messages.map((message) => {
122
+ if (message.role === 'user') {
123
+ return {
124
+ role: message.role,
125
+ content: messageContentToString(message.content),
126
+ };
127
+ }
128
+
129
+ if (message.role === 'assistant') {
130
+ const content = message.content
131
+ .map((block) => {
132
+ if (block.type === 'text') {
133
+ return block.text;
134
+ }
135
+ if (block.type === 'thinking') {
136
+ return block.thinking;
137
+ }
138
+ return '';
139
+ })
140
+ .filter(Boolean)
141
+ .join('\n');
142
+
143
+ return { role: message.role, content };
144
+ }
145
+
146
+ return {
147
+ role: 'tool',
148
+ content: messageContentToString(message.content),
149
+ tool_blocks: [],
150
+ };
151
+ });
152
+ }
153
+
154
+ export function buildRoutingRequest(
155
+ context: Context,
156
+ options: SimpleStreamOptions | undefined,
157
+ lifecycleHookState?: LifecycleHookState,
158
+ ): RoutingRequest {
159
+ const sessionId = options?.sessionId ?? randomUUID();
160
+ const lifecycleFlags = lifecycleHookState?.consume(sessionId) ?? {};
161
+
162
+ return {
163
+ request_id: randomUUID(),
164
+ session_id: sessionId,
165
+ prompt_text: extractPromptText(context.messages),
166
+ messages: mapContextMessages(context.messages),
167
+ turn_type: deriveTurnType(context.messages),
168
+ estimated_input_tokens: estimateInputTokens(context, options),
169
+ ...lifecycleFlags,
170
+ };
171
+ }
@@ -0,0 +1,44 @@
1
+ import type { RoutingDecision, RoutingRequest } from '../../../src/domain/types/index.js';
2
+ import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
3
+ import type { StreamDelegationDeps } from './types.js';
4
+
5
+ export function capturePreRouteOutcomes(
6
+ request: RoutingRequest,
7
+ deps: StreamDelegationDeps,
8
+ priorSnapshot: SessionRoutingSnapshot | undefined,
9
+ hadPin: boolean,
10
+ ): void {
11
+ if (!priorSnapshot || !deps.outcomeRecorder) {
12
+ return;
13
+ }
14
+
15
+ const sessionId = request.session_id;
16
+
17
+ if (request.compaction_flag && hadPin) {
18
+ deps.outcomeRecorder.recordCompactionPinBreak(priorSnapshot, sessionId);
19
+ }
20
+
21
+ if (request.force_model_id) {
22
+ deps.outcomeRecorder.recordModelOverride(
23
+ priorSnapshot,
24
+ sessionId,
25
+ request.force_model_id,
26
+ );
27
+ }
28
+ }
29
+
30
+ export function updateSessionRoutingSnapshot(
31
+ deps: StreamDelegationDeps,
32
+ sessionId: string | undefined,
33
+ request: RoutingRequest,
34
+ decision: RoutingDecision,
35
+ ): void {
36
+ if (!sessionId || !deps.sessionRouting) {
37
+ return;
38
+ }
39
+
40
+ deps.sessionRouting.set(sessionId, {
41
+ lastRequestId: request.request_id,
42
+ lastSelectedModelId: decision.selected_model_id,
43
+ });
44
+ }
@@ -0,0 +1,140 @@
1
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+
3
+ import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
4
+ import {
5
+ bindSharedModelRegistry,
6
+ ensureFleetFresh,
7
+ formatLmuStatus,
8
+ rebuildFleet,
9
+ } from './fleet-bootstrap.js';
10
+ import { notifyPricingStalenessIfNeeded } from './pricing-lifecycle.js';
11
+ import type { FleetMode, SmartRouterRuntime } from './types.js';
12
+
13
+ export const FLEET_MODE_ENTRY_TYPE = 'smart-router-fleet-mode' as const;
14
+
15
+ const SMART_ROUTER_PROVIDER = 'smart-router' as const;
16
+ const SMART_ROUTER_AUTO_ID = 'auto' as const;
17
+
18
+ export function isSmartRouterActive(model: { provider: string; id: string }): boolean {
19
+ return model.provider === SMART_ROUTER_PROVIDER && model.id === SMART_ROUTER_AUTO_ID;
20
+ }
21
+
22
+ function parseFleetModeEntry(data: unknown): FleetMode | undefined {
23
+ if (
24
+ typeof data === 'object' &&
25
+ data !== null &&
26
+ 'mode' in data &&
27
+ (data.mode === 'scoped' || data.mode === 'all')
28
+ ) {
29
+ return data.mode;
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ export function restoreFleetModeFromSession(ctx: ExtensionContext): FleetMode | undefined {
35
+ const entries = ctx.sessionManager.getEntries();
36
+ for (let i = entries.length - 1; i >= 0; i--) {
37
+ const entry = entries[i];
38
+ if (entry?.type === 'custom' && entry.customType === FLEET_MODE_ENTRY_TYPE) {
39
+ return parseFleetModeEntry(entry.data);
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ function restoreLmuFromLedger(runtime: SmartRouterRuntime, sessionId: string): void {
46
+ const lastExec = runtime.executionLedger.getLastExecution(sessionId);
47
+ if (lastExec) {
48
+ runtime.setLmuStatus?.(lastExec.id);
49
+ } else if (runtime.lastDecision) {
50
+ runtime.setLmuStatus?.(runtime.lastDecision.selected_model_id);
51
+ }
52
+ }
53
+
54
+ function wireLmuStatusHandlers(
55
+ runtime: SmartRouterRuntime,
56
+ ctx: ExtensionContext,
57
+ getActiveModel: () => { provider: string; id: string },
58
+ ): void {
59
+ runtime.setLmuStatus = (modelId) => {
60
+ if (!isSmartRouterActive(getActiveModel())) {
61
+ return;
62
+ }
63
+ ctx.ui.setStatus(
64
+ 'smart-router-lmu',
65
+ formatLmuStatus(modelId, ctx.ui.theme as { fg: (color: string, text: string) => string }),
66
+ );
67
+ };
68
+ runtime.clearLmuStatus = () => {
69
+ ctx.ui.setStatus('smart-router-lmu', undefined);
70
+ };
71
+ }
72
+
73
+ export function setupSessionHooks(
74
+ pi: ExtensionAPI,
75
+ runtime: SmartRouterRuntime,
76
+ sessionPinner: SessionPinner,
77
+ datasetNotify: { fn: ((message: string) => void) | undefined },
78
+ ): void {
79
+ let activeModel: { provider: string; id: string } | undefined;
80
+
81
+ pi.on('session_start', async (_event, ctx) => {
82
+ activeModel = ctx.model;
83
+ bindSharedModelRegistry(runtime, ctx.modelRegistry);
84
+ runtime.sessionCwd = ctx.cwd;
85
+ runtime.streamDeps.ensureFleetFresh = async () => {
86
+ if (runtime.sessionCwd === undefined) {
87
+ return;
88
+ }
89
+ await ensureFleetFresh(runtime, pi, runtime.sessionCwd);
90
+ };
91
+
92
+ const restoredMode = restoreFleetModeFromSession(ctx);
93
+ if (restoredMode) {
94
+ runtime.fleetMode = restoredMode;
95
+ }
96
+ await rebuildFleet(runtime, pi, ctx.cwd);
97
+
98
+ notifyPricingStalenessIfNeeded(runtime, (message, level) => {
99
+ ctx.ui.notify(message, level);
100
+ });
101
+
102
+ wireLmuStatusHandlers(runtime, ctx, () => {
103
+ const model = activeModel ?? ctx.model;
104
+ return model ?? { provider: '', id: '' };
105
+ });
106
+ runtime.notifyDatasetEnabled = (message) => {
107
+ ctx.ui.notify(message, 'info');
108
+ };
109
+ datasetNotify.fn = runtime.notifyDatasetEnabled;
110
+
111
+ const sessionId = ctx.sessionManager.getSessionId();
112
+ await sessionPinner.restoreSessionPin(sessionId);
113
+
114
+ if (ctx.model !== undefined && isSmartRouterActive(ctx.model)) {
115
+ restoreLmuFromLedger(runtime, sessionId);
116
+ } else {
117
+ runtime.clearLmuStatus?.();
118
+ }
119
+ });
120
+
121
+ pi.on('model_select', (event, ctx) => {
122
+ activeModel = event.model;
123
+ if (isSmartRouterActive(event.model)) {
124
+ restoreLmuFromLedger(runtime, ctx.sessionManager.getSessionId());
125
+ } else {
126
+ runtime.clearLmuStatus?.();
127
+ }
128
+ });
129
+
130
+ pi.on('session_shutdown', async (_event, ctx) => {
131
+ activeModel = undefined;
132
+ delete runtime.setLmuStatus;
133
+ delete runtime.clearLmuStatus;
134
+ delete runtime.notifyDatasetEnabled;
135
+ delete runtime.sessionCwd;
136
+ delete runtime.streamDeps.ensureFleetFresh;
137
+ datasetNotify.fn = undefined;
138
+ ctx.ui.setStatus('smart-router-lmu', undefined);
139
+ });
140
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ type Api,
3
+ type AssistantMessageEventStream,
4
+ type Context,
5
+ type Model,
6
+ type SimpleStreamOptions,
7
+ createAssistantMessageEventStream,
8
+ } from '@earendil-works/pi-ai/compat';
9
+
10
+ import type { RoutingDecision, RoutingFeatureSidecar } from '../../../src/domain/types/index.js';
11
+ import { createErrorMessage } from './delegation-runtime.js';
12
+ import { routeAndDelegate } from './route-and-delegate.js';
13
+ import type { StreamDelegationDeps } from './types.js';
14
+
15
+ function isRoutingLogEnabled(): boolean {
16
+ return process.env.SMART_ROUTER_LOG_ROUTING === '1';
17
+ }
18
+
19
+ export function logRoutingDecision(
20
+ decision: RoutingDecision,
21
+ delegate?: { provider: string; modelId: string; api: Api },
22
+ ): void {
23
+ if (!isRoutingLogEnabled()) {
24
+ return;
25
+ }
26
+
27
+ console.warn(
28
+ '[smart-router] routing decision',
29
+ JSON.stringify({
30
+ request_id: decision.request_id,
31
+ selected_model_id: decision.selected_model_id,
32
+ tier: decision.tier,
33
+ stage: decision.stage,
34
+ reason_code: decision.reason_code,
35
+ routing_latency_ms: decision.routing_latency_ms,
36
+ features: decision.features ?? null,
37
+ delegate,
38
+ }),
39
+ );
40
+ }
41
+
42
+ /** Read dataset feature sidecar from a routing decision (SP-057). */
43
+ export function getRoutingFeatureSidecar(
44
+ decision: RoutingDecision,
45
+ ): RoutingFeatureSidecar | undefined {
46
+ return decision.features;
47
+ }
48
+
49
+ export function createStreamSimple(deps: StreamDelegationDeps) {
50
+ return function streamSimple(
51
+ model: Model<Api>,
52
+ context: Context,
53
+ options?: SimpleStreamOptions,
54
+ ): AssistantMessageEventStream {
55
+ const stream = createAssistantMessageEventStream();
56
+
57
+ void (async () => {
58
+ try {
59
+ await routeAndDelegate(context, options, deps, stream);
60
+ } catch (error) {
61
+ stream.push({
62
+ type: 'error',
63
+ reason: options?.signal?.aborted ? 'aborted' : 'error',
64
+ error: createErrorMessage(model, options, error),
65
+ });
66
+ stream.end();
67
+ }
68
+ })();
69
+
70
+ return stream;
71
+ };
72
+ }
73
+
74
+ export { resolveDelegationOptions, buildDelegationContext } from './delegation-runtime.js';
75
+ export { resolveTargetModel } from './route-and-delegate.js';
@@ -0,0 +1,90 @@
1
+ import type {
2
+ Api,
3
+ AssistantMessageEventStream,
4
+ Context,
5
+ Model,
6
+ SimpleStreamOptions,
7
+ } from '@earendil-works/pi-ai/compat';
8
+ import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
9
+
10
+ import type { HydraMatcher } from '../../../src/domain/matching/hydra-matcher.js';
11
+ import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
12
+ import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
13
+ import type {
14
+ ModelProfile,
15
+ PriceCatalog,
16
+ RoutingDecision,
17
+ } from '../../../src/domain/types/index.js';
18
+ import type { StorePort } from '../../../src/domain/types/store-port.js';
19
+ import {
20
+ DatasetRecorder,
21
+ } from '../../../src/infrastructure/telemetry/dataset-recorder.js';
22
+ import {
23
+ OutcomeRecorder,
24
+ type SessionRoutingSnapshot,
25
+ } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
26
+ import {
27
+ LifecycleHookState,
28
+ type RouterHandle,
29
+ } from '../../../src/index.js';
30
+
31
+ export type FleetMode = 'scoped' | 'all';
32
+
33
+ export type SmartRouterCommand =
34
+ | { command: 'status' }
35
+ | { command: 'history'; limit: number }
36
+ | { command: 'mode'; mode: FleetMode }
37
+ | { command: 'pricing'; subcommand: 'refresh' }
38
+ | { command: 'export'; subcommand: 'dataset'; limit: number }
39
+ | { command: 'export'; subcommand: 'telemetry-contrib'; limit: number }
40
+ | { command: 'feedback'; rating: 'good' | 'bad' }
41
+ | { command: 'unpin' };
42
+
43
+ /** Provider stream delegate; defaults to pi-ai streamSimple when omitted. */
44
+ export type DelegateStreamFn = (
45
+ model: Model<Api>,
46
+ context: Context,
47
+ options?: SimpleStreamOptions,
48
+ ) => AssistantMessageEventStream;
49
+
50
+ export interface StreamDelegationDeps {
51
+ router: RouterHandle;
52
+ modelRegistry: ModelRegistry;
53
+ fleet: ModelProfile[];
54
+ /** Cheap scope fingerprint check before each routed turn. */
55
+ ensureFleetFresh?: () => Promise<void>;
56
+ readonly executionLedger: ExecutionLedger;
57
+ /** Injectable for tests; production uses pi-ai streamSimple. */
58
+ delegateStream?: DelegateStreamFn;
59
+ readonly lifecycleHookState?: LifecycleHookState;
60
+ readonly datasetRecorder?: DatasetRecorder;
61
+ readonly outcomeRecorder?: OutcomeRecorder;
62
+ readonly sessionPinner?: SessionPinner;
63
+ readonly sessionRouting?: Map<string, SessionRoutingSnapshot>;
64
+ onRoutingDecision?: (decision: RoutingDecision) => void;
65
+ /** Fired when a delegated provider stream completes successfully. */
66
+ onDelegatedModel?: (model: { readonly provider: string; readonly id: string }) => void;
67
+ }
68
+
69
+ export interface SmartRouterRuntime {
70
+ fleetMode: FleetMode;
71
+ lastDecision: RoutingDecision | undefined;
72
+ priceCatalog: PriceCatalog | null;
73
+ /** Cached scope fingerprint; rebuild when this changes. */
74
+ fleetScopeFingerprint?: string;
75
+ /** Session cwd for ensureFleetFresh before routed turns. */
76
+ sessionCwd?: string;
77
+ modelRegistry: ModelRegistry;
78
+ readonly store: StorePort;
79
+ readonly sessionPinner: SessionPinner;
80
+ readonly executionLedger: ExecutionLedger;
81
+ readonly lifecycleHookState: LifecycleHookState;
82
+ readonly datasetRecorder?: DatasetRecorder;
83
+ readonly outcomeRecorder?: OutcomeRecorder;
84
+ readonly sessionRouting: Map<string, SessionRoutingSnapshot>;
85
+ streamDeps: StreamDelegationDeps;
86
+ hydraMatcher: HydraMatcher | undefined;
87
+ setLmuStatus?: (modelId: string) => void;
88
+ clearLmuStatus?: () => void;
89
+ notifyDatasetEnabled?: (message: string) => void;
90
+ }
@@ -0,0 +1,58 @@
1
+ import { join } from 'node:path';
2
+
3
+ import {
4
+ createResilientStore,
5
+ SqliteStore,
6
+ SqliteStoreError,
7
+ } from '../../../src/infrastructure/persistence/sqlite-store.js';
8
+ import type { StorePort } from '../../../src/domain/types/store-port.js';
9
+ import type { RateLimitPort } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
10
+
11
+ export const DEFAULT_ROUTER_STATE_DB_PATH = '.pi-smart-router/state.db';
12
+ export const DEFAULT_RATE_LIMIT_MAX_TOKENS = 60;
13
+ export const DEFAULT_RATE_LIMIT_REFILL_RATE = 1;
14
+
15
+ export function getRouterStateDbPath(cwd: string): string {
16
+ const configured = process.env.ROUTER_STATE_DB_PATH?.trim();
17
+ if (configured && configured.length > 0) {
18
+ return configured;
19
+ }
20
+ return join(cwd, DEFAULT_ROUTER_STATE_DB_PATH);
21
+ }
22
+
23
+ export function createExtensionStore(cwd: string): StorePort {
24
+ return createResilientStore({
25
+ dbPath: getRouterStateDbPath(cwd),
26
+ models: [],
27
+ }).store;
28
+ }
29
+
30
+ export function createSqliteRateLimiter(sqliteStore: SqliteStore): RateLimitPort {
31
+ return {
32
+ consumeToken(key: string, cost = 1) {
33
+ try {
34
+ return sqliteStore.consumeToken(key, cost);
35
+ } catch (error) {
36
+ if (
37
+ error instanceof SqliteStoreError &&
38
+ error.message.includes('Token bucket not found')
39
+ ) {
40
+ sqliteStore.initBucket(
41
+ key,
42
+ DEFAULT_RATE_LIMIT_MAX_TOKENS,
43
+ DEFAULT_RATE_LIMIT_REFILL_RATE,
44
+ );
45
+ return sqliteStore.consumeToken(key, cost);
46
+ }
47
+ throw error;
48
+ }
49
+ },
50
+ };
51
+ }
52
+
53
+ export function resolveRateLimiter(store: StorePort): RateLimitPort | undefined {
54
+ if (!(store instanceof SqliteStore)) {
55
+ return undefined;
56
+ }
57
+ return createSqliteRateLimiter(store);
58
+ }