@genesislcap/ai-assistant 15.19.0 → 15.19.1

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 (288) hide show
  1. package/package.json +17 -18
  2. package/api-extractor.json +0 -11
  3. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
  4. package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
  5. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
  6. package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
  7. package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
  8. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
  9. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
  10. package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
  11. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
  12. package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
  13. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
  14. package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
  15. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
  16. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
  17. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
  18. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
  19. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
  23. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
  24. package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
  25. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
  26. package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
  27. package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
  28. package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
  29. package/dist/dts/config/validate-providers.test.d.ts +0 -2
  30. package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
  31. package/dist/dts/main/blocked-state.test.d.ts +0 -2
  32. package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
  33. package/dist/dts/main/budget-meter.test.d.ts +0 -2
  34. package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
  35. package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
  36. package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
  37. package/dist/dts/main/file-attachments.test.d.ts +0 -2
  38. package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
  39. package/dist/dts/main/interaction-cost.test.d.ts +0 -2
  40. package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
  41. package/dist/dts/main/main.styles.test.d.ts +0 -2
  42. package/dist/dts/main/main.styles.test.d.ts.map +0 -1
  43. package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
  44. package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
  45. package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
  46. package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
  47. package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
  48. package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
  49. package/dist/dts/state/debug-event-log.test.d.ts +0 -2
  50. package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
  51. package/dist/dts/state/driver-registry.test.d.ts +0 -2
  52. package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
  53. package/dist/dts/state/interaction-context.test.d.ts +0 -2
  54. package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
  55. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
  56. package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
  57. package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
  58. package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
  59. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
  60. package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
  61. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
  62. package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
  63. package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
  64. package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
  65. package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
  66. package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
  67. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
  68. package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
  69. package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
  70. package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
  71. package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
  72. package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
  73. package/dist/dts/utils/condense-history.test.d.ts +0 -2
  74. package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
  75. package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
  76. package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
  77. package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
  78. package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
  79. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
  80. package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
  81. package/dist/dts/utils/history-transform.test.d.ts +0 -2
  82. package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
  83. package/dist/dts/utils/message-partition.test.d.ts +0 -2
  84. package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
  85. package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
  86. package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
  87. package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
  88. package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
  89. package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
  90. package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
  91. package/dist/dts/utils/sum-costs.test.d.ts +0 -2
  92. package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
  93. package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
  94. package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
  95. package/dist/dts/utils/sum-usage.test.d.ts +0 -2
  96. package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
  97. package/dist/dts/utils/usage-rows.test.d.ts +0 -2
  98. package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
  99. package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
  100. package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
  101. package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
  102. package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
  103. package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
  104. package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
  105. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
  106. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
  107. package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
  108. package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
  109. package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
  110. package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
  111. package/dist/esm/config/define-stateful-agent.test.js +0 -53
  112. package/dist/esm/config/validate-providers.test.js +0 -100
  113. package/dist/esm/main/blocked-state.test.js +0 -1065
  114. package/dist/esm/main/budget-meter.test.js +0 -359
  115. package/dist/esm/main/cost-session-banking.test.js +0 -538
  116. package/dist/esm/main/file-attachments.test.js +0 -164
  117. package/dist/esm/main/interaction-cost.test.js +0 -107
  118. package/dist/esm/main/main.styles.test.js +0 -161
  119. package/dist/esm/main/persistence-broken-sources.test.js +0 -180
  120. package/dist/esm/main/popout-interaction-gate.test.js +0 -54
  121. package/dist/esm/state/ai-assistant-slice.test.js +0 -229
  122. package/dist/esm/state/debug-event-log.test.js +0 -117
  123. package/dist/esm/state/driver-registry.test.js +0 -55
  124. package/dist/esm/state/interaction-context.test.js +0 -78
  125. package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
  126. package/dist/esm/state/persistence/diagnostics.test.js +0 -280
  127. package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
  128. package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
  129. package/dist/esm/state/persistence/session-persister.test.js +0 -613
  130. package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
  131. package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
  132. package/dist/esm/utils/animation-exclusivity.test.js +0 -52
  133. package/dist/esm/utils/collect-session-models.test.js +0 -53
  134. package/dist/esm/utils/condense-history.test.js +0 -547
  135. package/dist/esm/utils/cost-session-history.test.js +0 -174
  136. package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
  137. package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
  138. package/dist/esm/utils/history-transform.test.js +0 -158
  139. package/dist/esm/utils/message-partition.test.js +0 -90
  140. package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
  141. package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
  142. package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
  143. package/dist/esm/utils/sum-costs.test.js +0 -114
  144. package/dist/esm/utils/sum-tokens.test.js +0 -51
  145. package/dist/esm/utils/sum-usage.test.js +0 -164
  146. package/dist/esm/utils/usage-rows.test.js +0 -189
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/license.txt +0 -46
  149. package/sandbox/tsconfig.json +0 -8
  150. package/src/channel/ai-activity-bus.ts +0 -209
  151. package/src/channel/ai-activity-channel.ts +0 -146
  152. package/src/chat-driver-node.ts +0 -159
  153. package/src/components/activity-halo/activity-halo.ts +0 -106
  154. package/src/components/agent-picker/agent-picker.constants.ts +0 -2
  155. package/src/components/agent-picker/agent-picker.styles.ts +0 -24
  156. package/src/components/agent-picker/agent-picker.template.ts +0 -54
  157. package/src/components/agent-picker/agent-picker.ts +0 -99
  158. package/src/components/agent-picker/index.ts +0 -1
  159. package/src/components/ai-driver/ai-driver.ts +0 -166
  160. package/src/components/ai-driver/index.ts +0 -1
  161. package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
  162. package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
  163. package/src/components/chat-bubble/chat-bubble.ts +0 -436
  164. package/src/components/chat-bubble/index.ts +0 -1
  165. package/src/components/chat-driver/align-event-globals.ts +0 -23
  166. package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
  167. package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
  168. package/src/components/chat-driver/chat-driver.test.ts +0 -3679
  169. package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
  170. package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
  171. package/src/components/chat-driver/chat-driver.ts +0 -3925
  172. package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
  173. package/src/components/chat-driver/index.ts +0 -1
  174. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
  175. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
  176. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
  177. package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
  178. package/src/components/chat-interaction-wrapper/index.ts +0 -3
  179. package/src/components/chat-markdown/chat-markdown.ts +0 -89
  180. package/src/components/chat-markdown/index.ts +0 -1
  181. package/src/components/flowing-waves-indicator.ts +0 -286
  182. package/src/components/halo-overlay.ts +0 -184
  183. package/src/components/orchestrating-driver/index.ts +0 -1
  184. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
  185. package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
  186. package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
  187. package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
  188. package/src/components/plasma-orb-indicator.ts +0 -281
  189. package/src/components/popout-manager/index.ts +0 -1
  190. package/src/components/popout-manager/popout-manager.ts +0 -212
  191. package/src/components/settings-modal/index.ts +0 -2
  192. package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
  193. package/src/components/settings-modal/settings-modal.styles.ts +0 -853
  194. package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
  195. package/src/components/settings-modal/settings-modal.template.ts +0 -943
  196. package/src/components/waves-indicator.ts +0 -241
  197. package/src/config/config.ts +0 -608
  198. package/src/config/define-stateful-agent.test.ts +0 -64
  199. package/src/config/define-stateful-agent.ts +0 -642
  200. package/src/config/fallback-agents.ts +0 -29
  201. package/src/config/index.ts +0 -2
  202. package/src/config/validate-providers.test.ts +0 -148
  203. package/src/config/validate-providers.ts +0 -58
  204. package/src/index.ts +0 -29
  205. package/src/main/blocked-state.test.ts +0 -1444
  206. package/src/main/budget-meter.test.ts +0 -477
  207. package/src/main/cost-session-banking.test.ts +0 -679
  208. package/src/main/file-attachments.test.ts +0 -215
  209. package/src/main/index.ts +0 -3
  210. package/src/main/interaction-cost.test.ts +0 -140
  211. package/src/main/main.styles.test.ts +0 -216
  212. package/src/main/main.styles.ts +0 -995
  213. package/src/main/main.template.ts +0 -1119
  214. package/src/main/main.ts +0 -5432
  215. package/src/main/main.types.ts +0 -225
  216. package/src/main/persistence-broken-sources.test.ts +0 -219
  217. package/src/main/popout-interaction-gate.test.ts +0 -87
  218. package/src/provider/ai-provider-switcher.ts +0 -40
  219. package/src/provider/assistant-app-settings.ts +0 -84
  220. package/src/state/ai-assistant-slice.test.ts +0 -336
  221. package/src/state/ai-assistant-slice.ts +0 -604
  222. package/src/state/debug-event-log.test.ts +0 -154
  223. package/src/state/debug-event-log.ts +0 -358
  224. package/src/state/driver-registry.test.ts +0 -70
  225. package/src/state/driver-registry.ts +0 -79
  226. package/src/state/interaction-context.test.ts +0 -90
  227. package/src/state/interaction-context.ts +0 -101
  228. package/src/state/persistence/build-timeline-entries.ts +0 -66
  229. package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
  230. package/src/state/persistence/diagnostics-cursors.ts +0 -80
  231. package/src/state/persistence/diagnostics.test.ts +0 -346
  232. package/src/state/persistence/diagnostics.ts +0 -220
  233. package/src/state/persistence/index.ts +0 -10
  234. package/src/state/persistence/persister-registry.ts +0 -55
  235. package/src/state/persistence/session-persistence-provider.test.ts +0 -136
  236. package/src/state/persistence/session-persistence-provider.ts +0 -226
  237. package/src/state/persistence/session-persistence.integration.test.ts +0 -141
  238. package/src/state/persistence/session-persister.test.ts +0 -787
  239. package/src/state/persistence/session-persister.ts +0 -917
  240. package/src/state/persistence/session-snapshot.test.ts +0 -352
  241. package/src/state/persistence/session-snapshot.ts +0 -344
  242. package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
  243. package/src/state/session-store.ts +0 -56
  244. package/src/styles/ai-colours.ts +0 -30
  245. package/src/styles/menu.ts +0 -187
  246. package/src/styles/settings-section.ts +0 -40
  247. package/src/suggestions/chat-suggestions.ts +0 -186
  248. package/src/tags/index.ts +0 -1
  249. package/src/types/ai-chat-widget.ts +0 -67
  250. package/src/types/interaction-context.ts +0 -82
  251. package/src/utils/animated-panel-toggle.ts +0 -62
  252. package/src/utils/animation-exclusivity.test.ts +0 -72
  253. package/src/utils/animation-exclusivity.ts +0 -40
  254. package/src/utils/banked-usage-baselines.ts +0 -90
  255. package/src/utils/collect-session-models.test.ts +0 -82
  256. package/src/utils/collect-session-models.ts +0 -65
  257. package/src/utils/condense-history.test.ts +0 -746
  258. package/src/utils/condense-history.ts +0 -448
  259. package/src/utils/cost-session-history.test.ts +0 -226
  260. package/src/utils/cost-session-history.ts +0 -218
  261. package/src/utils/derive-cost-session-title.test.ts +0 -63
  262. package/src/utils/derive-cost-session-title.ts +0 -76
  263. package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
  264. package/src/utils/flatten-sub-agent-messages.ts +0 -88
  265. package/src/utils/format-usd.ts +0 -26
  266. package/src/utils/history-transform.test.ts +0 -228
  267. package/src/utils/history-transform.ts +0 -163
  268. package/src/utils/index.ts +0 -3
  269. package/src/utils/logger.ts +0 -3
  270. package/src/utils/message-partition.test.ts +0 -129
  271. package/src/utils/message-partition.ts +0 -115
  272. package/src/utils/resolve-cost-history-config.test.ts +0 -38
  273. package/src/utils/resolve-cost-history-config.ts +0 -38
  274. package/src/utils/resolve-preference-baseline.test.ts +0 -84
  275. package/src/utils/resolve-preference-baseline.ts +0 -80
  276. package/src/utils/strip-agent-handlers.test.ts +0 -99
  277. package/src/utils/strip-agent-handlers.ts +0 -53
  278. package/src/utils/sum-costs.test.ts +0 -136
  279. package/src/utils/sum-costs.ts +0 -18
  280. package/src/utils/sum-tokens.test.ts +0 -93
  281. package/src/utils/sum-tokens.ts +0 -68
  282. package/src/utils/sum-usage.test.ts +0 -191
  283. package/src/utils/sum-usage.ts +0 -208
  284. package/src/utils/tool-fold.ts +0 -181
  285. package/src/utils/usage-rows.test.ts +0 -237
  286. package/src/utils/usage-rows.ts +0 -187
  287. package/src/utils/with-timeout.ts +0 -24
  288. package/tsconfig.json +0 -9
