@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,746 +0,0 @@
1
- import type { ChatMessage, CondensePolicy } from '@genesislcap/foundation-ai';
2
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
3
- import {
4
- applyCondensation,
5
- CONDENSE_MIN_CHARS,
6
- CONDENSED_ARGS_KEY,
7
- type CondenseContext,
8
- type CondensedEventDetail,
9
- type RegisteredCondensePolicy,
10
- } from './condense-history';
11
-
12
- const suite = createLogicSuite('applyCondensation');
13
-
14
- // Payloads must clear the driver-level floor to be condensed.
15
- const big = (n: number = CONDENSE_MIN_CHARS + 500): string => 'x'.repeat(n);
16
-
17
- const asstCall = (
18
- id: string,
19
- name: string,
20
- args: Record<string, unknown> = {},
21
- extra: Record<string, unknown> = {},
22
- ): ChatMessage => ({ role: 'assistant', content: '', toolCalls: [{ id, name, args, ...extra }] });
23
-
24
- const toolMsg = (toolCallId: string, content: string): ChatMessage => ({
25
- role: 'tool',
26
- content: '',
27
- toolResult: { toolCallId, content },
28
- });
29
-
30
- const reg = (
31
- policy: CondensePolicy,
32
- clocks: { iteration?: number; turn?: number; activation?: number; phaseEpoch?: number } = {},
33
- ): RegisteredCondensePolicy => ({
34
- policy,
35
- iteration: clocks.iteration ?? 1,
36
- turn: clocks.turn ?? 1,
37
- activation: clocks.activation ?? 1,
38
- phaseEpoch: clocks.phaseEpoch ?? 1,
39
- });
40
-
41
- /** A CondenseContext for the pure transform. Defaults fire nothing; override per test. */
42
- const ctx = (
43
- o: {
44
- modelCall?: number;
45
- turn?: number;
46
- activationEnded?: (a: number) => boolean;
47
- phaseEnded?: (e: number) => boolean;
48
- batchCalls?: number;
49
- } = {},
50
- ): CondenseContext => ({
51
- modelCall: o.modelCall ?? 0,
52
- turn: o.turn ?? 0,
53
- activationEnded: o.activationEnded ?? (() => false),
54
- phaseEnded: o.phaseEnded ?? (() => false),
55
- batchCalls: o.batchCalls,
56
- });
57
-
58
- const sink = () => {
59
- const events: CondensedEventDetail[] = [];
60
- return { events, on: (d: CondensedEventDetail) => events.push(d) };
61
- };
62
-
63
- // ---------------------------------------------------------------------------
64
- // supersession
65
- // ---------------------------------------------------------------------------
66
-
67
- suite('superseded response: collapses the earlier call, keeps the latest full', () => {
68
- const history: ChatMessage[] = [
69
- asstCall('r1', 'vfs_read', { path: 'A' }),
70
- toolMsg('r1', big()),
71
- asstCall('r2', 'vfs_read', { path: 'A' }),
72
- toolMsg('r2', big()),
73
- ];
74
- const policies = new Map<string, RegisteredCondensePolicy>([
75
- ['r1', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
76
- ['r2', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
77
- ]);
78
- const { events, on } = sink();
79
- const out = applyCondensation(history, policies, ctx(), on);
80
-
81
- assert.match(out[1].toolResult!.content, /re-call to restore/);
82
- assert.is(out[3].toolResult!.content, big(), 'latest call for the key stays full');
83
- // Exactly one transition reported, for the earlier call's response.
84
- assert.is(events.length, 1);
85
- assert.is(events[0].toolCallId, 'r1');
86
- assert.is(events[0].target, 'response');
87
- assert.is(events[0].trigger, 'superseded:A');
88
- // Input is never mutated; unchanged messages keep their identity.
89
- assert.is(history[1].toolResult!.content, big(), 'stored history untouched');
90
- assert.is(out[3], history[3], 'unchanged message reuses the same object');
91
- assert.ok(out[1] !== history[1], 'collapsed message is a new object');
92
- });
93
-
94
- suite('superseded crosses tools: a read collapses the earlier write args of the same key', () => {
95
- const history: ChatMessage[] = [
96
- asstCall('w1', 'vfs_write', { path: 'A', content: big() }),
97
- toolMsg('w1', 'written'),
98
- asstCall('r1', 'vfs_read', { path: 'A' }),
99
- toolMsg('r1', big()),
100
- ];
101
- const policies = new Map<string, RegisteredCondensePolicy>([
102
- ['w1', reg({ on: { kind: 'superseded', by: 'A' }, args: 'pointer' })],
103
- ['r1', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
104
- ]);
105
- const { events, on } = sink();
106
- const out = applyCondensation(history, policies, ctx(), on);
107
-
108
- // Earlier write is superseded by the later read of the same key → its args collapse.
109
- assert.match(String(out[0].toolCalls![0].args.condensed), /re-call to restore/);
110
- // The read is the latest for key A → its response stays full.
111
- assert.is(out[3].toolResult!.content, big());
112
- assert.is(events.length, 1);
113
- assert.is(events[0].toolCallId, 'w1');
114
- assert.is(events[0].target, 'args');
115
- });
116
-
117
- // ---------------------------------------------------------------------------
118
- // age
119
- // ---------------------------------------------------------------------------
120
-
121
- suite('age: the model sees the result on `turns` turns, then it collapses', () => {
122
- // Call made on iteration 1 → the result is first visible to the model on
123
- // iteration 2. With turns:1 it may be read on that one turn, then collapses.
124
- const history: ChatMessage[] = [asstCall('g1', 'grep_source', {}), toolMsg('g1', big())];
125
- const policies = new Map<string, RegisteredCondensePolicy>([
126
- ['g1', reg({ on: { kind: 'age', turns: 1 }, response: 'pointer' }, { iteration: 1 })],
127
- ]);
128
- const { events, on } = sink();
129
-
130
- // Iteration 2 — its one allowed look: still full, nothing reported.
131
- const seen = applyCondensation(history, policies, ctx({ modelCall: 2 }), on);
132
- assert.is(seen[1].toolResult!.content, big(), 'full on the one turn it may be seen');
133
- assert.is(events.length, 0, 'not collapsed while the model may still see it');
134
-
135
- // Iteration 3 — one turn past the allowed view: collapses.
136
- const collapsed = applyCondensation(history, policies, ctx({ modelCall: 3 }), on);
137
- assert.match(collapsed[1].toolResult!.content, /aged out/);
138
- assert.match(collapsed[1].toolResult!.content, /re-call to restore/);
139
- assert.is(events.length, 1);
140
- assert.is(events[0].trigger, 'age:1');
141
- });
142
-
143
- // ---------------------------------------------------------------------------
144
- // min-size floor
145
- // ---------------------------------------------------------------------------
146
-
147
- suite('payloads below the size floor are never condensed', () => {
148
- const history: ChatMessage[] = [
149
- asstCall('r1', 'vfs_read', { path: 'A' }),
150
- toolMsg('r1', 'tiny'),
151
- asstCall('r2', 'vfs_read', { path: 'A' }),
152
- toolMsg('r2', big()),
153
- ];
154
- const policies = new Map<string, RegisteredCondensePolicy>([
155
- ['r1', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
156
- ['r2', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
157
- ]);
158
- const { events, on } = sink();
159
- const out = applyCondensation(history, policies, ctx(), on);
160
-
161
- assert.is(out[1].toolResult!.content, 'tiny', 'superseded but too small to bother');
162
- assert.is(events.length, 0);
163
- });
164
-
165
- // ---------------------------------------------------------------------------
166
- // result modes
167
- // ---------------------------------------------------------------------------
168
-
169
- suite('drop / pointer / replaceWith produce distinct collapsed forms', () => {
170
- const mk = (id: string, result: CondensePolicy['response']): [ChatMessage, ChatMessage] => [
171
- asstCall(id, 'vfs_read', { path: id }),
172
- toolMsg(id, big()),
173
- ];
174
- // Each call is superseded by a later same-key call so it actually fires.
175
- const history: ChatMessage[] = [
176
- ...mk('drop', 'drop'),
177
- asstCall('drop2', 'vfs_read', { path: 'drop' }),
178
- toolMsg('drop2', big()),
179
- ...mk('ptr', 'pointer'),
180
- asstCall('ptr2', 'vfs_read', { path: 'ptr' }),
181
- toolMsg('ptr2', big()),
182
- ...mk('rep', { replaceWith: 'SUMMARY' }),
183
- asstCall('rep2', 'vfs_read', { path: 'rep' }),
184
- toolMsg('rep2', big()),
185
- ];
186
- const policies = new Map<string, RegisteredCondensePolicy>([
187
- ['drop', reg({ on: { kind: 'superseded', by: 'drop' }, response: 'drop' })],
188
- ['drop2', reg({ on: { kind: 'superseded', by: 'drop' }, response: 'drop' })],
189
- ['ptr', reg({ on: { kind: 'superseded', by: 'ptr' }, response: 'pointer' })],
190
- ['ptr2', reg({ on: { kind: 'superseded', by: 'ptr' }, response: 'pointer' })],
191
- ['rep', reg({ on: { kind: 'superseded', by: 'rep' }, response: { replaceWith: 'SUMMARY' } })],
192
- ['rep2', reg({ on: { kind: 'superseded', by: 'rep' }, response: { replaceWith: 'SUMMARY' } })],
193
- ]);
194
- const { on } = sink();
195
- const out = applyCondensation(history, policies, ctx(), on);
196
-
197
- // `drop` differs from `pointer` ONLY by the restore promise — it still identifies the call,
198
- // because a stub too bare to say what the call did is how GENC-1494 started.
199
- assert.match(out[1].toolResult!.content, /vfs_read/, 'drop names the tool');
200
- assert.match(out[1].toolResult!.content, /result elided/, 'drop says what was elided');
201
- assert.not.match(
202
- out[1].toolResult!.content,
203
- /re-call to restore/,
204
- 'drop makes no restore promise',
205
- );
206
- assert.match(
207
- out[5].toolResult!.content,
208
- /re-call to restore/,
209
- 'pointer carries the restore hint',
210
- );
211
- assert.is(out[9].toolResult!.content, 'SUMMARY', 'replaceWith is verbatim — no hint appended');
212
- });
213
-
214
- // GENC-1494: dropped args must NEVER collapse to `{}`. A bare empty object is
215
- // indistinguishable from a call the model made with no arguments at all, and the model
216
- // treats its own history as fact — a ui-builder run read its elided `vfs_write` calls as
217
- // proof it was writing files with no content and re-wrote them for two hours. A bare marker
218
- // is not enough either: `path` goes with the args, so the stub must still say WHICH file.
219
- suite('args drop identifies the call; pointer stashes a stub under one key', () => {
220
- const history: ChatMessage[] = [
221
- asstCall('w1', 'vfs_write', { path: 'A', content: big() }),
222
- toolMsg('w1', 'ok'),
223
- asstCall('w2', 'vfs_write', { path: 'A', content: big() }),
224
- toolMsg('w2', 'ok'),
225
- ];
226
- const policies = new Map<string, RegisteredCondensePolicy>([
227
- ['w1', reg({ on: { kind: 'superseded', by: 'A' }, args: 'drop' })],
228
- ['w2', reg({ on: { kind: 'superseded', by: 'A' }, args: 'drop' })],
229
- ]);
230
- const { on } = sink();
231
- const out = applyCondensation(history, policies, ctx(), on);
232
- const dropped = out[0].toolCalls![0].args;
233
- const stub = String(dropped[CONDENSED_ARGS_KEY]);
234
-
235
- assert.ok(
236
- Object.keys(dropped).length > 0,
237
- 'dropped args are never empty — that reads as a no-argument call',
238
- );
239
- assert.match(stub, /vfs_write/, 'stub names the tool');
240
- assert.match(stub, /\bA\b/, 'stub names the superseded key — which file this call wrote');
241
- assert.not.match(stub, /re-call to restore/, 'drop must not promise a restore it cannot honour');
242
- assert.equal(out[2].toolCalls![0].args, { path: 'A', content: big() }, 'latest write kept');
243
- });
244
-
245
- // GENC-1494: trigger resolution picks the first LISTED trigger that is currently true, so it
246
- // used to change after the collapse — with the usual `on: [{superseded}, {agentEnd}]`, a payload
247
- // collapsed at `agentEnd` re-resolved to `superseded` the moment a newer same-key call landed,
248
- // rewriting the stub and breaking the prompt cache a second time for an already-collapsed
249
- // payload. In one reported session that was 22/22 args collapses and 97 response collapses.
250
- suite('the fired trigger is latched, so a collapsed stub is never rewritten', () => {
251
- const history: ChatMessage[] = [asstCall('r1', 'vfs_read', { path: 'A' }), toolMsg('r1', big())];
252
- const entry = reg({
253
- on: [{ kind: 'superseded', by: 'A' }, { kind: 'agentEnd' }],
254
- response: 'pointer', // pointer embeds the reason, so a re-resolve is visible in the text
255
- });
256
- const policies = new Map<string, RegisteredCondensePolicy>([['r1', entry]]);
257
- const { events, on } = sink();
258
-
259
- // Pass 1: the activation has ended but nothing supersedes r1 yet → collapses as agentEnd.
260
- const first = applyCondensation(history, policies, ctx({ activationEnded: () => true }), on);
261
- assert.match(first[1].toolResult!.content, /agent finished/, 'collapses under agentEnd');
262
- assert.is(events.length, 1);
263
- assert.is(events[0].trigger, 'agentEnd');
264
-
265
- // Pass 2: a newer read of the SAME key now exists, which would re-resolve the trigger to
266
- // `superseded` (it is listed first). The already-collapsed stub must not change.
267
- const grown: ChatMessage[] = [
268
- ...history,
269
- asstCall('r2', 'vfs_read', { path: 'A' }),
270
- toolMsg('r2', big()),
271
- ];
272
- policies.set('r2', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' }));
273
- const second = applyCondensation(grown, policies, ctx({ activationEnded: () => true }), on);
274
-
275
- assert.match(second[1].toolResult!.content, /agent finished/, 'reason stays pinned to agentEnd');
276
- assert.not.match(second[1].toolResult!.content, /superseded/, 'must not re-resolve');
277
- assert.is(
278
- second[1].toolResult!.content,
279
- first[1].toolResult!.content,
280
- 'stub text is byte-stable',
281
- );
282
- });
283
-
284
- // ---------------------------------------------------------------------------
285
- // envelope + metadata invariants
286
- // ---------------------------------------------------------------------------
287
-
288
- suite('collapsing args preserves the tool-call envelope and providerMetadata', () => {
289
- const history: ChatMessage[] = [
290
- asstCall(
291
- 'w1',
292
- 'vfs_write',
293
- { path: 'A', content: big() },
294
- { providerMetadata: { sig: 'keep' } },
295
- ),
296
- toolMsg('w1', 'ok'),
297
- asstCall('w2', 'vfs_write', { path: 'A', content: big() }),
298
- toolMsg('w2', 'ok'),
299
- ];
300
- const policies = new Map<string, RegisteredCondensePolicy>([
301
- ['w1', reg({ on: { kind: 'superseded', by: 'A' }, args: 'pointer' })],
302
- ['w2', reg({ on: { kind: 'superseded', by: 'A' }, args: 'pointer' })],
303
- ]);
304
- const { on } = sink();
305
- const out = applyCondensation(history, policies, ctx(), on);
306
- const tc = out[0].toolCalls![0];
307
- assert.is(tc.id, 'w1', 'id preserved');
308
- assert.is(tc.name, 'vfs_write', 'name preserved');
309
- assert.equal(tc.providerMetadata, { sig: 'keep' }, 'reasoning signature round-trips');
310
- });
311
-
312
- // ---------------------------------------------------------------------------
313
- // report-once + no-op
314
- // ---------------------------------------------------------------------------
315
-
316
- suite('the event fires once per payload across repeated applies', () => {
317
- const history: ChatMessage[] = [
318
- asstCall('r1', 'vfs_read', { path: 'A' }),
319
- toolMsg('r1', big()),
320
- asstCall('r2', 'vfs_read', { path: 'A' }),
321
- toolMsg('r2', big()),
322
- ];
323
- const policies = new Map<string, RegisteredCondensePolicy>([
324
- ['r1', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
325
- ['r2', reg({ on: { kind: 'superseded', by: 'A' }, response: 'pointer' })],
326
- ]);
327
- const { events, on } = sink();
328
- applyCondensation(history, policies, ctx(), on);
329
- applyCondensation(history, policies, ctx(), on);
330
- applyCondensation(history, policies, ctx(), on);
331
- assert.is(events.length, 1, 'collapse re-runs every call but the transition is reported once');
332
- });
333
-
334
- suite('an empty registry is a pure pass-through (same array reference)', () => {
335
- const history: ChatMessage[] = [asstCall('r1', 'vfs_read'), toolMsg('r1', big())];
336
- const { events, on } = sink();
337
- const out = applyCondensation(history, new Map(), ctx(), on);
338
- assert.is(out, history);
339
- assert.is(events.length, 0);
340
- });
341
-
342
- // ---------------------------------------------------------------------------
343
- // turnEnd
344
- // ---------------------------------------------------------------------------
345
-
346
- suite('turnEnd: full during its own turn, collapsed on a later turn', () => {
347
- const history: ChatMessage[] = [asstCall('t1', 'read'), toolMsg('t1', big())];
348
- const policies = new Map<string, RegisteredCondensePolicy>([
349
- ['t1', reg({ on: { kind: 'turnEnd' }, response: 'pointer' }, { turn: 1 })],
350
- ]);
351
- const { events, on } = sink();
352
-
353
- const sameTurn = applyCondensation(history, policies, ctx({ turn: 1 }), on);
354
- assert.is(sameTurn[1].toolResult!.content, big(), 'full for the rest of the request');
355
- assert.is(events.length, 0);
356
-
357
- const laterTurn = applyCondensation(history, policies, ctx({ turn: 2 }), on);
358
- assert.match(laterTurn[1].toolResult!.content, /turn ended/);
359
- assert.is(events.length, 1);
360
- assert.is(events[0].trigger, 'turnEnd');
361
- });
362
-
363
- // ---------------------------------------------------------------------------
364
- // agentEnd
365
- // ---------------------------------------------------------------------------
366
-
367
- suite('agentEnd: kept while the agent is active, collapsed once its activation ends', () => {
368
- const history: ChatMessage[] = [asstCall('a1', 'load'), toolMsg('a1', big())];
369
- const policies = new Map<string, RegisteredCondensePolicy>([
370
- ['a1', reg({ on: { kind: 'agentEnd' }, response: 'pointer' }, { activation: 1 })],
371
- ]);
372
- const { events, on } = sink();
373
-
374
- const active = applyCondensation(history, policies, ctx({ activationEnded: () => false }), on);
375
- assert.is(active[1].toolResult!.content, big(), 'full while the agent flow is active');
376
- assert.is(events.length, 0);
377
-
378
- const ended = applyCondensation(history, policies, ctx({ activationEnded: (a) => a === 1 }), on);
379
- assert.match(ended[1].toolResult!.content, /agent finished/);
380
- assert.is(events.length, 1);
381
- assert.is(events[0].trigger, 'agentEnd');
382
- });
383
-
384
- // ---------------------------------------------------------------------------
385
- // phaseEnd
386
- // ---------------------------------------------------------------------------
387
-
388
- suite('phaseEnd: kept within the phase, collapsed once the app advances the epoch', () => {
389
- // Registered in phase epoch 1.
390
- const history: ChatMessage[] = [asstCall('p1', 'load_entity'), toolMsg('p1', big())];
391
- const policies = new Map<string, RegisteredCondensePolicy>([
392
- ['p1', reg({ on: { kind: 'phaseEnd' }, response: 'pointer' }, { phaseEpoch: 1 })],
393
- ]);
394
- const { events, on } = sink();
395
-
396
- // Still in phase 1 (no endPhase yet) — full.
397
- const sameP = applyCondensation(history, policies, ctx({ phaseEnded: (e) => e < 1 }), on);
398
- assert.is(sameP[1].toolResult!.content, big(), 'full while the phase is current');
399
- assert.is(events.length, 0);
400
-
401
- // endPhase() advanced the current epoch to 2 → the phase-1 payload collapses.
402
- const ended = applyCondensation(history, policies, ctx({ phaseEnded: (e) => e < 2 }), on);
403
- assert.match(ended[1].toolResult!.content, /phase ended/);
404
- assert.match(ended[1].toolResult!.content, /re-call to restore/);
405
- assert.is(events.length, 1);
406
- assert.is(events[0].trigger, 'phaseEnd');
407
- });
408
-
409
- suite('phaseEnd: falls back to agentEnd when no phase boundary is ever declared', () => {
410
- // Phase epoch never advances (phaseEnded always false), but the activation ends.
411
- const history: ChatMessage[] = [asstCall('p1', 'load_entity'), toolMsg('p1', big())];
412
- const policies = new Map<string, RegisteredCondensePolicy>([
413
- [
414
- 'p1',
415
- reg({ on: { kind: 'phaseEnd' }, response: 'pointer' }, { activation: 1, phaseEpoch: 1 }),
416
- ],
417
- ]);
418
- const { events, on } = sink();
419
-
420
- const active = applyCondensation(
421
- history,
422
- policies,
423
- ctx({ phaseEnded: () => false, activationEnded: () => false }),
424
- on,
425
- );
426
- assert.is(active[1].toolResult!.content, big(), 'full while both phase and agent are live');
427
- assert.is(events.length, 0);
428
-
429
- const disposed = applyCondensation(
430
- history,
431
- policies,
432
- ctx({ phaseEnded: () => false, activationEnded: (a) => a === 1 }),
433
- on,
434
- );
435
- assert.match(disposed[1].toolResult!.content, /phase ended/, 'agentEnd backstop collapses it');
436
- assert.is(events.length, 1);
437
- assert.is(events[0].trigger, 'phaseEnd');
438
- });
439
-
440
- // ---------------------------------------------------------------------------
441
- // multi-trigger (OR / first-wins)
442
- // ---------------------------------------------------------------------------
443
-
444
- suite('multi-trigger: collapses when ANY listed trigger fires', () => {
445
- // superseded OR turnEnd — only one call for key A (never superseded), but the turn ends.
446
- const history: ChatMessage[] = [asstCall('m1', 'read', { path: 'A' }), toolMsg('m1', big())];
447
- const policies = new Map<string, RegisteredCondensePolicy>([
448
- [
449
- 'm1',
450
- reg(
451
- { on: [{ kind: 'superseded', by: 'A' }, { kind: 'turnEnd' }], response: 'pointer' },
452
- { turn: 1 },
453
- ),
454
- ],
455
- ]);
456
- const { events, on } = sink();
457
-
458
- assert.is(
459
- applyCondensation(history, policies, ctx({ turn: 1 }), on)[1].toolResult!.content,
460
- big(),
461
- 'neither trigger has fired',
462
- );
463
- const later = applyCondensation(history, policies, ctx({ turn: 2 }), on);
464
- assert.match(later[1].toolResult!.content, /turn ended/, 'turnEnd fired via the OR');
465
- assert.is(events[0].trigger, 'turnEnd');
466
- });
467
-
468
- suite('multi-trigger first-wins: the earliest-listed firing trigger is the reason', () => {
469
- // m1 is superseded by m2 AND its turn has ended; m2 is the latest (not superseded)
470
- // but its turn has ended too. `superseded` is listed first.
471
- const history: ChatMessage[] = [
472
- asstCall('m1', 'read', { path: 'A' }),
473
- toolMsg('m1', big()),
474
- asstCall('m2', 'read', { path: 'A' }),
475
- toolMsg('m2', big()),
476
- ];
477
- const on0: CondensePolicy = {
478
- on: [{ kind: 'superseded', by: 'A' }, { kind: 'turnEnd' }],
479
- response: 'pointer',
480
- };
481
- const policies = new Map<string, RegisteredCondensePolicy>([
482
- ['m1', reg(on0, { turn: 1 })],
483
- ['m2', reg(on0, { turn: 1 })],
484
- ]);
485
- const { events, on } = sink();
486
- applyCondensation(history, policies, ctx({ turn: 2 }), on);
487
-
488
- assert.is(
489
- events.find((e) => e.toolCallId === 'm1')?.trigger,
490
- 'superseded:A',
491
- 'first-listed firing trigger wins as the reason',
492
- );
493
- assert.is(
494
- events.find((e) => e.toolCallId === 'm2')?.trigger,
495
- 'turnEnd',
496
- 'the supersession survivor still collapses via the next trigger',
497
- );
498
- });
499
-
500
- // ── batched collapse (GENC-1476) ───────────────────────────────────────────
501
- // Condensation rewrites history in place, so collapsing the instant a trigger fires
502
- // breaks the prompt cache on nearly every call of a re-read loop. Batching holds the
503
- // collapse set still between boundaries.
504
-
505
- const batch = createLogicSuite('applyCondensation batching');
506
-
507
- /** A re-read loop: `n` reads of the same path, each registered one model-call apart. */
508
- const reReadLoop = (n: number) => {
509
- const history: ChatMessage[] = [];
510
- const policies = new Map<string, RegisteredCondensePolicy>();
511
- for (let i = 1; i <= n; i += 1) {
512
- history.push(asstCall(`r${i}`, 'vfs_read', { path: 'A.tsx' }), toolMsg(`r${i}`, big()));
513
- policies.set(
514
- `r${i}`,
515
- reg({ on: { kind: 'superseded', by: 'A.tsx' }, response: 'pointer' }, { iteration: i }),
516
- );
517
- }
518
- return { history, policies };
519
- };
520
-
521
- /** Result content of every tool message, in order. */
522
- const results = (out: ChatMessage[]): string[] =>
523
- out.filter((m) => m.role === 'tool').map((m) => m.toolResult?.content ?? '');
524
-
525
- batch('without batching every superseded read collapses immediately', () => {
526
- const { history, policies } = reReadLoop(4);
527
- const out = results(applyCondensation(history, policies, ctx({ modelCall: 4 }), sink().on));
528
- // Only the newest survives; the other three are already stubs.
529
- assert.is(out.filter((c) => c.startsWith('[')).length, 3);
530
- assert.is(out[3].length, big().length);
531
- });
532
-
533
- batch('batching leaves the prefix untouched between boundaries', () => {
534
- const { history, policies } = reReadLoop(4);
535
- // Boundary is floor(4/10)*10 = 0, so no call is within the batch yet.
536
- const out = results(
537
- applyCondensation(history, policies, ctx({ modelCall: 4, batchCalls: 10 }), sink().on),
538
- );
539
- assert.equal(
540
- out.map((c) => c.length),
541
- [big().length, big().length, big().length, big().length],
542
- );
543
- });
544
-
545
- batch('the batch boundary collapses the backlog in one step', () => {
546
- const { history, policies } = reReadLoop(12);
547
- const before = results(
548
- applyCondensation(history, policies, ctx({ modelCall: 9, batchCalls: 10 }), sink().on),
549
- );
550
- const after = results(
551
- applyCondensation(history, policies, ctx({ modelCall: 10, batchCalls: 10 }), sink().on),
552
- );
553
- assert.is(before.filter((c) => c.startsWith('[')).length, 0);
554
- // At the boundary, reads 1-9 collapse together; 10-12 are past it and stay full.
555
- assert.is(after.filter((c) => c.startsWith('[')).length, 9);
556
- });
557
-
558
- batch('a call past the boundary neither collapses nor supersedes an older one', () => {
559
- // The regression this guards: excluding the new call as a SUPERSEDER but still letting
560
- // it collapse would elide the very result the agent just fetched.
561
- const { history, policies } = reReadLoop(2);
562
- policies.set(
563
- 'r2',
564
- reg({ on: { kind: 'superseded', by: 'A.tsx' }, response: 'pointer' }, { iteration: 7 }),
565
- );
566
- const out = results(
567
- applyCondensation(history, policies, ctx({ modelCall: 7, batchCalls: 5 }), sink().on),
568
- );
569
- assert.is(out[1].length, big().length, 'the newest read must stay full');
570
- assert.is(out[0].length, big().length, 'the older read must not collapse early either');
571
- });
572
-
573
- batch('age is measured against the boundary, not the live clock', () => {
574
- const history = [asstCall('g1', 'grep', {}), toolMsg('g1', big())];
575
- const policies = new Map([
576
- ['g1', reg({ on: { kind: 'age', turns: 1 }, response: 'pointer' }, { iteration: 1 })],
577
- ]);
578
- // Live clock says age 4 > 1 (would collapse); the boundary says 0, so it holds.
579
- const held = results(
580
- applyCondensation(history, policies, ctx({ modelCall: 5, batchCalls: 10 }), sink().on),
581
- );
582
- assert.is(held[0].length, big().length);
583
- const collapsed = results(
584
- applyCondensation(history, policies, ctx({ modelCall: 10, batchCalls: 10 }), sink().on),
585
- );
586
- assert.ok(collapsed[0].startsWith('['));
587
- });
588
-
589
- batch('batchCalls of 1, 0 or absent all keep the historical behaviour', () => {
590
- for (const batchCalls of [1, 0, undefined]) {
591
- const { history, policies } = reReadLoop(3);
592
- const out = results(
593
- applyCondensation(history, policies, ctx({ modelCall: 3, batchCalls }), sink().on),
594
- );
595
- assert.is(out.filter((c) => c.startsWith('[')).length, 2, `batchCalls=${batchCalls}`);
596
- }
597
- });
598
-
599
- batch('agentEnd is exempt from batching — it collapses as it fires', () => {
600
- // An agent swap changes the system prompt and tool defs, which invalidates the cached
601
- // prefix anyway, so deferring the collapse would hold a stale payload for nothing.
602
- const history = [asstCall('a1', 'read_context', {}), toolMsg('a1', big())];
603
- const policies = new Map([
604
- ['a1', reg({ on: { kind: 'agentEnd' }, response: 'pointer' }, { iteration: 7, activation: 1 })],
605
- ]);
606
- const out = results(
607
- applyCondensation(
608
- history,
609
- policies,
610
- ctx({ modelCall: 7, batchCalls: 10, activationEnded: () => true }),
611
- sink().on,
612
- ),
613
- );
614
- // Boundary is 0 and the call sits at 7, so a batched trigger would have been deferred.
615
- assert.ok(out[0].startsWith('['));
616
- });
617
-
618
- batch('phaseEnd is exempt too', () => {
619
- const history = [asstCall('p1', 'read_docs', {}), toolMsg('p1', big())];
620
- const policies = new Map([
621
- ['p1', reg({ on: { kind: 'phaseEnd' }, response: 'pointer' }, { iteration: 7, phaseEpoch: 1 })],
622
- ]);
623
- const out = results(
624
- applyCondensation(
625
- history,
626
- policies,
627
- ctx({ modelCall: 7, batchCalls: 10, phaseEnded: () => true }),
628
- sink().on,
629
- ),
630
- );
631
- assert.ok(out[0].startsWith('['));
632
- });
633
-
634
- batch('a mixed policy batches superseded but still honours agentEnd immediately', () => {
635
- // The vfs shape: `on: [{ superseded }, { agentEnd }]`. Mid-run the supersession is
636
- // deferred; once the agent ends the payload collapses regardless of the boundary.
637
- const { history, policies } = reReadLoop(2);
638
- for (const id of ['r1', 'r2'])
639
- policies.set(
640
- id,
641
- reg(
642
- { on: [{ kind: 'superseded', by: 'A.tsx' }, { kind: 'agentEnd' }], response: 'pointer' },
643
- { iteration: id === 'r1' ? 1 : 2, activation: 1 },
644
- ),
645
- );
646
- const midRun = results(
647
- applyCondensation(history, policies, ctx({ modelCall: 4, batchCalls: 10 }), sink().on),
648
- );
649
- assert.is(midRun.filter((c) => c.startsWith('[')).length, 0, 'supersession is deferred');
650
-
651
- const afterSwap = results(
652
- applyCondensation(
653
- history,
654
- policies,
655
- ctx({ modelCall: 4, batchCalls: 10, activationEnded: () => true }),
656
- sink().on,
657
- ),
658
- );
659
- assert.is(afterSwap.filter((c) => c.startsWith('[')).length, 2, 'agentEnd collapses both');
660
- });
661
-
662
- batch.run();
663
-
664
- // ---------------------------------------------------------------------------
665
- // image-bearing tool results (GENC-1508)
666
- //
667
- // A render tool returns a few words of text plus ~15KB of base64. Sizing the payload
668
- // on `content` alone leaves it permanently under the floor, so the image rides every
669
- // later turn — the exact bytes condensation exists to shed. And a stub that leaves the
670
- // attachments in place claims a collapse that did not happen.
671
- // ---------------------------------------------------------------------------
672
-
673
- /** A tool result shaped like a mockup render: short text, one large image. */
674
- const imageToolMsg = (toolCallId: string, dataLen: number): ChatMessage => ({
675
- role: 'tool',
676
- content: 'rendered',
677
- toolResult: {
678
- toolCallId,
679
- content: 'rendered',
680
- attachments: [
681
- { kind: 'image', name: 'mockup.png', mimeType: 'image/png', data: 'a'.repeat(dataLen) },
682
- ],
683
- },
684
- });
685
-
686
- suite('image bytes count toward the condense floor, so a short-text render condenses', () => {
687
- const history: ChatMessage[] = [
688
- asstCall('m1', 'render_mockup', { html: '<p/>' }),
689
- imageToolMsg('m1', CONDENSE_MIN_CHARS + 500),
690
- asstCall('m2', 'render_mockup', { html: '<p/>' }),
691
- imageToolMsg('m2', CONDENSE_MIN_CHARS + 500),
692
- ];
693
- const policies = new Map<string, RegisteredCondensePolicy>([
694
- ['m1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
695
- ['m2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
696
- ]);
697
- const { on } = sink();
698
- const out = applyCondensation(history, policies, ctx(), on);
699
- assert.ok(
700
- out[1].toolResult!.content.startsWith('['),
701
- 'the superseded render is collapsed to a stub — sizing on content alone would skip it',
702
- );
703
- });
704
-
705
- suite('a condensed image result drops its attachments, not just its text', () => {
706
- const history: ChatMessage[] = [
707
- asstCall('m1', 'render_mockup', { html: '<p/>' }),
708
- imageToolMsg('m1', CONDENSE_MIN_CHARS + 500),
709
- asstCall('m2', 'render_mockup', { html: '<p/>' }),
710
- imageToolMsg('m2', CONDENSE_MIN_CHARS + 500),
711
- ];
712
- const policies = new Map<string, RegisteredCondensePolicy>([
713
- ['m1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
714
- ['m2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
715
- ]);
716
- const { on } = sink();
717
- const out = applyCondensation(history, policies, ctx(), on);
718
- assert.is(
719
- out[1].toolResult!.attachments,
720
- undefined,
721
- 'leaving the image attached would shed none of the bytes the stub claims were elided',
722
- );
723
- assert.equal(
724
- out[3].toolResult!.attachments?.length,
725
- 1,
726
- 'the surviving latest render keeps its image',
727
- );
728
- });
729
-
730
- suite('a short text-only result is still below the floor with no attachments', () => {
731
- const history: ChatMessage[] = [
732
- asstCall('s1', 'render_mockup', {}),
733
- toolMsg('s1', 'rendered'),
734
- asstCall('s2', 'render_mockup', {}),
735
- toolMsg('s2', 'rendered'),
736
- ];
737
- const policies = new Map<string, RegisteredCondensePolicy>([
738
- ['s1', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
739
- ['s2', reg({ on: { kind: 'superseded', by: 'mockup' }, response: 'pointer' })],
740
- ]);
741
- const { on } = sink();
742
- const out = applyCondensation(history, policies, ctx(), on);
743
- assert.is(out[1].toolResult!.content, 'rendered', 'the floor still protects small payloads');
744
- });
745
-
746
- suite.run();