@polderlabs/bizar 6.3.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/.claude/hooks/sessionend-recall.mjs +3 -3
  2. package/.claude/hooks/worker-suggest.mjs +110 -0
  3. package/.claude/settings.json +9 -0
  4. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-CW6fWKds.js} +3 -3
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js.map → EnvVarsSection-CW6fWKds.js.map} +1 -1
  6. package/bizar-dash/dist/assets/MobileChat-D7zRhU9K.js +1 -0
  7. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-D7zRhU9K.js.map} +1 -1
  8. package/bizar-dash/dist/assets/MobileSettings-K-sNsn_r.js +1 -0
  9. package/bizar-dash/dist/assets/{MobileSettings-CEQNJNLJ.js.map → MobileSettings-K-sNsn_r.js.map} +1 -1
  10. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  12. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  14. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  16. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  17. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  18. package/bizar-dash/dist/assets/main-C-Md3DaY.css +1 -0
  19. package/bizar-dash/dist/assets/main-CO8fIxQK.js +18 -0
  20. package/bizar-dash/dist/assets/main-CO8fIxQK.js.map +1 -0
  21. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  22. package/bizar-dash/dist/assets/{markdown-tOLaD6nm.js.map → markdown-w2bDDkAH.js.map} +1 -1
  23. package/bizar-dash/dist/assets/mobile-DXYCA0hC.js +1 -0
  24. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DXYCA0hC.js.map} +1 -1
  25. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js +2 -0
  26. package/bizar-dash/dist/assets/mobile-layout-DSmtNPja.js.map +1 -0
  27. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  28. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  29. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  30. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js +2 -0
  31. package/bizar-dash/dist/assets/useSlashCommands-3DFLOfpk.js.map +1 -0
  32. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  33. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  34. package/bizar-dash/dist/index.html +12 -10
  35. package/bizar-dash/dist/mobile.html +7 -4
  36. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  37. package/bizar-dash/src/server/api.mjs +11 -0
  38. package/bizar-dash/src/server/memory-consolidator.mjs +218 -0
  39. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  40. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  41. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  42. package/bizar-dash/src/web/App.tsx +4 -0
  43. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  44. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  45. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  46. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  47. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  48. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  49. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  51. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  52. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  53. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  54. package/bizar-dash/src/web/lib/types.ts +25 -0
  55. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  56. package/bizar-dash/src/web/styles/main.css +251 -0
  57. package/bizar-dash/src/web/styles/tasks-redesign.css +231 -0
  58. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  59. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  60. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  61. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  62. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  63. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  64. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  65. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  66. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  67. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  68. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  69. package/bizar-dash/src/web/ui/controls/controls.css +454 -0
  70. package/bizar-dash/src/web/ui/controls/index.ts +33 -0
  71. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  72. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  73. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  74. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  75. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  76. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  77. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  78. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  79. package/bizar-dash/src/web/ui/data/data.css +535 -0
  80. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  81. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  82. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  83. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  84. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  85. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  86. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  87. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  88. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  89. package/bizar-dash/src/web/ui/index.ts +56 -0
  90. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  91. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  92. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  93. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  94. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  95. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  96. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  97. package/bizar-dash/src/web/ui/layout/index.ts +33 -0
  98. package/bizar-dash/src/web/ui/layout/layout.css +413 -0
  99. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  100. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  101. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  102. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  103. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  104. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  105. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  106. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  107. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  108. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  109. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  110. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  111. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  112. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  113. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  114. package/bizar-dash/src/web/ui/styles/tokens.css +195 -0
  115. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  116. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  117. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  118. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  119. package/bizar-dash/src/web/views/Agents.tsx +506 -266
  120. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  121. package/bizar-dash/src/web/views/Overview.tsx +996 -448
  122. package/bizar-dash/src/web/views/Tasks.tsx +277 -273
  123. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  124. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  125. package/bizar-dash/tests/event-log.test.tsx +61 -0
  126. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  127. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  128. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  129. package/bizar-dash/tests/setup.ts +31 -1
  130. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  131. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  132. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  133. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  134. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  135. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  136. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  137. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  138. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  139. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  140. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  141. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  142. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  143. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  144. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  145. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  146. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  147. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  148. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  149. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  151. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  152. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  153. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  154. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  155. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  156. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  157. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  158. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  159. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  160. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  161. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  163. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  164. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  165. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  166. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  167. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  168. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  169. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  170. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  171. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  172. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  173. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  174. package/bizar-dash/vitest.config.ts +3 -1
  175. package/cli/__tests__/cost-gate.test.mjs +301 -0
  176. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  177. package/cli/bin.mjs +38 -0
  178. package/cli/commands/claim.mjs +282 -0
  179. package/cli/commands/cost.mjs +279 -0
  180. package/cli/cost-gate.mjs +501 -0
  181. package/cli/feature-list-bridge.mjs +400 -0
  182. package/cli/memory-constants.mjs +17 -0
  183. package/cli/worker-dispatcher.mjs +259 -0
  184. package/cli/worker-dispatcher.test.mjs +204 -0
  185. package/config/trigger-patterns.json +186 -0
  186. package/package.json +2 -2
  187. package/packages/sdk/package.json +1 -1
  188. package/packages/sdk/src/agent-registry.ts +304 -0
  189. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  190. package/packages/sdk/src/consensus/index.ts +147 -0
  191. package/packages/sdk/src/consensus/queen.ts +173 -0
  192. package/packages/sdk/src/consensus/types.ts +199 -0
  193. package/packages/sdk/src/federation/audit.ts +156 -0
  194. package/packages/sdk/src/federation/budget.ts +269 -0
  195. package/packages/sdk/src/federation/envelope.ts +144 -0
  196. package/packages/sdk/src/federation/hmac.ts +160 -0
  197. package/packages/sdk/src/federation/index.ts +443 -0
  198. package/packages/sdk/src/federation/pii.ts +241 -0
  199. package/packages/sdk/src/federation/policy.ts +112 -0
  200. package/packages/sdk/src/federation/trust.ts +146 -0
  201. package/packages/sdk/src/index.ts +67 -0
  202. package/packages/sdk/src/mcp/server.ts +351 -1
  203. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  204. package/packages/sdk/src/router/index.ts +247 -0
  205. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  206. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  207. package/packages/sdk/src/router/model-router.ts +235 -0
  208. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  209. package/packages/sdk/src/swarm-topology.ts +279 -0
  210. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  211. package/packages/sdk/tests/consensus.test.ts +376 -0
  212. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  213. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  214. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  215. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  216. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  217. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  218. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  219. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  220. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  221. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  222. package/packages/sdk/tests/model-router.test.mjs +108 -0
  223. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  224. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  225. package/packages/sdk/tests/router.test.mjs +91 -0
  226. package/packages/sdk/tests/sdk.test.mjs +26 -2
  227. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  228. package/scripts/bh-full-e2e.mjs +11 -15
  229. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  230. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  231. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  232. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  233. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  234. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  235. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  236. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  237. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  238. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  239. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  240. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  241. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  242. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  243. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  244. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  245. package/packages/sdk/package-lock.json +0 -32
