@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

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 (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -1,17 +1,24 @@
1
1
  /**
2
- * Unit tests for `runOverflowReductionLoop` the direct-call overflow
3
- * reducer driver.
2
+ * Unit tests for the default `overflowReduce` plugin (PR 23).
4
3
  *
5
- * The default loop produces results **identical** to the historical inline
6
- * tier loop for a golden set of over-budget histories. We exercise this by
7
- * running the same inputs through two paths `runOverflowReductionLoop` and
8
- * a faithful re-implementation of the original inline loop — and asserting
9
- * the final `(messages, runMessages, injectionMode, reducerState,
10
- * reducerCompacted, attempts)` tuple matches byte-for-byte. Additional cases
11
- * cover the two abort gates and the `reinjectForMode` two-flag semantics.
4
+ * Two goals:
5
+ * 1. The default middleware produces results **identical** to the historical
6
+ * inline tier loop for a golden set of over-budget histories. We exercise
7
+ * this by running the same inputs through two pathsthe pipeline and a
8
+ * faithful re-implementation of the pre-PR-23 inline loop and asserting
9
+ * the final `(messages, runMessages, injectionMode, reducerState,
10
+ * reducerCompacted, attempts)` tuple matches byte-for-byte.
11
+ * 2. A user-registered spy middleware observes **every** reduction attempt
12
+ * when wrapped around the default. This covers the onion-composition
13
+ * contract: the spy sees each call from the outside and can count
14
+ * iterations without changing reducer behavior.
15
+ *
16
+ * The test creates its own plugin registry via
17
+ * `resetPluginRegistryForTests()` and re-registers the default before each
18
+ * case so the registry is deterministic across runs.
12
19
  */
13
20
 
14
- import { describe, expect, test } from "bun:test";
21
+ import { beforeEach, describe, expect, test } from "bun:test";
15
22
 
16
23
  import { estimatePromptTokens } from "../context/token-estimator.js";
17
24
  import type {
@@ -25,10 +32,22 @@ import {
25
32
  type ReducerState,
26
33
  } from "../daemon/context-overflow-reducer.js";
27
34
  import type { InjectionMode } from "../daemon/conversation-runtime-assembly.js";
35
+ import type { TrustContext } from "../daemon/trust-context.js";
36
+ import defaultOverflowReduceMiddleware from "../plugins/defaults/overflow-reduce/middlewares/overflowReduce.js";
37
+ import { defaultOverflowReducePlugin } from "../plugins/defaults/overflow-reduce/register.js";
38
+ import { runPipeline } from "../plugins/pipeline.js";
28
39
  import {
29
- type OverflowReduceArgs,
30
- runOverflowReductionLoop,
31
- } from "../daemon/overflow-reduction-loop.js";
40
+ getMiddlewaresFor,
41
+ registerPlugin,
42
+ resetPluginRegistryForTests,
43
+ } from "../plugins/registry.js";
44
+ import type {
45
+ Middleware,
46
+ OverflowReduceArgs,
47
+ OverflowReduceResult,
48
+ Plugin,
49
+ TurnContext,
50
+ } from "../plugins/types.js";
32
51
  import type { Message } from "../providers/types.js";
33
52
 
34
53
  // ── Fixtures ────────────────────────────────────────────────────────────────
@@ -68,6 +87,21 @@ const CONTEXT_WINDOW = {
68
87
  },
69
88
  };
70
89
 