@@ -1,69 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { resolvePreferenceBaseline } from './resolve-preference-baseline';
3
- // GENC-1351 #2: the preference baseline resolves with precedence
4
- // slice defaults < chatConfig.ui defaults < stored user preferences.
5
- // The regression this guards: when stored prefs are absent (first load / wiped
6
- // backend) the result must fall through to the chatConfig.ui defaults, NOT to
7
- // everything-disabled.
8
- const FULL_UI = {
9
- showToolCalls: true,
10
- showThinkingSteps: true,
11
- showNarration: true,
12
- showAgentSwitchIndicator: true,
13
- animations: { userConfigurable: true },
14
- };
15
- const Suite = createLogicSuite('resolvePreferenceBaseline');
16
- Suite('with no stored prefs, falls back to chatConfig.ui defaults (the nuke case)', () => {
17
- const r = resolvePreferenceBaseline(FULL_UI, null);
18
- assert.is(r.showToolCalls, true);
19
- assert.is(r.showThinkingSteps, true);
20
- assert.is(r.showNarration, true);
21
- assert.is(r.showAgentSwitchIndicator, true);
22
- assert.ok(r.enabledAnimations.length > 0, 'config animations enabled by default');
23
- assert.is(r.enabled, undefined, 'no stored enabled → leave host default untouched');
24
- });
25
- Suite('with no stored prefs AND empty/unbound chatConfig, everything is off', () => {
26
- // The transient state during connect before chatConfig binds — all-false is
27
- // correct HERE; the element re-runs the baseline once chatConfig arrives.
28
- const r = resolvePreferenceBaseline(undefined, null);
29
- assert.equal([r.showToolCalls, r.showThinkingSteps, r.showNarration, r.showAgentSwitchIndicator], [false, false, false, false]);
30
- assert.equal(r.enabledAnimations, []);
31
- });
32
- Suite('stored prefs override the config defaults field-by-field', () => {
33
- const stored = {
34
- enabled: true,
35
- showToolCalls: false,
36
- showThinkingSteps: false,
37
- showNarration: true,
38
- showAgentSwitchIndicator: false,
39
- animations: ['halo'],
40
- };
41
- const r = resolvePreferenceBaseline(FULL_UI, stored);
42
- assert.is(r.showToolCalls, false, 'stored false beats config true');
43
- assert.is(r.showThinkingSteps, false);
44
- assert.is(r.showNarration, true);
45
- assert.is(r.showAgentSwitchIndicator, false);
46
- assert.equal(r.enabledAnimations, ['halo'], 'stored animations applied as-is');
47
- assert.is(r.enabled, true, 'stored enabled surfaces');
48
- });
49
- Suite('a partial stored blob overrides only present fields; others keep config default', () => {
50
- const stored = { showToolCalls: false };
51
- const r = resolvePreferenceBaseline(FULL_UI, stored);
52
- assert.is(r.showToolCalls, false, 'present field overridden');
53
- assert.is(r.showThinkingSteps, true, 'absent field keeps config default');
54
- assert.is(r.showAgentSwitchIndicator, true);
55
- assert.is(r.enabled, undefined, 'absent enabled stays undefined');
56
- });
57
- Suite('stored prefs over an empty config still apply (config layer is all-false)', () => {
58
- const stored = { showToolCalls: true, showThinkingSteps: true };
59
- const r = resolvePreferenceBaseline(undefined, stored);
60
- assert.is(r.showToolCalls, true);
61
- assert.is(r.showThinkingSteps, true);
62
- assert.is(r.showNarration, false, 'not in stored, not in config → false');
63
- });
64
- Suite('de-dupes stored animations (heals blobs from the old settings-modal append bug)', () => {
65
- const stored = { animations: ['loading', 'halo', 'loading', 'halo'] };
66
- const r = resolvePreferenceBaseline(FULL_UI, stored);
67
- assert.equal(r.enabledAnimations, ['loading', 'halo'], 'duplicates collapsed, order preserved');
68
- });
69
- Suite.run();
@@ -1,81 +0,0 @@
1
- import { __awaiter } from "tslib";
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import { stripAgentHandlers } from './strip-agent-handlers';
4
- const cfg = (overrides) => (Object.assign({ name: 'agent' }, overrides));
5
- /** Mirrors what redux's serializable-state check does: find any function anywhere. */
6
- const hasFunctionDeep = (value) => {
7
- if (typeof value === 'function')
8
- return true;
9
- if (Array.isArray(value))
10
- return value.some(hasFunctionDeep);
11
- if (value !== null && typeof value === 'object') {
12
- return Object.values(value).some(hasFunctionDeep);
13
- }
14
- return false;
15
- };
16
- const suite = createLogicSuite('stripAgentHandlers');
17
- suite('drops object-form toolHandlers (the redux-leak regression)', () => {
18
- const stripped = stripAgentHandlers(cfg({ toolHandlers: { vfs_list: () => __awaiter(void 0, void 0, void 0, function* () { return 'x'; }), vfs_read: () => __awaiter(void 0, void 0, void 0, function* () { return 'y'; }) } }));
19
- // The whole handler bag is gone — not left behind with live functions inside.
20
- assert.not.ok(stripped.toolHandlers);
21
- assert.not.ok(hasFunctionDeep(stripped));
22
- });
23
- suite('drops factory-function toolHandlers and the lifecycle/dispatch hooks', () => {
24
- const stripped = stripAgentHandlers(cfg({
25
- toolHandlers: () => ({ t: () => __awaiter(void 0, void 0, void 0, function* () { return 'x'; }) }),
26
- onActivate: () => __awaiter(void 0, void 0, void 0, function* () { }),
27
- onDeactivate: () => __awaiter(void 0, void 0, void 0, function* () { }),
28
- getDebugSnapshot: () => ({}),
29
- onUnresolvedTool: () => 'redirect',
30
- }));
31
- assert.not.ok(stripped.toolHandlers);
32
- assert.not.ok(stripped.onActivate);
33
- assert.not.ok(stripped.onDeactivate);
34
- assert.not.ok(stripped.getDebugSnapshot);
35
- assert.not.ok(stripped.onUnresolvedTool);
36
- });
37
- suite('drops function-form resolvers but keeps static forms and plain data', () => {
38
- const stripped = stripAgentHandlers(cfg({
39
- systemPrompt: () => 'dynamic', // function → dropped
40
- displayName: 'Static Name', // string → kept
41
- temperature: 0.5, // number → kept
42
- toolChoice: 'auto', // string → kept
43
- toolDefinitions: [{ name: 'x', description: 'd', parameters: {} }], // data array → kept
44
- manualSelection: { enabled: true, hint: 'pick me' }, // data object → kept
45
- }));
46
- assert.not.ok(stripped.systemPrompt);
47
- assert.is(stripped.displayName, 'Static Name');
48
- assert.is(stripped.temperature, 0.5);
49
- assert.is(stripped.toolChoice, 'auto');
50
- assert.equal(stripped.toolDefinitions, [{ name: 'x', description: 'd', parameters: {} }]);
51
- assert.equal(stripped.manualSelection, { enabled: true, hint: 'pick me' });
52
- });
53
- suite('recurses into subAgents, stripping their object-form toolHandlers', () => {
54
- const stripped = stripAgentHandlers(cfg({
55
- name: 'boss',
56
- toolHandlers: () => ({}),
57
- subAgents: [
58
- cfg({
59
- name: 'worker',
60
- toolHandlers: { vfs_list: () => __awaiter(void 0, void 0, void 0, function* () { return 'x'; }), vfs_read: () => __awaiter(void 0, void 0, void 0, function* () { return 'y'; }) },
61
- }),
62
- ],
63
- }));
64
- const subs = stripped.subAgents;
65
- assert.is(subs.length, 1);
66
- assert.is(subs[0].name, 'worker');
67
- assert.not.ok(subs[0].toolHandlers, 'sub-agent toolHandlers must be stripped (the reported bug)');
68
- // Nothing function-valued survives anywhere in the projection.
69
- assert.not.ok(hasFunctionDeep(stripped));
70
- });
71
- suite('produces a projection with no function at any depth', () => {
72
- const stripped = stripAgentHandlers(cfg({
73
- name: 'boss',
74
- onActivate: () => __awaiter(void 0, void 0, void 0, function* () { }),
75
- subAgents: [cfg({ name: 'w', toolHandlers: { t: () => __awaiter(void 0, void 0, void 0, function* () { }) } })],
76
- }));
77
- assert.not.ok(hasFunctionDeep(stripped));
78
- // Round-trips to the expected serializable shape.
79
- assert.equal(JSON.parse(JSON.stringify(stripped)), { name: 'boss', subAgents: [{ name: 'w' }] });
80
- });
81
- suite.run();
@@ -1,114 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { sumCosts } from './sum-costs';
3
- const userMsg = (overrides = {}) => (Object.assign({ role: 'user', content: '' }, overrides));
4
- const assistantMsg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
5
- const suite = createLogicSuite('sumCosts');
6
- suite('returns 0 for an empty message list', () => {
7
- assert.is(sumCosts([]), 0);
8
- });
9
- suite('returns 0 when no message carries cost (e.g. all from a no-usage provider)', () => {
10
- assert.is(sumCosts([userMsg(), assistantMsg({ content: 'hi' })]), 0);
11
- });
12
- suite('sums top-level message costs', () => {
13
- // Use integer-cents arithmetic to avoid float-equality flake; the production
14
- // path only ever adds floats here, so any rounding lives in display, not sum.
15
- const cost = sumCosts([
16
- assistantMsg({ cost: 0.5 }),
17
- assistantMsg({ cost: 1.5 }),
18
- userMsg(), // no cost — ignored
19
- ]);
20
- assert.is(cost, 2);
21
- });
22
- suite('includes sub-agent trace costs attached to a tool call', () => {
23
- const parentToolCallMsg = assistantMsg({
24
- cost: 0.1,
25
- toolCalls: [
26
- {
27
- id: 'tc1',
28
- name: 'spawn_sub',
29
- args: {},
30
- subAgentTrace: [assistantMsg({ cost: 0.5 }), assistantMsg({ cost: 0.4 })],
31
- },
32
- ],
33
- });
34
- // 0.1 (parent) + 0.5 + 0.4 (sub) = 1.0
35
- assert.is(sumCosts([parentToolCallMsg]), 1);
36
- });
37
- suite('recurses into nested sub-agent traces', () => {
38
- const innerTrace = [assistantMsg({ cost: 0.25 })];
39
- const middleTrace = [
40
- assistantMsg({
41
- cost: 0.5,
42
- toolCalls: [{ id: 'inner', name: 'spawn_inner', args: {}, subAgentTrace: innerTrace }],
43
- }),
44
- ];
45
- const top = assistantMsg({
46
- cost: 1,
47
- toolCalls: [{ id: 'middle', name: 'spawn_middle', args: {}, subAgentTrace: middleTrace }],
48
- });
49
- // 1 + 0.5 + 0.25 = 1.75
50
- assert.is(sumCosts([top]), 1.75);
51
- });
52
- suite('handles a mix of tool calls with and without subAgentTrace', () => {
53
- const cost = sumCosts([
54
- assistantMsg({
55
- cost: 1,
56
- toolCalls: [
57
- { id: 'a', name: 'plain_tool', args: {} }, // no trace — skipped
58
- {
59
- id: 'b',
60
- name: 'sub_tool',
61
- args: {},
62
- subAgentTrace: [assistantMsg({ cost: 2 })],
63
- },
64
- ],
65
- }),
66
- ]);
67
- assert.is(cost, 3);
68
- });
69
- suite('ignores missing cost fields inside an otherwise-populated trace', () => {
70
- const cost = sumCosts([
71
- assistantMsg({
72
- toolCalls: [
73
- {
74
- id: 'tc',
75
- name: 'sub',
76
- args: {},
77
- subAgentTrace: [
78
- userMsg(), // role=user, no cost
79
- assistantMsg({ content: 'partial', cost: 0.3 }),
80
- assistantMsg({ content: 'unbilled' }), // no cost field
81
- ],
82
- },
83
- ],
84
- }),
85
- ]);
86
- assert.is(cost, 0.3);
87
- });
88
- suite('adds widget-reported externalCostUsd alongside provider cost', () => {
89
- // A message can carry both: provider LLM `cost` and a widget's `externalCostUsd`.
90
- const cost = sumCosts([
91
- assistantMsg({ cost: 0.5 }), // provider only
92
- assistantMsg({ externalCostUsd: 0.25 }), // external only (e.g. an interaction message)
93
- assistantMsg({ cost: 0.1, externalCostUsd: 0.4 }), // both
94
- userMsg(), // neither — ignored
95
- ]);
96
- // 0.5 + 0.25 + 0.1 + 0.4 = 1.25
97
- assert.is(cost, 1.25);
98
- });
99
- suite('includes externalCostUsd inside a sub-agent trace', () => {
100
- const cost = sumCosts([
101
- assistantMsg({
102
- toolCalls: [
103
- {
104
- id: 'tc',
105
- name: 'sub',
106
- args: {},
107
- subAgentTrace: [assistantMsg({ cost: 0.5, externalCostUsd: 0.5 })],
108
- },
109
- ],
110
- }),
111
- ]);
112
- assert.is(cost, 1);
113
- });
114
- suite.run();
@@ -1,51 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { formatCompactTokenCount, formatCompactTokenCountSafe, sumTokens } from './sum-tokens';
3
- const assistantMsg = (overrides = {}) => (Object.assign({ role: 'assistant', content: '' }, overrides));
4
- const suite = createLogicSuite('sumTokens');
5
- suite('returns 0 for an empty message list', () => {
6
- assert.is(sumTokens([]), 0);
7
- });
8
- suite('sums input and output tokens on top-level messages', () => {
9
- assert.is(sumTokens([
10
- assistantMsg({ inputTokens: 100, outputTokens: 50 }),
11
- assistantMsg({ inputTokens: 200, outputTokens: 0 }),
12
- ]), 350);
13
- });
14
- suite('includes sub-agent trace tokens attached to a tool call', () => {
15
- assert.is(sumTokens([
16
- assistantMsg({
17
- inputTokens: 10,
18
- outputTokens: 5,
19
- toolCalls: [
20
- {
21
- id: 'tc1',
22
- name: 'spawn_sub',
23
- args: {},
24
- subAgentTrace: [assistantMsg({ inputTokens: 1000, outputTokens: 400 })],
25
- },
26
- ],
27
- }),
28
- ]), 1415);
29
- });
30
- const formatSuite = createLogicSuite('formatCompactTokenCount');
31
- formatSuite('formats millions with one decimal when needed', () => {
32
- assert.is(formatCompactTokenCount(1400000), '<span class="value">1.4</span><span class="suffix">M</span>');
33
- });
34
- formatSuite('formats whole millions without a decimal', () => {
35
- assert.is(formatCompactTokenCount(2000000), '<span class="value">2</span><span class="suffix">M</span>');
36
- });
37
- formatSuite('formats thousands compactly', () => {
38
- assert.is(formatCompactTokenCount(3200), '<span class="value">3.2</span><span class="suffix">K</span>');
39
- });
40
- formatSuite('formats small counts with grouping', () => {
41
- assert.is(formatCompactTokenCount(842), '842');
42
- });
43
- formatSuite('rolls K suffix up to M when rounding would yield 1000K', () => {
44
- assert.is(formatCompactTokenCount(999950), '<span class="value">1</span><span class="suffix">M</span>');
45
- assert.is(formatCompactTokenCount(999949), '<span class="value">999.9</span><span class="suffix">K</span>');
46
- });
47
- formatSuite('safe formatter preserves compact markup', () => {
48
- assert.is(formatCompactTokenCountSafe(1400000), '<span class="value">1.4</span><span class="suffix">M</span>');
49
- });
50
- suite.run();
51
- formatSuite.run();
@@ -1,164 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { addUsage, emptyUsage, messageUsage, sumUsage, totalTokens } from './sum-usage';
3
- const suite = createLogicSuite('sumUsage');
4
- const msg = (over = {}) => (Object.assign({ role: 'assistant', content: '' }, over));
5
- suite('returns zeroes when no message carries usage', () => {
6
- const usage = sumUsage([msg({ role: 'user', content: 'hi' })]);
7
- assert.equal(usage, emptyUsage());
8
- assert.is(totalTokens(usage), 0);
9
- });
10
- suite('splits the prompt into three disjoint input buckets', () => {
11
- // `inputTokens` is the WHOLE prompt; the cache fields break it down. So uncached is
12
- // the remainder, and the three must re-add to the prompt exactly — that identity is
13
- // what stops the buckets double-counting the prompt when summed.
14
- const usage = sumUsage([
15
- msg({ inputTokens: 1000, cacheReadTokens: 700, cacheWriteTokens: 200, outputTokens: 50 }),
16
- ]);
17
- assert.is(usage.uncachedInputTokens, 100);
18
- assert.is(usage.cacheReadTokens, 700);
19
- assert.is(usage.cacheWriteTokens, 200);
20
- assert.is(usage.outputTokens, 50);
21
- assert.is(usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens, 1000, 'input buckets re-add to the prompt total');
22
- assert.is(totalTokens(usage), 1050);
23
- });
24
- suite('treats the whole prompt as uncached when no cache split is reported', () => {
25
- // Providers that report no cache breakdown (and messages persisted before the fields
26
- // existed) must still contribute their full prompt, not drop it.
27
- const usage = sumUsage([msg({ inputTokens: 400, outputTokens: 60 })]);
28
- assert.is(usage.uncachedInputTokens, 400);
29
- assert.is(usage.cacheReadTokens, 0);
30
- assert.is(usage.cacheWriteTokens, 0);
31
- assert.is(totalTokens(usage), 460);
32
- });
33
- suite('clamps uncached input at zero when buckets exceed the prompt total', () => {
34
- // Malformed or hand-edited history must not be able to drive a cost display negative.
35
- const usage = sumUsage([
36
- msg({ inputTokens: 100, cacheReadTokens: 90, cacheWriteTokens: 90, outputTokens: 0 }),
37
- ]);
38
- assert.is(usage.uncachedInputTokens, 0);
39
- });
40
- suite('sums LLM cost and widget-reported external cost', () => {
41
- const usage = sumUsage([msg({ cost: 0.25 }), msg({ externalCostUsd: 0.1, cost: 0.05 })]);
42
- assert.is(Math.round(usage.costUsd * 100) / 100, 0.4);
43
- });
44
- suite('recurses into sub-agent traces', () => {
45
- // A sub-agent's whole conversation is embedded in the parent's tool call, so this is
46
- // the only place its spend is visible to a host total.
47
- const usage = sumUsage([
48
- msg({
49
- toolCalls: [
50
- {
51
- id: 'tc-1',
52
- name: 'delegate',
53
- args: {},
54
- subAgentTrace: [
55
- msg({ cost: 0.2, inputTokens: 300, cacheReadTokens: 100, outputTokens: 40 }),
56
- ],
57
- },
58
- ],
59
- }),
60
- ]);
61
- assert.is(usage.costUsd, 0.2);
62
- assert.is(usage.uncachedInputTokens, 200);
63
- assert.is(usage.cacheReadTokens, 100);
64
- assert.is(usage.outputTokens, 40);
65
- });
66
- suite('counts usage banked onto a compaction summary', () => {
67
- // Compaction destroys the messages it summarises. Without the banked rollup, a total
68
- // re-derived from the transcript would fall by whatever those turns had spent.
69
- const before = sumUsage([
70
- msg({ cost: 0.5, inputTokens: 1000, cacheReadTokens: 400, outputTokens: 100 }),
71
- msg({ cost: 0.3, inputTokens: 500, outputTokens: 50 }),
72
- ]);
73
- const afterCompaction = sumUsage([
74
- msg({
75
- role: 'compacted-summary',
76
- content: 'summary of the above',
77
- compaction: {
78
- compactedCount: 2,
79
- createdAt: '2026-06-14T12:00:00.000Z',
80
- rolledUpUsage: before,
81
- },
82
- }),
83
- ]);
84
- assert.equal(afterCompaction, before, 'compaction preserves the running total');
85
- });
86
- suite('ignores tokensBefore, which is a context reading rather than spend', () => {
87
- // `tokensBefore` is one turn's context-window size. Counting it as cumulative spend
88
- // would silently inflate the session total on every compaction.
89
- const usage = sumUsage([
90
- msg({
91
- role: 'compacted-summary',
92
- content: 'summary',
93
- compaction: {
94
- compactedCount: 3,
95
- createdAt: '2026-06-14T12:00:00.000Z',
96
- tokensBefore: 90000,
97
- },
98
- }),
99
- ]);
100
- assert.is(totalTokens(usage), 0);
101
- });
102
- suite('addUsage adds every bucket and mutates neither operand', () => {
103
- const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 1.5, uncachedInputTokens: 100, outputTokens: 10 });
104
- const b = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 0.25, cacheReadTokens: 900, outputTokens: 5 });
105
- const sum = addUsage(a, b);
106
- assert.is(sum.costUsd, 1.75);
107
- assert.is(sum.uncachedInputTokens, 100);
108
- assert.is(sum.cacheReadTokens, 900);
109
- assert.is(sum.outputTokens, 15);
110
- // Both operands are live state (a row's banked figure and the session total), so a
111
- // caller must be able to add without either being altered underneath it.
112
- assert.is(a.costUsd, 1.5);
113
- assert.is(b.cacheReadTokens, 900);
114
- });
115
- suite('addUsage with an empty operand is the identity', () => {
116
- const a = Object.assign(Object.assign({}, emptyUsage()), { costUsd: 3, cacheWriteTokens: 42 });
117
- assert.equal(addUsage(a, emptyUsage()), a);
118
- assert.equal(addUsage(emptyUsage(), a), a);
119
- });
120
- suite('messageUsage is undefined for a message reporting nothing', () => {
121
- // A user turn, or a display-only reasoning/narration split. Distinct from a zeroed
122
- // usage: "this call reported nothing" is not "this call cost nothing".
123
- assert.is(messageUsage(msg({ role: 'user', content: 'hi' })), undefined);
124
- assert.is(messageUsage(msg({ content: 'thinking…', category: 'reasoning' })), undefined);
125
- });
126
- suite('messageUsage agrees with sumUsage over the same single message', () => {
127
- // The two derivations are deliberately separate — `accumulate` mutates one
128
- // accumulator rather than allocating per message — so pin them together. A drift
129
- // here would put one figure on a turn entry and a different one in the session
130
- // total, both looking plausible.
131
- const cases = [
132
- { inputTokens: 1000, cacheReadTokens: 700, cacheWriteTokens: 200, outputTokens: 50, cost: 0.4 },
133
- { inputTokens: 400, outputTokens: 60 },
134
- { inputTokens: 100, cacheReadTokens: 90, cacheWriteTokens: 90 },
135
- { cost: 0.01, externalCostUsd: 0.02 },
136
- ];
137
- for (const over of cases) {
138
- const m = msg(over);
139
- assert.equal(messageUsage(m), sumUsage([m]), `messageUsage must match sumUsage for ${JSON.stringify(over)}`);
140
- }
141
- });
142
- suite('messageUsage is shallow — it prices the one request, not the tree', () => {
143
- // Deliberately blind to sub-agent traces and banked compaction usage: it answers
144
- // "what did this call cost", which is what a turn snapshot needs. `sumUsage` is the
145
- // function that walks everything underneath.
146
- const m = msg({
147
- inputTokens: 100,
148
- outputTokens: 10,
149
- cost: 0.05,
150
- toolCalls: [
151
- {
152
- id: 't1',
153
- name: 'delegate',
154
- args: {},
155
- subAgentTrace: [msg({ inputTokens: 900, outputTokens: 90, cost: 0.5 })],
156
- },
157
- ],
158
- });
159
- const usage = messageUsage(m);
160
- assert.is(usage.costUsd, 0.05, 'the child conversation is not folded in');
161
- assert.is(usage.uncachedInputTokens, 100);
162
- assert.ok(sumUsage([m]).costUsd > usage.costUsd, 'sumUsage does fold it in');
163
- });
164
- suite.run();
@@ -1,189 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { sumUsage } from './sum-usage';
3
- import { usageRows } from './usage-rows';
4
- const msg = (over) => (Object.assign({ role: 'assistant', content: '' }, over));
5
- const rolled = (over) => (Object.assign({ costUsd: 0, uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }, over));
6
- /** Every place spend can hide, in one transcript: a delegation and a compaction. */
7
- const richHistory = () => [
8
- msg({
9
- role: 'compacted-summary',
10
- content: 'summary',
11
- compaction: {
12
- compactedCount: 4,
13
- rolledUpUsage: rolled({ costUsd: 0.05, uncachedInputTokens: 400, outputTokens: 60 }),
14
- // The summariser's model — NOT what the banked spend ran on.
15
- model: 'claude-haiku-4-5-20251001',
16
- },
17
- }),
18
- msg({ role: 'user', content: 'go' }),
19
- msg({
20
- model: 'claude-sonnet-5',
21
- agentName: 'boss',
22
- cost: 0.01,
23
- inputTokens: 1000,
24
- cacheReadTokens: 900,
25
- outputTokens: 50,
26
- toolCalls: [
27
- {
28
- id: 'tc1',
29
- name: 'delegate',
30
- args: {},
31
- subAgentTrace: [
32
- msg({
33
- model: 'gemini-2.5-flash',
34
- agentName: 'worker',
35
- cost: 0.12,
36
- inputTokens: 500,
37
- outputTokens: 200,
38
- }),
39
- ],
40
- },
41
- ],
42
- }),
43
- ];
44
- const suite = createLogicSuite('usageRows');
45
- suite('reconciles exactly with sumUsage over the same input', () => {
46
- // THE guarantee. A per-call view that disagrees with the total is worse than none,
47
- // because both look right in isolation — which is how the consumer's own copy
48
- // under-reported for a month.
49
- const history = richHistory();
50
- const rows = usageRows(history);
51
- const total = sumUsage(history);
52
- const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
53
- assert.ok(Math.abs(rowCost - total.costUsd) < 1e-12, `${rowCost} vs ${total.costUsd}`);
54
- const bucket = (k) => rows.reduce((n, r) => n + r[k], 0);
55
- assert.is(bucket('uncachedInputTokens'), total.uncachedInputTokens, 'uncached input');
56
- assert.is(bucket('cacheReadTokens'), total.cacheReadTokens, 'cache read');
57
- assert.is(bucket('cacheWriteTokens'), total.cacheWriteTokens, 'cache write');
58
- assert.is(bucket('outputTokens'), total.outputTokens, 'output');
59
- });
60
- suite('emits a compaction row so banked spend is not lost from a per-call view', () => {
61
- // The shortfall this function exists to close: the summary message carries no
62
- // `cost` and no token fields of its own, so a naive per-message walk drops the
63
- // spend of every turn the compaction deleted.
64
- const rows = usageRows(richHistory());
65
- const compactionRows = rows.filter((r) => r.source === 'compaction');
66
- assert.is(compactionRows.length, 1);
67
- assert.is(compactionRows[0].costUsd, 0.05);
68
- assert.is(compactionRows[0].uncachedInputTokens, 400);
69
- assert.is(compactionRows[0].model, undefined, 'no model — the banked spend may span several, and compaction.model is the summariser');
70
- });
71
- suite('recurses into sub-agent traces, tagging depth and the spawning call', () => {
72
- const rows = usageRows(richHistory());
73
- const child = rows.find((r) => r.model === 'gemini-2.5-flash');
74
- assert.ok(child, 'the sub-agent turn produced a row');
75
- assert.is(child.subAgentDepth, 1);
76
- assert.is(child.subAgentOf, 'tc1');
77
- assert.is(child.agentName, 'worker');
78
- const parent = rows.find((r) => r.model === 'claude-sonnet-5');
79
- assert.is(parent.subAgentDepth, 0);
80
- assert.is(parent.subAgentOf, undefined);
81
- });
82
- suite('prefers the provider the driver stamped over re-deriving it from the model', () => {
83
- // `ChatMessage.provider` comes from the resolved provider's own status, so it is
84
- // authoritative and knows vendors the model allowlists do not. Re-deriving from the
85
- // model id alone drops attribution for a server-proxied turn — exactly the rows a
86
- // usage ledger cares most about getting right. PR review.
87
- const rows = usageRows([
88
- msg({ model: 'gpt-5', provider: 'openai', cost: 1, inputTokens: 1 }),
89
- // Stamped provider disagrees with what the model id would derive: the stamp still wins.
90
- msg({
91
- model: 'claude-sonnet-5',
92
- provider: 'chrome',
93
- cost: 1,
94
- inputTokens: 1,
95
- }),
96
- ]);
97
- assert.equal(rows.map((r) => r.provider), ['openai', 'chrome']);
98
- });
99
- suite('resolves the provider from the model, and leaves it unset when unknown', () => {
100
- const rows = usageRows([
101
- msg({ model: 'claude-sonnet-5', cost: 1, inputTokens: 1 }),
102
- msg({ model: 'gemini-2.5-flash', cost: 1, inputTokens: 1 }),
103
- msg({ model: 'gpt-5', cost: 1, inputTokens: 1 }),
104
- msg({ cost: 1, inputTokens: 1 }),
105
- ]);
106
- assert.equal(rows.map((r) => r.provider), ['anthropic', 'gemini', undefined, undefined], 'an unrecognised or absent model leaves provider unset rather than guessing');
107
- });
108
- suite('splits the prompt into buckets the same way sumUsage does', () => {
109
- const [row] = usageRows([
110
- msg({
111
- model: 'claude-sonnet-5',
112
- inputTokens: 1000,
113
- cacheReadTokens: 600,
114
- cacheWriteTokens: 300,
115
- }),
116
- ]);
117
- assert.is(row.uncachedInputTokens, 100, 'the remainder, not an addition');
118
- assert.is(row.cacheReadTokens, 600);
119
- assert.is(row.cacheWriteTokens, 300);
120
- });
121
- suite('clamps a prompt whose cache buckets exceed the total', () => {
122
- const [row] = usageRows([msg({ inputTokens: 100, cacheReadTokens: 900 })]);
123
- assert.is(row.uncachedInputTokens, 0, 'never negative');
124
- });
125
- suite('leaves costUsd undefined when nothing was reported, rather than zero', () => {
126
- // What the "carried usage but no cost" audit keys off. A zero here would read as a
127
- // free call and the alarm would never fire.
128
- const [row] = usageRows([msg({ model: 'claude-sonnet-5', inputTokens: 500, outputTokens: 10 })]);
129
- assert.is(row.costUsd, undefined);
130
- assert.is(row.source, 'request');
131
- });
132
- suite('records external (non-LLM) cost and counts it in the reconciliation', () => {
133
- const history = [msg({ externalCostUsd: 0.4 }), msg({ model: 'claude-sonnet-5', cost: 0.1 })];
134
- const rows = usageRows(history);
135
- assert.is(rows.find((r) => r.externalCostUsd != null).externalCostUsd, 0.4);
136
- const rowCost = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
137
- assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12);
138
- });
139
- suite('produces no row for a message carrying no usage', () => {
140
- assert.equal(usageRows([
141
- msg({ role: 'user', content: 'hello' }),
142
- msg({ content: 'thinking out loud', category: 'reasoning' }),
143
- ]), []);
144
- });
145
- suite('is empty for an empty transcript', () => {
146
- assert.equal(usageRows([]), []);
147
- });
148
- suite('reconciles on a nested delegation two levels deep', () => {
149
- const history = [
150
- msg({
151
- model: 'claude-sonnet-5',
152
- cost: 0.01,
153
- inputTokens: 100,
154
- toolCalls: [
155
- {
156
- id: 'a',
157
- name: 'delegate',
158
- args: {},
159
- subAgentTrace: [
160
- msg({
161
- model: 'claude-sonnet-5',
162
- cost: 0.02,
163
- inputTokens: 200,
164
- toolCalls: [
165
- {
166
- id: 'b',
167
- name: 'delegate',
168
- args: {},
169
- subAgentTrace: [msg({ model: 'gemini-2.5-flash', cost: 0.04, inputTokens: 400 })],
170
- },
171
- ],
172
- }),
173
- ],
174
- },
175
- ],
176
- }),
177
- ];
178
- const rows = usageRows(history);
179
- assert.equal(rows.map((r) => r.subAgentDepth), [0, 1, 2]);
180
- const rowCost = rows.reduce((n, r) => { var _a; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0); }, 0);
181
- assert.ok(Math.abs(rowCost - sumUsage(history).costUsd) < 1e-12, 'still reconciles');
182
- });
183
- suite('does not mutate the transcript', () => {
184
- const history = richHistory();
185
- const before = JSON.stringify(history);
186
- usageRows(history);
187
- assert.is(JSON.stringify(history), before, 'a projection, not a transform');
188
- });
189
- suite.run();