@@ -0,0 +1,235 @@
1
+ /**
2
+ * router/model-router.ts — Adaptive model-tier router.
3
+ *
4
+ * v6.4.0 — ported from ruflo `model-router.ts` (Thompson-sampling bandit,
5
+ * ADR-026 + ADR-143). Trims the surface to what Bizar needs:
6
+ *
7
+ * - 3 tiers: `flash` (cheap), `mid` (default), `expensive`
8
+ * - Per-tier Beta(α, β) priors, updated by `recordOutcome()`.
9
+ * - Marsaglia-Tsang Gamma sampler + Box-Muller normal → sample from Beta
10
+ * by drawing two Gamma(shape, 1) variates and dividing.
11
+ * - Tier-1 codemod short-circuit: if `detectCodemodIntent()` says the
12
+ * prompt is codemod-eligible, we return `{tier: 'flash'}` with
13
+ * confidence 1.0 and tag `codemodIntent` on the result. This is the
14
+ * $0 path — no model call required.
15
+ *
16
+ * The tier names match Bizar's actual model lineup
17
+ * (`m2.7-flash`, `m2.7`, `m3`). The ruflo vocabulary uses
18
+ * `haiku / sonnet / opus`; we rename to `flash / mid / expensive`
19
+ * so future call-site code reads naturally.
20
+ */
21
+
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
23
+ import { dirname, isAbsolute, resolve } from "node:path";
24
+
25
+ import { detectCodemodIntent } from "./codemod-intent.js";
26
+
27
+ export type ModelTier = "flash" | "mid" | "expensive";
28
+
29
+ export interface RouteDecision {
30
+ tier: ModelTier;
31
+ confidence: number;
32
+ codemodIntent?: "var-to-const" | "remove-console" | "add-logging";
33
+ }
34
+
35
+ export interface BetaPrior {
36
+ alpha: number;
37
+ beta: number;
38
+ }
39
+
40
+ const TIERS: ModelTier[] = ["flash", "mid", "expensive"];
41
+
42
+ /**
43
+ * Default priors — slight optimism for `flash` (so it gets tried for
44
+ * cheap prompts), uniform for `mid`, slight pessimism for `expensive`
45
+ * (it should have to earn its keep). Adjust here, not at the call site.
46
+ */
47
+ const DEFAULT_PRIORS: Record<ModelTier, BetaPrior> = {
48
+ flash: { alpha: 2, beta: 1 }, // mean 2/3 ≈ 0.67
49
+ mid: { alpha: 1, beta: 1 }, // mean 0.5
50
+ expensive: { alpha: 1, beta: 2 }, // mean 1/3 ≈ 0.33
51
+ };
52
+
53
+ export class ModelRouter {
54
+ private priors: Record<ModelTier, BetaPrior>;
55
+ private rngState: number;
56
+
57
+ constructor(opts?: { priors?: Partial<Record<ModelTier, BetaPrior>>; seed?: number }) {
58
+ this.priors = {
59
+ flash: { ...DEFAULT_PRIORS.flash, ...(opts?.priors?.flash ?? {}) },
60
+ mid: { ...DEFAULT_PRIORS.mid, ...(opts?.priors?.mid ?? {}) },
61
+ expensive: { ...DEFAULT_PRIORS.expensive, ...(opts?.priors?.expensive ?? {}) },
62
+ };
63
+ // LCG seed for reproducible sampling in tests. Default uses
64
+ // Math.random() when seed is not provided.
65
+ this.rngState = (opts?.seed ?? 0) | 0;
66
+ }
67
+
68
+ /** Read-only access for telemetry / tests. */
69
+ getPriors(): Record<ModelTier, BetaPrior> {
70
+ return {
71
+ flash: { ...this.priors.flash },
72
+ mid: { ...this.priors.mid },
73
+ expensive: { ...this.priors.expensive },
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Pick a tier for `prompt`. Codemod-eligible prompts short-circuit
79
+ * to `flash` (the Tier-1 $0 path). Otherwise we Thompson-sample each
80
+ * tier and return the highest sample, with confidence = max_sample
81
+ * (the posterior mean is `α/(α+β)`, and the max-sampled value is a
82
+ * lower bound on the probability that this tier beats the others).
83
+ */
84
+ route(prompt: string): RouteDecision {
85
+ // Tier-1 codemod short-circuit ($0).
86
+ const cm = detectCodemodIntent(prompt);
87
+ if (cm !== null) {
88
+ return { tier: "flash", confidence: 1.0, codemodIntent: cm.intent };
89
+ }
90
+
91
+ let best: { tier: ModelTier; sample: number } | null = null;
92
+ for (const tier of TIERS) {
93
+ const prior = this.priors[tier];
94
+ const sample = this.sampleBeta(prior.alpha, prior.beta);
95
+ if (best === null || sample > best.sample) {
96
+ best = { tier, sample };
97
+ }
98
+ }
99
+ const confidence = clamp01(best ? best.sample : 0.5);
100
+ return { tier: best ? best.tier : "mid", confidence };
101
+ }
102
+
103
+ /**
104
+ * Update the per-tier prior after observing the outcome of a call.
105
+ * `success === true` increments α; `success === false` increments β.
106
+ * Cost-adjusted (ruflo ADR-026 §BANDIT_REWARDS):
107
+ * - flash-success: +1.0 → α++ (cheap wins are gold)
108
+ * - mid-success: +0.7 → α += 0.7 (still good)
109
+ * - expensive-success:+0.4 → α += 0.4 (expensive wins are wasteful)
110
+ * - any failure: β ++
111
+ *
112
+ * The fractional updates work fine with the Beta sampler.
113
+ */
114
+ recordOutcome(tier: ModelTier, success: boolean): void {
115
+ const prior = this.priors[tier];
116
+ if (!prior) return;
117
+ if (success) {
118
+ const reward = tier === "flash" ? 1.0 : tier === "mid" ? 0.7 : 0.4;
119
+ prior.alpha += reward;
120
+ } else {
121
+ prior.beta += 1;
122
+ }
123
+ }
124
+
125
+ // -----------------------------------------------------------------
126
+ // Sampling primitives (lifted from ruflo model-router.ts).
127
+ // Marsaglia-Tsang Gamma + Box-Muller normal. Module-local so we
128
+ // don't pull a stats library for one sampler.
129
+ // -----------------------------------------------------------------
130
+
131
+ /** Seeded or Math.random()-backed uniform in [0,1). */
132
+ private random(): number {
133
+ if (this.rngState !== 0) {
134
+ // LCG (Numerical Recipes). Period ~2^32.
135
+ this.rngState = (this.rngState * 1664525 + 1013904223) | 0;
136
+ const u = (this.rngState >>> 0) / 0xffffffff;
137
+ return u || 1e-12;
138
+ }
139
+ return Math.random() || 1e-12;
140
+ }
141
+
142
+ /** Box-Muller standard normal sample. */
143
+ private sampleStandardNormal(): number {
144
+ const u1 = this.random();
145
+ const u2 = this.random();
146
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
147
+ }
148
+
149
+ /** Marsaglia-Tsang Gamma sample for shape α≥1 (with boost for α<1). */
150
+ private sampleGamma(alpha: number): number {
151
+ if (alpha < 1) {
152
+ const u = this.random();
153
+ return this.sampleGamma(alpha + 1) * Math.pow(u, 1 / alpha);
154
+ }
155
+ const d = alpha - 1 / 3;
156
+ const c = 1 / Math.sqrt(9 * d);
157
+ // eslint-disable-next-line no-constant-condition
158
+ while (true) {
159
+ let x = this.sampleStandardNormal();
160
+ let v = 1 + c * x;
161
+ if (v <= 0) continue;
162
+ v = v * v * v;
163
+ const u = this.random();
164
+ const xx = x * x;
165
+ if (u < 1 - 0.0331 * xx * xx) return d * v;
166
+ if (Math.log(u) < 0.5 * xx + d * (1 - v + Math.log(v))) return d * v;
167
+ }
168
+ }
169
+
170
+ /** Sample from Beta(α, β) via two Gamma draws. */
171
+ private sampleBeta(alpha: number, beta: number): number {
172
+ const x = this.sampleGamma(alpha);
173
+ const y = this.sampleGamma(beta);
174
+ const denom = x + y;
175
+ if (denom <= 0) return alpha / (alpha + beta);
176
+ return x / denom;
177
+ }
178
+
179
+ // -----------------------------------------------------------------
180
+ // Persistence (ADR-174 invariant: router state survives restarts).
181
+ // We avoid native deps — `.harness/router-state.json` is the
182
+ // canonical store; loading is best-effort and falls back to
183
+ // defaults if the file is missing or corrupt.
184
+ // -----------------------------------------------------------------
185
+
186
+ /** JSON snapshot of the current per-tier priors + RNG seed. */
187
+ snapshot(): RouterStateSnapshot {
188
+ return {
189
+ version: 1,
190
+ priors: this.getPriors(),
191
+ rngSeed: this.rngState,
192
+ };
193
+ }
194
+
195
+ /** Restore from a snapshot; defaults applied where fields are missing. */
196
+ static fromSnapshot(snap: Partial<RouterStateSnapshot>): ModelRouter {
197
+ return new ModelRouter({
198
+ priors: snap.priors,
199
+ seed: snap.rngSeed,
200
+ });
201
+ }
202
+
203
+ /** Persist state to `path`. Creates parent dirs as needed. */
204
+ saveTo(path: string): void {
205
+ const fp = isAbsolute(path) ? path : resolve(path);
206
+ mkdirSync(dirname(fp), { recursive: true });
207
+ writeFileSync(fp, JSON.stringify(this.snapshot(), null, 2), "utf-8");
208
+ }
209
+
210
+ /** Load state from `path`. Returns a default router if file is
211
+ * missing or unparseable. Does NOT throw. */
212
+ static loadFrom(path: string): ModelRouter {
213
+ try {
214
+ if (!existsSync(path)) return new ModelRouter();
215
+ const raw = readFileSync(path, "utf-8");
216
+ const snap = JSON.parse(raw) as Partial<RouterStateSnapshot>;
217
+ return ModelRouter.fromSnapshot(snap);
218
+ } catch {
219
+ return new ModelRouter();
220
+ }
221
+ }
222
+ }
223
+
224
+ export interface RouterStateSnapshot {
225
+ version: number;
226
+ priors: Record<ModelTier, BetaPrior>;
227
+ rngSeed: number;
228
+ }
229
+
230
+ function clamp01(n: number): number {
231
+ if (Number.isNaN(n)) return 0;
232
+ if (n < 0) return 0;
233
+ if (n > 1) return 1;
234
+ return n;
235
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * router/q-learning-router.ts — Q-learning agent router.
3
+ *
4
+ * v6.4.0 — ported from ruflo `q-learning-router.ts` (8-action softmax
5
+ * agent-route over coder / tester / reviewer / …).
6
+ *
7
+ * Bizar has more than 8 named agents, so the action set is the 8 most
8
+ * commonly delegated targets from `Agent` tool calls (matches
9
+ * `.claude/agents/*.md` to first 8 names). Adding the full
10
+ * 12-agent space could be done in a follow-up but each new agent
11
+ * increases state-action coverage cost; 8 keeps the bandit honest.
12
+ *
13
+ * - State: a 64-dim bag-of-words hash of the task string (FNV-1a
14
+ * folded into 64 buckets). LRU-cached so repeated task patterns
15
+ * are O(1).
16
+ * - Action: one of `odin|frigg|vor|mimir|heimdall|thor|tyr|forseti`.
17
+ * - Reward: `+1` on `recordOutcome(agent, true)`, `0` on false.
18
+ * - Exploration: ε-greedy with ε = 0.1 (configurable).
19
+ *
20
+ * `selectAgent()` returns `{agent, confidence}`. `confidence` is the
21
+ * posterior mean of the picked action's Q-value clamped to [0,1].
22
+ *
23
+ * Tiny surface area by design — the
24
+ * `AgentRouteDecision` shape is what `model_route` /
25
+ * `agent_route` MCP tools expose.
26
+ */
27
+
28
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
29
+ import { dirname, isAbsolute, resolve } from "node:path";
30
+
31
+ import { detectCodemodIntent } from "./codemod-intent.js";
32
+
33
+ export const AGENT_ACTIONS: readonly string[] = [
34
+ "odin",
35
+ "frigg",
36
+ "vor",
37
+ "mimir",
38
+ "heimdall",
39
+ "thor",
40
+ "tyr",
41
+ "forseti",
42
+ ] as const;
43
+
44
+ export type AgentName = (typeof AGENT_ACTIONS)[number];
45
+
46
+ export interface AgentRouteDecision {
47
+ agent: string;
48
+ confidence: number;
49
+ }
50
+
51
+ export interface QLearningRouterOpts {
52
+ stateDim?: number;
53
+ epsilon?: number;
54
+ lruSize?: number;
55
+ seed?: number;
56
+ }
57
+
58
+ const DEFAULT_STATE_DIM = 64;
59
+ const DEFAULT_EPSILON = 0.1;
60
+ const DEFAULT_LRU = 256;
61
+
62
+ const STOPWORDS = new Set([
63
+ "the", "a", "an", "and", "or", "but", "in", "on", "at", "to",
64
+ "for", "of", "with", "by", "is", "are", "be", "was", "were", "this",
65
+ "that", "it", "as", "if", "from", "into", "so", "than",
66
+ ]);
67
+
68
+ export class QLearningRouter {
69
+ private readonly stateDim: number;
70
+ private readonly epsilon: number;
71
+ private readonly lruSize: number;
72
+ // Q[agentIdx][stateBucket] → mean reward for that bucket.
73
+ // Initialised to 0.5 (neutral). Updated by EMA on recordOutcome.
74
+ private readonly q: Float32Array[] = [];
75
+ private readonly counts: Uint32Array[] = [];
76
+ private readonly lru: Map<string, AgentRouteDecision> = new Map();
77
+ private rngState: number;
78
+
79
+ constructor(opts?: QLearningRouterOpts) {
80
+ this.stateDim = opts?.stateDim ?? DEFAULT_STATE_DIM;
81
+ this.epsilon = opts?.epsilon ?? DEFAULT_EPSILON;
82
+ this.lruSize = opts?.lruSize ?? DEFAULT_LRU;
83
+ this.rngState = (opts?.seed ?? 0) | 0;
84
+ for (let i = 0; i < AGENT_ACTIONS.length; i++) {
85
+ this.q.push(new Float32Array(this.stateDim).fill(0.5));
86
+ this.counts.push(new Uint32Array(this.stateDim));
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Greedy ε-soft policy. Returns a cached decision when the same
92
+ * (task) hash has been seen recently (LRU bounded by `lruSize`).
93
+ * Codemod-eligible prompts short-circuit to `heimdall` (the
94
+ * routine-implementation agent) so they still get a "best" agent
95
+ * without paying the bandit lookup cost twice.
96
+ */
97
+ selectAgent(task: string): AgentRouteDecision {
98
+ if (!task || typeof task !== "string") {
99
+ return { agent: "odin", confidence: 0.5 };
100
+ }
101
+
102
+ // Tier-1 codemod-eligible prompt → deterministic safe pick.
103
+ const cm = detectCodemodIntent(task);
104
+ if (cm !== null) {
105
+ return { agent: "heimdall", confidence: 1.0 };
106
+ }
107
+
108
+ const bucket = this.bucketize(task);
109
+ const cacheKey = String(bucket);
110
+ const cached = this.lru.get(cacheKey);
111
+ if (cached) {
112
+ // Touch the LRU entry to mark recently used.
113
+ this.lru.delete(cacheKey);
114
+ this.lru.set(cacheKey, cached);
115
+ return cached;
116
+ }
117
+
118
+ let agentIdx: number;
119
+ let confidence = 0.5;
120
+ if (this.random() < this.epsilon) {
121
+ // Explore: uniformly at random.
122
+ agentIdx = Math.floor(this.random() * AGENT_ACTIONS.length) % AGENT_ACTIONS.length;
123
+ confidence = 0.25;
124
+ } else {
125
+ // Exploit: argmax Q for this bucket. Tie-break by lower count
126
+ // (prefer the less-tried action), then by the canonical order.
127
+ let bestQ = -Infinity;
128
+ let bestCount = Infinity;
129
+ const ties: number[] = [];
130
+ for (let i = 0; i < AGENT_ACTIONS.length; i++) {
131
+ const qv = this.q[i][bucket];
132
+ if (qv > bestQ) {
133
+ bestQ = qv; bestCount = this.counts[i][bucket]; ties.length = 0; ties.push(i);
134
+ } else if (qv === bestQ) {
135
+ const c = this.counts[i][bucket];
136
+ if (c < bestCount) { bestCount = c; ties.length = 0; ties.push(i); }
137
+ else ties.push(i);
138
+ }
139
+ }
140
+ agentIdx = ties[Math.floor(this.random() * ties.length)] ?? 0;
141
+ confidence = clamp01(bestQ);
142
+ }
143
+
144
+ const decision: AgentRouteDecision = {
145
+ agent: AGENT_ACTIONS[agentIdx],
146
+ confidence,
147
+ };
148
+
149
+ // Promote to LRU. LRU has a hard cap; evict oldest on overflow.
150
+ if (this.lru.size >= this.lruSize) {
151
+ const oldest = this.lru.keys().next().value;
152
+ if (oldest !== undefined) this.lru.delete(oldest);
153
+ }
154
+ this.lru.set(cacheKey, decision);
155
+ return decision;
156
+ }
157
+
158
+ /**
159
+ * EMA update of Q[agentIdx][bucket]. Uses the standard
160
+ * Q ← Q + α (reward − Q)
161
+ * where α = 1 / (1 + count) — decreasing step size over time.
162
+ */
163
+ recordOutcome(agent: string, success: boolean): void {
164
+ const idx = AGENT_ACTIONS.indexOf(agent as AgentName);
165
+ if (idx < 0) return;
166
+ // Reward shape: +1 success, 0 failure, normalised to [0,1] in Q.
167
+ const reward = success ? 1.0 : 0.0;
168
+ for (let b = 0; b < this.stateDim; b++) {
169
+ const c = this.counts[idx][b] + 1;
170
+ this.counts[idx][b] = c;
171
+ const alpha = 1 / (1 + c);
172
+ this.q[idx][b] = this.q[idx][b] + alpha * (reward - this.q[idx][b]);
173
+ }
174
+ }
175
+
176
+ /** Read-only access for tests / telemetry. */
177
+ snapshot(): Array<{ agent: string; qMean: number; n: number }> {
178
+ return AGENT_ACTIONS.map((agent, i) => {
179
+ let total = 0;
180
+ let n = 0;
181
+ for (let b = 0; b < this.stateDim; b++) {
182
+ total += this.q[i][b];
183
+ n += this.counts[i][b];
184
+ }
185
+ return {
186
+ agent,
187
+ qMean: n === 0 ? 0.5 : total / this.stateDim,
188
+ n,
189
+ };
190
+ });
191
+ }
192
+
193
+ // -----------------------------------------------------------------
194
+ // Persistence — same JSON-file policy as the model router.
195
+ // -----------------------------------------------------------------
196
+
197
+ /** Stable on-disk serialisation: Q-table per agent, counts, seed. */
198
+ fullSnapshot(): QLearningSnapshot {
199
+ const q: number[][] = [];
200
+ const counts: number[][] = [];
201
+ for (let i = 0; i < AGENT_ACTIONS.length; i++) {
202
+ q.push(Array.from(this.q[i]));
203
+ counts.push(Array.from(this.counts[i]));
204
+ }
205
+ return {
206
+ version: 1,
207
+ stateDim: this.stateDim,
208
+ epsilon: this.epsilon,
209
+ q,
210
+ counts,
211
+ rngSeed: this.rngState,
212
+ };
213
+ }
214
+
215
+ static fromSnapshot(snap: Partial<QLearningSnapshot>): QLearningRouter {
216
+ const inst = new QLearningRouter({
217
+ stateDim: snap.stateDim,
218
+ epsilon: snap.epsilon,
219
+ seed: snap.rngSeed,
220
+ });
221
+ if (snap.q && snap.counts) {
222
+ const dim = inst.stateDim;
223
+ for (let i = 0; i < Math.min(snap.q.length, AGENT_ACTIONS.length); i++) {
224
+ const row = snap.q[i] ?? [];
225
+ const crows = snap.counts[i] ?? [];
226
+ for (let b = 0; b < Math.min(dim, row.length); b++) {
227
+ inst.q[i][b] = row[b];
228
+ inst.counts[i][b] = crows[b] ?? 0;
229
+ }
230
+ }
231
+ }
232
+ return inst;
233
+ }
234
+
235
+ saveTo(path: string): void {
236
+ const fp = isAbsolute(path) ? path : resolve(path);
237
+ mkdirSync(dirname(fp), { recursive: true });
238
+ writeFileSync(fp, JSON.stringify(this.fullSnapshot(), null, 2), "utf-8");
239
+ }
240
+
241
+ static loadFrom(path: string): QLearningRouter {
242
+ try {
243
+ if (!existsSync(path)) return new QLearningRouter();
244
+ const snap = JSON.parse(readFileSync(path, "utf-8")) as Partial<QLearningSnapshot>;
245
+ return QLearningRouter.fromSnapshot(snap);
246
+ } catch {
247
+ return new QLearningRouter();
248
+ }
249
+ }
250
+
251
+ // -----------------------------------------------------------------
252
+ // Helpers (FNV-1a bucketing + seeded RNG).
253
+ // -----------------------------------------------------------------
254
+
255
+ /** FNV-1a 32-bit hash, folded into [0, stateDim). Bag-of-words. */
256
+ private bucketize(text: string): number {
257
+ // Tokenise, drop stopwords.
258
+ const tokens = text
259
+ .toLowerCase()
260
+ .replace(/[^a-z0-9\s]+/g, " ")
261
+ .split(/\s+/)
262
+ .filter((t) => t.length > 1 && !STOPWORDS.has(t));
263
+
264
+ // If everything was stopwords, the whole string is its own bucket.
265
+ const words = tokens.length > 0 ? tokens : [text.toLowerCase().trim()];
266
+
267
+ let acc = 2166136261 >>> 0; // FNV-1a init.
268
+ for (const w of words) {
269
+ let h = 2166136261 >>> 0;
270
+ for (let i = 0; i < w.length; i++) {
271
+ h ^= w.charCodeAt(i);
272
+ h = Math.imul(h, 16777619) >>> 0;
273
+ }
274
+ acc ^= h;
275
+ acc = Math.imul(acc + 0x9e3779b9, 16777619) >>> 0;
276
+ }
277
+ return acc % this.stateDim;
278
+ }
279
+
280
+ private random(): number {
281
+ if (this.rngState !== 0) {
282
+ this.rngState = (this.rngState * 1664525 + 1013904223) | 0;
283
+ const u = (this.rngState >>> 0) / 0xffffffff;
284
+ return u || 1e-12;
285
+ }
286
+ return Math.random() || 1e-12;
287
+ }
288
+ }
289
+
290
+ export interface QLearningSnapshot {
291
+ version: number;
292
+ stateDim: number;
293
+ epsilon: number;
294
+ q: number[][];
295
+ counts: number[][];
296
+ rngSeed: number;
297
+ }
298
+
299
+ function clamp01(n: number): number {
300
+ if (Number.isNaN(n)) return 0;
301
+ if (n < 0) return 0;
302
+ if (n > 1) return 1;
303
+ return n;
304
+ }