90
+ const TRUST: TrustContext = {
91
+ sourceChannel: "vellum",
92
+ trustClass: "guardian",
93
+ };
94
+
95
+ function makeTurnContext(overrides: Partial<TurnContext> = {}): TurnContext {
96
+ return {
97
+ requestId: "req-overflow-test",
98
+ conversationId: "conv-overflow-test",
99
+ turnIndex: 0,
100
+ trust: TRUST,
101
+ ...overrides,
102
+ };
103
+ }
104
+
71
105
  /**
72
106
  * Minimal compaction stub — always compacts to a one-message summary so the
73
107
  * reducer's forced-compaction tier succeeds. Mirrors `makeCompactFn` from
@@ -112,9 +146,9 @@ function makeCompactFn(
112
146
  }
113
147
 
114
148
  /**
115
- * Faithful re-implementation of the original inline tier loop — lives in
149
+ * Faithful re-implementation of the pre-PR-23 inline tier loop — lives in
116
150
  * this test file rather than the production module so we have an immutable
117
- * baseline `runOverflowReductionLoop` can be diffed against. If either
151
+ * baseline the default middleware can be diffed against. If either
118
152
  * implementation drifts, the golden-output cases below fail.
119
153
  *
120
154
  * The function intentionally avoids any side effects on external state — no
@@ -233,9 +267,9 @@ function buildArgs(messages: Message[]): {
233
267
 
234
268
  // Identity reinject: the test harness does not exercise the full
235
269
  // `applyRuntimeInjections` pipeline; it simply tracks how many times the
236
- // orchestrator would have been asked to rebuild `runMessages`. Returns the
237
- // reducer's latest `messages` untouched real orchestrator code re-injects
238
- // runtime blocks.
270
+ // orchestrator would have been asked to rebuild `runMessages` so the spy
271
+ // middleware can attribute each iteration. Returns the reducer's latest
272
+ // `messages` untouched — real orchestrator code re-injects runtime blocks.
239
273
  const reinjectForMode = async (
240
274
  reducedMessages: Message[],
241
275
  mode: InjectionMode,
@@ -261,7 +295,7 @@ function buildArgs(messages: Message[]): {
261
295
  toolTokenBudget: 0,
262
296
  maxAttempts: CONTEXT_WINDOW.overflowRecovery.maxAttempts,
263
297
  // `OverflowReduceArgs.compactFn` types `options` as `unknown` to avoid
264
- // leaking the `ContextWindowCompactOptions` shape into the loop's args
298
+ // leaking the `ContextWindowCompactOptions` shape into the plugin
265
299
  // surface. The test helper produces a real `ContextWindowCompactOptions`
266
300
  // signature, so we trampoline through a widened wrapper.
267
301
  compactFn: (msgs, signal, opts) =>
@@ -281,10 +315,14 @@ function buildArgs(messages: Message[]): {
281
315
 
282
316
  // ── Test suite ──────────────────────────────────────────────────────────────
283
317
 
284
- describe("runOverflowReductionLoop", () => {
285
- describe("matches historical inline loop", () => {
318
+ describe("overflow-reduce pipeline", () => {
319
+ beforeEach(() => {
320
+ resetPluginRegistryForTests();
321
+ registerPlugin(defaultOverflowReducePlugin);
322
+ });
323
+
324
+ describe("default middleware matches historical inline loop", () => {
286
325
  test("large tool-result history — identical reduced output", async () => {
287
- // GIVEN an over-budget history dominated by a large tool result.
288
326
  const longToolResult = "r".repeat(8000);
289
327
  const goldenHistory: Message[] = [
290
328
  msg("user", "Start"),
@@ -294,13 +332,27 @@ describe("runOverflowReductionLoop", () => {
294
332
  msg("user", "Next"),
295
333
  ];
296
334
 
297
- // AND two independently-built arg sets over the SAME fixture so the
298
- // direct call and the inline baseline never share a `compactFn`.
299
- const directBuild = buildArgs(goldenHistory);
335
+ const pipelineBuild = buildArgs(goldenHistory);
300
336
  const inlineBuild = buildArgs(goldenHistory);
301
337
 
302
- // WHEN we reduce via the direct loop and the inline baseline.
303
- const directResult = await runOverflowReductionLoop(directBuild.args);
338
+ // Run both paths against the SAME fixture. `buildArgs` gives each
339
+ // call its own `compactFn` instance so nothing leaks between runs.
340
+ const pipelineResult = await runPipeline<
341
+ OverflowReduceArgs,
342
+ OverflowReduceResult
343
+ >(
344
+ "overflowReduce",
345
+ getMiddlewaresFor("overflowReduce"),
346
+ // Sentinel terminal — the default middleware doesn't call next,
347
+ // so this must never fire. Assert that invariant here.
348
+ async () => {
349
+ throw new Error("terminal unexpectedly reached");
350
+ },
351
+ pipelineBuild.args,
352
+ makeTurnContext(),
353
+ 30000,
354
+ );
355
+
304
356
  const inlineResult = await runInlineBaseline({
305
357
  messages: goldenHistory,
306
358
  runMessages: goldenHistory,
@@ -315,27 +367,41 @@ describe("runOverflowReductionLoop", () => {
315
367
  estimatePostInjection: inlineBuild.args.estimatePostInjection,
316
368
  });
317
369
 
318
- // THEN every field the orchestrator relies on matches byte-for-byte.
319
- expect(directResult.messages).toEqual(inlineResult.messages);
320
- expect(directResult.runMessages).toEqual(inlineResult.runMessages);
321
- expect(directResult.injectionMode).toBe(inlineResult.injectionMode);
322
- expect(directResult.reducerState).toEqual(inlineResult.reducerState);
323
- expect(directResult.reducerCompacted).toBe(inlineResult.reducerCompacted);
324
- expect(directResult.attempts).toBe(inlineResult.attempts);
370
+ // Byte-for-byte match across every field the orchestrator relies on.
371
+ expect(pipelineResult.messages).toEqual(inlineResult.messages);
372
+ expect(pipelineResult.runMessages).toEqual(inlineResult.runMessages);
373
+ expect(pipelineResult.injectionMode).toBe(inlineResult.injectionMode);
374
+ expect(pipelineResult.reducerState).toEqual(inlineResult.reducerState);
375
+ expect(pipelineResult.reducerCompacted).toBe(
376
+ inlineResult.reducerCompacted,
377
+ );
378
+ expect(pipelineResult.attempts).toBe(inlineResult.attempts);
325
379
  });
326
380
 
327
381
  test("small conversation that fits after first reduction — single attempt", async () => {
328
- // GIVEN a history that the first forced compaction brings under budget.
382
+ // A history that's already within budget so the first `applyForcedCompaction`
383
+ // brings us under — the loop must exit without iterating further.
329
384
  const smallHistory: Message[] = [
330
385
  msg("user", "Hello"),
331
386
  msg("assistant", "Hi there — how can I help?"),
332
387
  ];
333
388
 
334
- const directBuild = buildArgs(smallHistory);
389
+ const pipelineBuild = buildArgs(smallHistory);
335
390
  const inlineBuild = buildArgs(smallHistory);
336
391
 
337
- // WHEN we reduce via the direct loop and the inline baseline.
338
- const directResult = await runOverflowReductionLoop(directBuild.args);
392
+ const pipelineResult = await runPipeline<
393
+ OverflowReduceArgs,
394
+ OverflowReduceResult
395
+ >(
396
+ "overflowReduce",
397
+ getMiddlewaresFor("overflowReduce"),
398
+ async () => {
399
+ throw new Error("terminal unexpectedly reached");
400
+ },
401
+ pipelineBuild.args,
402
+ makeTurnContext(),
403
+ 30000,
404
+ );
339
405
  const inlineResult = await runInlineBaseline({
340
406
  messages: smallHistory,
341
407
  runMessages: smallHistory,
@@ -350,17 +416,149 @@ describe("runOverflowReductionLoop", () => {
350
416
  estimatePostInjection: inlineBuild.args.estimatePostInjection,
351
417
  });
352
418
 
353
- // THEN both paths converge in the same single attempt with equal output.
354
- expect(directResult.attempts).toBe(inlineResult.attempts);
355
- expect(directResult.attempts).toBeGreaterThanOrEqual(1);
356
- expect(directResult.messages).toEqual(inlineResult.messages);
357
- expect(directResult.reducerCompacted).toBe(inlineResult.reducerCompacted);
419
+ expect(pipelineResult.attempts).toBe(inlineResult.attempts);
420
+ expect(pipelineResult.attempts).toBeGreaterThanOrEqual(1);
421
+ expect(pipelineResult.messages).toEqual(inlineResult.messages);
422
+ expect(pipelineResult.reducerCompacted).toBe(
423
+ inlineResult.reducerCompacted,
424
+ );
425
+ });
426
+ });
427
+
428
+ describe("spy middleware observes each reduction attempt", () => {
429
+ test("spy sees one invocation when the default converges in one step", async () => {
430
+ const history: Message[] = [msg("user", "Hello"), msg("assistant", "Hi")];
431
+
432
+ // Spy tracks the args passed into its layer. It must forward via
433
+ // `next` so the default still fires.
434
+ const spyCalls: Array<{
435
+ hadMessages: number;
436
+ budget: number;
437
+ attempts: number;
438
+ }> = [];
439
+ const spy: Middleware<OverflowReduceArgs, OverflowReduceResult> =
440
+ async function spyMiddleware(args, next, _ctx) {
441
+ spyCalls.push({
442
+ hadMessages: args.messages.length,
443
+ budget: args.preflightBudget,
444
+ attempts: 0, // populated after next() from the result
445
+ });
446
+ const result = await next(args);
447
+ spyCalls[spyCalls.length - 1]!.attempts = result.attempts;
448
+ return result;
449
+ };
450
+ const spyPlugin: Plugin = {
451
+ manifest: {
452
+ name: "spy-overflow",
453
+ version: "0.0.1",
454
+ },
455
+ middleware: { overflowReduce: spy },
456
+ };
457
+ // Register spy first so it wraps the default (registration order =
458
+ // outer→inner). The default therefore runs as the spy's downstream.
459
+ resetPluginRegistryForTests();
460
+ registerPlugin(spyPlugin);
461
+ registerPlugin(defaultOverflowReducePlugin);
462
+
463
+ const { args } = buildArgs(history);
464
+ const result = await runPipeline<
465
+ OverflowReduceArgs,
466
+ OverflowReduceResult
467
+ >(
468
+ "overflowReduce",
469
+ getMiddlewaresFor("overflowReduce"),
470
+ async () => {
471
+ throw new Error("terminal unexpectedly reached");
472
+ },
473
+ args,
474
+ makeTurnContext(),
475
+ 30000,
476
+ );
477
+
478
+ // Spy was called exactly once — the pipeline invokes each middleware
479
+ // once per pipeline call, not once per reducer iteration. Iteration
480
+ // count shows up in the result.attempts field.
481
+ expect(spyCalls).toHaveLength(1);
482
+ expect(spyCalls[0]?.hadMessages).toBe(2);
483
+ expect(spyCalls[0]?.budget).toBe(1000);
484
+ expect(spyCalls[0]?.attempts).toBe(result.attempts);
485
+ expect(result.attempts).toBeGreaterThanOrEqual(1);
486
+ });
487
+
488
+ test("spy can short-circuit the default by not calling next", async () => {
489
+ const history: Message[] = [msg("user", "Hi")];
490
+
491
+ const shortCircuit: Middleware<OverflowReduceArgs, OverflowReduceResult> =
492
+ async function shortCircuitMiddleware(args, _next, _ctx) {
493
+ // Returns a synthetic "no-op" result — the default is never invoked.
494
+ return {
495
+ messages: args.messages,
496
+ runMessages: args.runMessages,
497
+ injectionMode: "minimal",
498
+ reducerState: {
499
+ appliedTiers: ["injection_downgrade"],
500
+ injectionMode: "minimal",
501
+ exhausted: true,
502
+ },
503
+ reducerCompacted: false,
504
+ attempts: 0,
505
+ };
506
+ };
507
+ resetPluginRegistryForTests();
508
+ registerPlugin({
509
+ manifest: {
510
+ name: "short-circuit-overflow",
511
+ version: "0.0.1",
512
+ },
513
+ middleware: { overflowReduce: shortCircuit },
514
+ });
515
+ registerPlugin(defaultOverflowReducePlugin);
516
+
517
+ const { args, compactionResults, reinjectCalls } = buildArgs(history);
518
+ const result = await runPipeline<
519
+ OverflowReduceArgs,
520
+ OverflowReduceResult
521
+ >(
522
+ "overflowReduce",
523
+ getMiddlewaresFor("overflowReduce"),
524
+ async () => {
525
+ throw new Error("terminal unexpectedly reached");
526
+ },
527
+ args,
528
+ makeTurnContext(),
529
+ 30000,
530
+ );
531
+
532
+ // Because the outer middleware short-circuited, the default never
533
+ // ran — no compactFn invocations, no reinject callbacks.
534
+ expect(result.injectionMode).toBe("minimal");
535
+ expect(result.attempts).toBe(0);
536
+ expect(compactionResults).toHaveLength(0);
537
+ expect(reinjectCalls).toHaveLength(0);
538
+ });
539
+ });
540
+
541
+ describe("direct middleware invocation", () => {
542
+ test("default middleware without the pipeline runner still executes the tier loop", async () => {
543
+ const history: Message[] = [msg("user", "Hi")];
544
+ const { args } = buildArgs(history);
545
+
546
+ const result = await defaultOverflowReduceMiddleware(
547
+ args,
548
+ async () => {
549
+ throw new Error("next should not be invoked by the default");
550
+ },
551
+ makeTurnContext(),
552
+ );
553
+
554
+ expect(result.attempts).toBeGreaterThanOrEqual(1);
555
+ expect(result.reducerState.appliedTiers.length).toBeGreaterThanOrEqual(1);
358
556
  });
359
557
  });
360
558
 
361
559
  describe("abort signal propagation", () => {
362
- test("bails between iterations when abortSignal fires", async () => {
363
- // GIVEN a history that won't converge in one step (multiple iterations).
560
+ test("middleware bails between iterations when abortSignal fires", async () => {
561
+ // History that won't converge in one step multiple iterations.
364
562
  const longToolResult = "r".repeat(8000);
365
563
  const history: Message[] = [
366
564
  msg("user", "Start"),
@@ -371,8 +569,9 @@ describe("runOverflowReductionLoop", () => {
371
569
 
372
570
  const controller = new AbortController();
373
571
  const build = buildArgs(history);
374
- // AND an estimator that aborts on its first call while reporting
375
- // over-budget so without the abort gate another iteration would run.
572
+ // Abort on the first `estimatePostInjection` simulates the
573
+ // pipeline-level timeout firing mid-turn. The next loop iteration
574
+ // must see the signal and throw rather than starting another round.
376
575
  let estimateCalls = 0;
377
576
  const aborting: OverflowReduceArgs = {
378
577
  ...build.args,
@@ -380,18 +579,26 @@ describe("runOverflowReductionLoop", () => {
380
579
  estimatePostInjection: () => {
381
580
  estimateCalls++;
382
581
  if (estimateCalls === 1) controller.abort();
582
+ // Return a value that guarantees another iteration would fire
583
+ // without the abort gate.
383
584
  return build.args.preflightBudget + 1_000_000;
384
585
  },
385
586
  };
386
587
 
387
- // WHEN the loop runs THEN it throws on the post-side-effect abort gate.
388
- await expect(runOverflowReductionLoop(aborting)).rejects.toThrow();
389
- // AND exactly one iteration ran; the gate stopped the next round.
588
+ await expect(
589
+ defaultOverflowReduceMiddleware(
590
+ aborting,
591
+ async () => {
592
+ throw new Error("next should not be invoked");
593
+ },
594
+ makeTurnContext(),
595
+ ),
596
+ ).rejects.toThrow();
597
+ // Exactly one iteration ran; the abort gate stopped the next round.
390
598
  expect(estimateCalls).toBe(1);
391
599
  });
392
600
 
393
- test("refuses to start when abortSignal is already aborted", async () => {
394
- // GIVEN an already-aborted signal.
601
+ test("middleware refuses to start when abortSignal is already aborted", async () => {
395
602
  const history: Message[] = [msg("user", "Hi")];
396
603
  const controller = new AbortController();
397
604
  controller.abort();
@@ -401,9 +608,16 @@ describe("runOverflowReductionLoop", () => {
401
608
  abortSignal: controller.signal,
402
609
  };
403
610
 
404
- // WHEN the loop runs THEN it throws before the reducer ever runs.
405
- await expect(runOverflowReductionLoop(args)).rejects.toThrow();
406
- // AND no compaction or reinject callbacks were observed.
611
+ await expect(
612
+ defaultOverflowReduceMiddleware(
613
+ args,
614
+ async () => {
615
+ throw new Error("next should not be invoked");
616
+ },
617
+ makeTurnContext(),
618
+ ),
619
+ ).rejects.toThrow();
620
+ // Reducer never ran — zero compaction and reinject callbacks observed.
407
621
  expect(build.compactionResults).toHaveLength(0);
408
622
  expect(build.reinjectCalls).toHaveLength(0);
409
623
  });
@@ -411,10 +625,10 @@ describe("runOverflowReductionLoop", () => {
411
625
 
412
626
  describe("reinjectForMode two-flag semantics", () => {
413
627
  test("stepCompacted reflects current iteration; accumulatedCompacted stays sticky", async () => {
414
- // GIVEN a history that stays over-budget so the loop runs every tier:
415
- // the first iteration compacts (stepCompacted=true), later iterations
416
- // run other tiers (stepCompacted=false), but accumulatedCompacted must
417
- // remain true for slack suppression.
628
+ // Force multiple iterations by returning over-budget until the loop
629
+ // exits on maxAttempts. First iteration compacts (stepCompacted=true);
630
+ // subsequent iterations run other tiers (stepCompacted=false), but
631
+ // accumulatedCompacted must remain true for slack suppression.
418
632
  const longToolResult = "r".repeat(8000);
419
633
  const history: Message[] = [
420
634
  msg("user", "Start"),
@@ -428,13 +642,19 @@ describe("runOverflowReductionLoop", () => {
428
642
  estimatePostInjection: () => build.args.preflightBudget + 1_000_000,
429
643
  };
430
644
 
431
- // WHEN the loop runs to exhaustion / maxAttempts.
432
- await runOverflowReductionLoop(overBudget);
645
+ await defaultOverflowReduceMiddleware(
646
+ overBudget,
647
+ async () => {
648
+ throw new Error("next should not be invoked");
649
+ },
650
+ makeTurnContext(),
651
+ );
433
652
 
434
- // THEN at least one compaction iteration happened.
653
+ // At least one compaction attempt happened.
435
654
  expect(build.reinjectCalls.length).toBeGreaterThanOrEqual(1);
436
- // AND once an iteration compacted, accumulatedCompacted stays true for
437
- // every subsequent iteration even when that step did not compact.
655
+ // The first iteration that compacted set accumulatedCompacted=true,
656
+ // and every subsequent call continues to see it true even when
657
+ // that iteration's own step did NOT compact.
438
658
  const firstCompactedAt = build.reinjectCalls.findIndex(
439
659
  (c) => c.stepCompacted,
440
660
  );