@namzu/sdk 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
  3. package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
  4. package/dist/advisory/__tests__/consultation-context.test.js +124 -0
  5. package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
  6. package/dist/advisory/context.d.ts +25 -0
  7. package/dist/advisory/context.d.ts.map +1 -1
  8. package/dist/advisory/context.js +18 -0
  9. package/dist/advisory/context.js.map +1 -1
  10. package/dist/advisory/executor.d.ts.map +1 -1
  11. package/dist/advisory/executor.js +25 -3
  12. package/dist/advisory/executor.js.map +1 -1
  13. package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
  14. package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
  15. package/dist/compaction/__tests__/context-reducer.test.js +197 -0
  16. package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
  17. package/dist/compaction/factory.d.ts +7 -0
  18. package/dist/compaction/factory.d.ts.map +1 -1
  19. package/dist/compaction/factory.js +7 -0
  20. package/dist/compaction/factory.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -0
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js +1 -0
  24. package/dist/compaction/index.js.map +1 -1
  25. package/dist/compaction/interface.d.ts +13 -0
  26. package/dist/compaction/interface.d.ts.map +1 -1
  27. package/dist/compaction/managers/null.d.ts +3 -0
  28. package/dist/compaction/managers/null.d.ts.map +1 -1
  29. package/dist/compaction/managers/null.js +3 -0
  30. package/dist/compaction/managers/null.js.map +1 -1
  31. package/dist/compaction/managers/slidingWindow.d.ts +6 -0
  32. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
  33. package/dist/compaction/managers/slidingWindow.js +6 -0
  34. package/dist/compaction/managers/slidingWindow.js.map +1 -1
  35. package/dist/compaction/managers/structured.d.ts +10 -0
  36. package/dist/compaction/managers/structured.d.ts.map +1 -1
  37. package/dist/compaction/managers/structured.js +10 -0
  38. package/dist/compaction/managers/structured.js.map +1 -1
  39. package/dist/compaction/reducer.d.ts +86 -0
  40. package/dist/compaction/reducer.d.ts.map +1 -0
  41. package/dist/compaction/reducer.js +77 -0
  42. package/dist/compaction/reducer.js.map +1 -0
  43. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
  44. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
  45. package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
  46. package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
  47. package/dist/connector/builtins/http.d.ts.map +1 -1
  48. package/dist/connector/builtins/http.js +24 -2
  49. package/dist/connector/builtins/http.js.map +1 -1
  50. package/dist/connector/builtins/http.test.js +18 -2
  51. package/dist/connector/builtins/http.test.js.map +1 -1
  52. package/dist/connector/index.d.ts +2 -2
  53. package/dist/connector/index.d.ts.map +1 -1
  54. package/dist/connector/index.js +1 -1
  55. package/dist/connector/index.js.map +1 -1
  56. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
  57. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
  58. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
  59. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
  60. package/dist/connector/mcp/client.d.ts +52 -1
  61. package/dist/connector/mcp/client.d.ts.map +1 -1
  62. package/dist/connector/mcp/client.js +86 -0
  63. package/dist/connector/mcp/client.js.map +1 -1
  64. package/dist/connector/mcp/discovery.d.ts +12 -1
  65. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  66. package/dist/connector/mcp/discovery.js +19 -4
  67. package/dist/connector/mcp/discovery.js.map +1 -1
  68. package/dist/connector/mcp/index.d.ts +2 -2
  69. package/dist/connector/mcp/index.d.ts.map +1 -1
  70. package/dist/connector/mcp/index.js +1 -1
  71. package/dist/connector/mcp/index.js.map +1 -1
  72. package/dist/connector/mcp/server.d.ts +42 -1
  73. package/dist/connector/mcp/server.d.ts.map +1 -1
  74. package/dist/connector/mcp/server.js +77 -4
  75. package/dist/connector/mcp/server.js.map +1 -1
  76. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
  77. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
  78. package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
  79. package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
  80. package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
  81. package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
  82. package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
  83. package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
  84. package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
  85. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
  86. package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
  87. package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
  88. package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
  89. package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
  90. package/dist/plugin/lifecycle.d.ts +41 -0
  91. package/dist/plugin/lifecycle.d.ts.map +1 -1
  92. package/dist/plugin/lifecycle.js +29 -1
  93. package/dist/plugin/lifecycle.js.map +1 -1
  94. package/dist/plugin/loader.d.ts +39 -3
  95. package/dist/plugin/loader.d.ts.map +1 -1
  96. package/dist/plugin/loader.js +37 -4
  97. package/dist/plugin/loader.js.map +1 -1
  98. package/dist/public-runtime.d.ts +4 -2
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +5 -2
  101. package/dist/public-runtime.js.map +1 -1
  102. package/dist/public-types.d.ts +2 -2
  103. package/dist/public-types.d.ts.map +1 -1
  104. package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
  105. package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
  106. package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
  107. package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
  108. package/dist/rag/ingestion.d.ts.map +1 -1
  109. package/dist/rag/ingestion.js +1 -0
  110. package/dist/rag/ingestion.js.map +1 -1
  111. package/dist/rag/retriever.d.ts.map +1 -1
  112. package/dist/rag/retriever.js +2 -0
  113. package/dist/rag/retriever.js.map +1 -1
  114. package/dist/rag/vector-store.d.ts.map +1 -1
  115. package/dist/rag/vector-store.js +6 -0
  116. package/dist/rag/vector-store.js.map +1 -1
  117. package/dist/registry/tool/execute.d.ts.map +1 -1
  118. package/dist/registry/tool/execute.js +113 -109
  119. package/dist/registry/tool/execute.js.map +1 -1
  120. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  121. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  122. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  125. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  128. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  129. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  132. package/dist/runtime/query/index.d.ts +10 -0
  133. package/dist/runtime/query/index.d.ts.map +1 -1
  134. package/dist/runtime/query/index.js +26 -0
  135. package/dist/runtime/query/index.js.map +1 -1
  136. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  137. package/dist/runtime/query/iteration/index.js +96 -34
  138. package/dist/runtime/query/iteration/index.js.map +1 -1
  139. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  140. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  141. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  142. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  143. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  144. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  145. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  147. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  148. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  149. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  150. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  151. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  153. package/dist/runtime/query/resume-run.d.ts +70 -0
  154. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  155. package/dist/runtime/query/resume-run.js +46 -0
  156. package/dist/runtime/query/resume-run.js.map +1 -0
  157. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  158. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  159. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  160. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  161. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  162. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  163. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  164. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  165. package/dist/tools/advisory/index.js +1 -1
  166. package/dist/tools/advisory/index.js.map +1 -1
  167. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  168. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  169. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  170. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  171. package/dist/tools/coordinator/index.d.ts.map +1 -1
  172. package/dist/tools/coordinator/index.js +13 -1
  173. package/dist/tools/coordinator/index.js.map +1 -1
  174. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  175. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  176. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  177. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  178. package/dist/types/agent/supervisor.d.ts +15 -0
  179. package/dist/types/agent/supervisor.d.ts.map +1 -1
  180. package/dist/types/connector/core.d.ts +34 -0
  181. package/dist/types/connector/core.d.ts.map +1 -1
  182. package/dist/types/connector/definition.d.ts +10 -0
  183. package/dist/types/connector/definition.d.ts.map +1 -1
  184. package/dist/types/connector/mcp.d.ts +13 -0
  185. package/dist/types/connector/mcp.d.ts.map +1 -1
  186. package/dist/types/rag/retrieval.d.ts +16 -0
  187. package/dist/types/rag/retrieval.d.ts.map +1 -1
  188. package/dist/types/rag/storage.d.ts +9 -0
  189. package/dist/types/rag/storage.d.ts.map +1 -1
  190. package/dist/types/rag/vector.d.ts +11 -0
  191. package/dist/types/rag/vector.d.ts.map +1 -1
  192. package/dist/types/router/task-router.d.ts +19 -0
  193. package/dist/types/router/task-router.d.ts.map +1 -1
  194. package/dist/types/run/prepare-step.d.ts +56 -3
  195. package/dist/types/run/prepare-step.d.ts.map +1 -1
  196. package/dist/types/toolset/index.d.ts +22 -0
  197. package/dist/types/toolset/index.d.ts.map +1 -1
  198. package/package.json +1 -1
  199. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  200. package/src/advisory/context.ts +32 -0
  201. package/src/advisory/executor.ts +30 -3
  202. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  203. package/src/compaction/factory.ts +7 -0
  204. package/src/compaction/index.ts +8 -0
  205. package/src/compaction/interface.ts +13 -0
  206. package/src/compaction/managers/null.ts +3 -0
  207. package/src/compaction/managers/slidingWindow.ts +6 -0
  208. package/src/compaction/managers/structured.ts +10 -0
  209. package/src/compaction/reducer.ts +154 -0
  210. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  211. package/src/connector/builtins/http.test.ts +28 -2
  212. package/src/connector/builtins/http.ts +26 -2
  213. package/src/connector/index.ts +6 -2
  214. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  215. package/src/connector/mcp/client.ts +95 -0
  216. package/src/connector/mcp/discovery.ts +19 -4
  217. package/src/connector/mcp/index.ts +6 -2
  218. package/src/connector/mcp/server.ts +101 -3
  219. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  220. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  221. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  222. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  223. package/src/plugin/lifecycle.ts +57 -1
  224. package/src/plugin/loader.ts +57 -3
  225. package/src/public-runtime.ts +8 -0
  226. package/src/public-types.ts +5 -0
  227. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  228. package/src/rag/ingestion.ts +1 -0
  229. package/src/rag/retriever.ts +2 -0
  230. package/src/rag/vector-store.ts +5 -0
  231. package/src/registry/tool/execute.ts +123 -119
  232. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  233. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  234. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  235. package/src/runtime/query/index.ts +39 -0
  236. package/src/runtime/query/iteration/index.ts +103 -34
  237. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  238. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  239. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  240. package/src/runtime/query/iteration/phases/context.ts +11 -0
  241. package/src/runtime/query/resume-run.ts +93 -0
  242. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  243. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  244. package/src/tools/advisory/index.ts +1 -1
  245. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  246. package/src/tools/coordinator/index.ts +14 -1
  247. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  248. package/src/types/agent/supervisor.ts +15 -0
  249. package/src/types/connector/core.ts +34 -0
  250. package/src/types/connector/definition.ts +10 -0
  251. package/src/types/connector/mcp.ts +14 -0
  252. package/src/types/rag/retrieval.ts +16 -0
  253. package/src/types/rag/storage.ts +9 -0
  254. package/src/types/rag/vector.ts +11 -0
  255. package/src/types/router/task-router.ts +19 -0
  256. package/src/types/run/prepare-step.ts +58 -3
  257. package/src/types/toolset/index.ts +22 -0
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { WorkingStateManager } from '../../../../compaction/manager.js'
4
+ import { CompactionConfigSchema } from '../../../../config/runtime.js'
5
+ import type { CompactionConfig } from '../../../../config/runtime.js'
6
+ import type { Message } from '../../../../types/message/index.js'
7
+ import type { TaskRouterConfig } from '../../../../types/router/index.js'
8
+ import { runCompactionCheck } from './compaction.js'
9
+ import type { IterationContext } from './context.js'
10
+
11
+ /**
12
+ * The compaction summary is the only model call a run makes that nobody
13
+ * asked for, and it was hardwired to the primary model. `taskRouter` had been
14
+ * accepted, schema-validated and threaded through four types since it was
15
+ * added, with `resolveTaskModel` exported and never called — so a host who
16
+ * pointed compaction at a cheap model kept paying the expensive one.
17
+ */
18
+
19
+ function harness(taskRouter?: TaskRouterConfig): {
20
+ ctx: IterationContext
21
+ modelsUsed: string[]
22
+ } {
23
+ const messages: Message[] = [
24
+ { role: 'system', content: 'prompt', timestamp: 1 },
25
+ ...Array.from({ length: 40 }, (_, i) => ({
26
+ role: 'user' as const,
27
+ content: `turn ${i} `.repeat(200),
28
+ timestamp: 1,
29
+ })),
30
+ ]
31
+ const config: CompactionConfig = {
32
+ ...CompactionConfigSchema.parse({}),
33
+ contextWindowTokens: 1_000,
34
+ keepRecentMessages: 4,
35
+ llmVerification: true,
36
+ // Keep the slot count under the rich-state threshold so the verified
37
+ // summary path — the one that calls a model — is the one taken.
38
+ richStateThreshold: 1_000,
39
+ }
40
+ const modelsUsed: string[] = []
41
+
42
+ const ctx = {
43
+ compactionConfig: config,
44
+ workingStateManager: new WorkingStateManager(config),
45
+ runConfig: { model: 'primary-model' },
46
+ ...(taskRouter ? { taskRouter } : {}),
47
+ runMgr: {
48
+ id: 'run_routing',
49
+ messages,
50
+ accumulateUsage: vi.fn(),
51
+ clearLastPromptTokens: vi.fn(),
52
+ },
53
+ provider: {
54
+ chatStream: async function* (params: { model: string }) {
55
+ modelsUsed.push(params.model)
56
+ yield { id: 'c1', delta: { content: 'a summary' } }
57
+ yield {
58
+ id: 'c1',
59
+ delta: {},
60
+ finishReason: 'stop',
61
+ usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 },
62
+ }
63
+ },
64
+ },
65
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
66
+ } as unknown as IterationContext
67
+
68
+ return { ctx, modelsUsed }
69
+ }
70
+
71
+ describe('the compaction summary goes to the model a host routed it to', () => {
72
+ it('uses the primary model when nothing is routed', async () => {
73
+ const h = harness()
74
+
75
+ await runCompactionCheck(h.ctx)
76
+
77
+ expect(h.modelsUsed).toEqual(['primary-model'])
78
+ })
79
+
80
+ it('uses the compaction model when one is named', async () => {
81
+ const h = harness({ compaction: 'small-model' })
82
+
83
+ await runCompactionCheck(h.ctx)
84
+
85
+ expect(h.modelsUsed).toEqual(['small-model'])
86
+ })
87
+
88
+ it('falls back to the router default', async () => {
89
+ const h = harness({ default: 'fallback-model' })
90
+
91
+ await runCompactionCheck(h.ctx)
92
+
93
+ expect(h.modelsUsed).toEqual(['fallback-model'])
94
+ })
95
+
96
+ it('prefers the specific key over the default', async () => {
97
+ const h = harness({ compaction: 'small-model', default: 'fallback-model' })
98
+
99
+ await runCompactionCheck(h.ctx)
100
+
101
+ expect(h.modelsUsed).toEqual(['small-model'])
102
+ })
103
+
104
+ it('ignores a key that names no model', async () => {
105
+ // `null` is what the schema produces for an explicitly cleared key, and
106
+ // it has to mean "unrouted" rather than "route to nothing" — sending an
107
+ // empty model id is an endpoint error on backends where the id IS the
108
+ // endpoint.
109
+ const h = harness({ compaction: null, default: 'fallback-model' })
110
+
111
+ await runCompactionCheck(h.ctx)
112
+
113
+ expect(h.modelsUsed).toEqual(['fallback-model'])
114
+ })
115
+
116
+ it('does not route a key the runtime does not consult', async () => {
117
+ // `coding` is documented as inert. Pinning that keeps the docs honest:
118
+ // if a future change starts consulting it, this test says so.
119
+ const h = harness({ coding: 'coding-model' })
120
+
121
+ await runCompactionCheck(h.ctx)
122
+
123
+ expect(h.modelsUsed).toEqual(['primary-model'])
124
+ })
125
+ })
@@ -1,10 +1,13 @@
1
1
  import { resolveContextWindow } from '../../../../compaction/context-window.js'
2
- import { findSafeTrimIndex } from '../../../../compaction/dangling.js'
2
+ import { findDanglingMessages, findSafeTrimIndex } from '../../../../compaction/dangling.js'
3
+ import type { ContextReducer, ContextReduction } from '../../../../compaction/reducer.js'
4
+ import { createSlidingWindowReducer } from '../../../../compaction/reducer.js'
3
5
  import { findRetainedIndices } from '../../../../compaction/retention.js'
4
6
  import { serializeState } from '../../../../compaction/serializer.js'
5
7
  import { clearStaleToolResults } from '../../../../compaction/tool-result-editing.js'
6
8
  import { buildVerifiedSummary } from '../../../../compaction/verifier.js'
7
9
  import { CHARS_PER_TOKEN } from '../../../../constants/limits.js'
10
+ import { resolveTaskModel } from '../../../../router/task-router.js'
8
11
  import type { Message } from '../../../../types/message/index.js'
9
12
  import { createSystemMessage } from '../../../../types/message/index.js'
10
13
  import type { IterationContext } from './context.js'
@@ -202,6 +205,78 @@ function totalChars(messages: readonly { content: unknown }[]): number {
202
205
  return total
203
206
  }
204
207
 
208
+ /**
209
+ * Run a reducer and install what it returns, or leave the history alone.
210
+ *
211
+ * Three ways to decline, all of them ending the same way — the run keeps its
212
+ * full history. `undefined` is the reducer saying so; a throw is treated as
213
+ * the same answer, because a broken reduction hook should not kill a healthy
214
+ * run any more than a broken `prepareStep` should; and a result that splits a
215
+ * tool pair is REFUSED rather than repaired.
216
+ *
217
+ * That last one is the least obvious and the most important. `tool_result`
218
+ * without its `tool_use` is a provider 400 on the next turn, so quietly
219
+ * repairing it would trade a clear "this reducer split a tool pair" for an
220
+ * opaque rejection a call later, with the reducer never implicated. The
221
+ * invariant is written on {@link ContextReducer}; enforcing it where it is
222
+ * violated is what makes it true rather than aspirational.
223
+ */
224
+ async function applyReducer(
225
+ ctx: IterationContext,
226
+ reducer: ContextReducer,
227
+ reduction: ContextReduction,
228
+ ): Promise<void> {
229
+ const messages = ctx.runMgr.messages
230
+ const before = messages.length
231
+ const beforeChars = totalChars(messages)
232
+
233
+ let next: readonly Message[] | undefined
234
+ try {
235
+ next = await reducer(reduction)
236
+ } catch (error) {
237
+ ctx.log.warn('Context reducer threw — keeping the full history', {
238
+ runId: ctx.runMgr.id,
239
+ reason: reduction.reason,
240
+ error: error instanceof Error ? error.message : String(error),
241
+ })
242
+ return
243
+ }
244
+
245
+ if (!next || next.length >= before) {
246
+ ctx.log.debug('Context reducer shed nothing', {
247
+ runId: ctx.runMgr.id,
248
+ reason: reduction.reason,
249
+ messages: before,
250
+ })
251
+ return
252
+ }
253
+
254
+ if (!findDanglingMessages([...next]).isValid) {
255
+ ctx.log.warn('Context reducer split a tool pair — refusing its result', {
256
+ runId: ctx.runMgr.id,
257
+ reason: reduction.reason,
258
+ hint: 'use findSafeTrimIndex to move a cut off a tool_use/tool_result boundary',
259
+ })
260
+ return
261
+ }
262
+
263
+ messages.length = 0
264
+ for (const message of next) messages.push(message)
265
+
266
+ // The provider's count described the pre-reduction prompt. Same reasoning
267
+ // as the structured path: leaving it would have the next trigger check
268
+ // compare an old size against the new history and reduce again.
269
+ ctx.runMgr.clearLastPromptTokens?.()
270
+
271
+ ctx.log.info('Context reduced', {
272
+ runId: ctx.runMgr.id,
273
+ reason: reduction.reason,
274
+ oldMessageCount: before,
275
+ newMessageCount: messages.length,
276
+ charsShed: beforeChars - totalChars(messages),
277
+ })
278
+ }
279
+
205
280
  export async function runCompactionCheck(
206
281
  ctx: IterationContext,
207
282
  options?: { force?: boolean },
@@ -210,9 +285,6 @@ export async function runCompactionCheck(
210
285
  if (!config) return
211
286
  if (config.strategy === 'disabled') return
212
287
 
213
- const manager = ctx.workingStateManager
214
- if (!manager) return
215
-
216
288
  const measured = measureContext(ctx)
217
289
  const estimatedTokens = measured.tokens
218
290
 
@@ -232,6 +304,29 @@ export async function runCompactionCheck(
232
304
  // rejected the prompt, which is stronger evidence than any estimate.
233
305
  if (!options?.force && usage < config.triggerThreshold) return
234
306
 
307
+ // A reducer, when the run has one, OWNS reduction — the structured pass
308
+ // below does not also run. `strategy: 'sliding-window'` resolves to the
309
+ // built-in one; a host-supplied reducer outranks the strategy entirely,
310
+ // because someone who wrote a reducer has said what they want more
311
+ // specifically than an enum can.
312
+ const reducer =
313
+ ctx.contextReducer ??
314
+ (config.strategy === 'sliding-window' ? createSlidingWindowReducer() : undefined)
315
+ if (reducer) {
316
+ await applyReducer(ctx, reducer, {
317
+ messages: ctx.runMgr.messages,
318
+ reason: options?.force ? 'overflow' : 'threshold',
319
+ estimatedTokens,
320
+ contextWindowTokens: budget,
321
+ model: ctx.runConfig.model,
322
+ keepRecentMessages: config.keepRecentMessages,
323
+ })
324
+ return
325
+ }
326
+
327
+ const manager = ctx.workingStateManager
328
+ if (!manager) return
329
+
235
330
  ctx.log.info('Compaction threshold reached — compacting context', {
236
331
  runId: ctx.runMgr.id,
237
332
  contextTokens: estimatedTokens,
@@ -380,7 +475,13 @@ export async function runCompactionCheck(
380
475
  ctx.provider,
381
476
  config,
382
477
  (usage) => ctx.runMgr.accumulateUsage(usage),
383
- ctx.runConfig.model,
478
+ // The one model call a run makes that the user never asked for. It
479
+ // reads a transcript and writes a summary, which is the cheapest
480
+ // thing a small model does well, and it fires on exactly the long
481
+ // runs where the primary model is most expensive. `taskRouter` had
482
+ // been accepted, validated and threaded through four types since it
483
+ // was added, and nothing ever consulted it.
484
+ resolveTaskModel('compaction', ctx.taskRouter, ctx.runConfig.model),
384
485
  )
385
486
  } else {
386
487
  compactedContent = serializeState(manager.getState())
@@ -0,0 +1,238 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { WorkingStateManager } from '../../../../compaction/manager.js'
4
+ import type { ContextReducer } from '../../../../compaction/reducer.js'
5
+ import { CompactionConfigSchema } from '../../../../config/runtime.js'
6
+ import type { CompactionConfig } from '../../../../config/runtime.js'
7
+ import type { Message } from '../../../../types/message/index.js'
8
+ import { runCompactionCheck } from './compaction.js'
9
+ import type { IterationContext } from './context.js'
10
+
11
+ /**
12
+ * `strategy` had three values and two behaviours.
13
+ *
14
+ * The runtime asked only `strategy !== 'disabled'`, so `'sliding-window'` —
15
+ * the name a host picks precisely to AVOID paying for summarization — ran the
16
+ * full structured pass, model call included. These tests are about the
17
+ * dispatch rather than the algorithms: that choosing a strategy selects one,
18
+ * that a host-supplied reducer outranks the enum, and that the structured
19
+ * pass does not also run when something else owns reduction.
20
+ */
21
+
22
+ const sys = (content: string): Message => ({ role: 'system', content, timestamp: 1 })
23
+ const user = (content: string): Message => ({ role: 'user', content, timestamp: 1 })
24
+
25
+ function longHistory(): Message[] {
26
+ // Long enough to be over any threshold once the window is small.
27
+ return [sys('prompt'), ...Array.from({ length: 40 }, (_, i) => user(`turn ${i} `.repeat(200)))]
28
+ }
29
+
30
+ interface Harness {
31
+ readonly ctx: IterationContext
32
+ readonly messages: Message[]
33
+ readonly providerCalls: () => number
34
+ readonly warnings: string[]
35
+ }
36
+
37
+ function harness(
38
+ over: Partial<IterationContext> & { compaction?: Partial<CompactionConfig> },
39
+ ): Harness {
40
+ const messages = longHistory()
41
+ const config: CompactionConfig = {
42
+ ...CompactionConfigSchema.parse({}),
43
+ // A tiny window puts the run over the trigger immediately.
44
+ contextWindowTokens: 1_000,
45
+ keepRecentMessages: 4,
46
+ ...over.compaction,
47
+ }
48
+ const warnings: string[] = []
49
+ let providerCalls = 0
50
+
51
+ const ctx = {
52
+ compactionConfig: config,
53
+ workingStateManager: new WorkingStateManager(config),
54
+ runConfig: { model: 'mock-model' },
55
+ runMgr: {
56
+ id: 'run_dispatch',
57
+ messages,
58
+ accumulateUsage: vi.fn(),
59
+ clearLastPromptTokens: vi.fn(),
60
+ },
61
+ provider: {
62
+ // The structured path's only observable side effect that a reducer
63
+ // path must not have: it pays for a summary.
64
+ chat: async () => {
65
+ providerCalls++
66
+ return { content: 'summary', usage: { promptTokens: 1, completionTokens: 1 } }
67
+ },
68
+ },
69
+ log: {
70
+ debug: vi.fn(),
71
+ info: vi.fn(),
72
+ warn: (message: string) => warnings.push(message),
73
+ error: vi.fn(),
74
+ },
75
+ ...over,
76
+ } as unknown as IterationContext
77
+
78
+ return { ctx, messages, providerCalls: () => providerCalls, warnings }
79
+ }
80
+
81
+ describe('choosing a strategy selects one', () => {
82
+ it("'sliding-window' trims and summarizes nothing", async () => {
83
+ const h = harness({ compaction: { strategy: 'sliding-window', llmVerification: true } })
84
+
85
+ await runCompactionCheck(h.ctx)
86
+
87
+ expect(h.messages.length).toBeLessThan(41)
88
+ // The whole reason a host names this strategy.
89
+ expect(h.providerCalls()).toBe(0)
90
+ // Every survivor is verbatim — no `[COMPACTED CONTEXT]` block appears.
91
+ expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(false)
92
+ })
93
+
94
+ it("'structured' still summarizes", async () => {
95
+ const h = harness({ compaction: { strategy: 'structured', llmVerification: false } })
96
+
97
+ await runCompactionCheck(h.ctx)
98
+
99
+ expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(true)
100
+ })
101
+
102
+ it("'disabled' leaves the history alone", async () => {
103
+ const h = harness({ compaction: { strategy: 'disabled' } })
104
+
105
+ await runCompactionCheck(h.ctx)
106
+
107
+ expect(h.messages.length).toBe(41)
108
+ })
109
+ })
110
+
111
+ describe('a host reducer outranks the enum', () => {
112
+ it('runs instead of the structured pass', async () => {
113
+ // Deliberately NOT a tiny slice: leave more than the structured pass
114
+ // needs to act, so "the reducer ran" and "only the reducer ran" are
115
+ // different observations.
116
+ const reduce: ContextReducer = ({ messages }) => messages.slice(0, 20)
117
+ const h = harness({ contextReducer: reduce, compaction: { strategy: 'structured' } })
118
+
119
+ await runCompactionCheck(h.ctx)
120
+
121
+ expect(h.messages.length).toBe(20)
122
+ expect(h.messages.some((m) => String(m.content).includes('COMPACTED CONTEXT'))).toBe(false)
123
+ })
124
+
125
+ it('is told whether the provider already rejected the prompt', async () => {
126
+ const seen: string[] = []
127
+ const reduce: ContextReducer = ({ messages, reason }) => {
128
+ seen.push(reason)
129
+ return messages.slice(0, 5)
130
+ }
131
+ const h = harness({ contextReducer: reduce })
132
+
133
+ await runCompactionCheck(h.ctx)
134
+ await runCompactionCheck(h.ctx, { force: true })
135
+
136
+ expect(seen).toEqual(['threshold', 'overflow'])
137
+ })
138
+
139
+ it('may be async, so a reducer can call a model of its own', async () => {
140
+ const reduce: ContextReducer = async ({ messages }) => {
141
+ await Promise.resolve()
142
+ return messages.slice(0, 3)
143
+ }
144
+ const h = harness({ contextReducer: reduce })
145
+
146
+ await runCompactionCheck(h.ctx)
147
+
148
+ expect(h.messages.length).toBe(3)
149
+ })
150
+
151
+ it('gets the window it has to fit, not the run token budget', async () => {
152
+ let saw = 0
153
+ const reduce: ContextReducer = ({ contextWindowTokens, messages }) => {
154
+ saw = contextWindowTokens
155
+ return messages.slice(0, 5)
156
+ }
157
+ const h = harness({ contextReducer: reduce, compaction: { contextWindowTokens: 12_345 } })
158
+
159
+ await runCompactionCheck(h.ctx)
160
+
161
+ expect(saw).toBe(12_345)
162
+ })
163
+ })
164
+
165
+ describe('a reducer that cannot be trusted is not obeyed', () => {
166
+ it('keeps the full history when the reducer throws', async () => {
167
+ const reduce: ContextReducer = () => {
168
+ throw new Error('boom')
169
+ }
170
+ const h = harness({ contextReducer: reduce })
171
+
172
+ await runCompactionCheck(h.ctx)
173
+
174
+ // Fail open: a broken reduction hook should not take down a healthy run,
175
+ // the same way a broken `prepareStep` does not.
176
+ expect(h.messages.length).toBe(41)
177
+ expect(h.warnings.some((w) => w.includes('threw'))).toBe(true)
178
+ })
179
+
180
+ it('refuses a result that orphans a tool result', async () => {
181
+ const h = harness({
182
+ contextReducer: ({ messages }) => [
183
+ messages[0] as Message,
184
+ { role: 'tool', content: 'ok', timestamp: 1, toolCallId: 'nobody' } as Message,
185
+ ],
186
+ })
187
+
188
+ await runCompactionCheck(h.ctx)
189
+
190
+ // Installing this would trade a nameable "your reducer split a tool
191
+ // pair" for an opaque provider rejection a call later.
192
+ expect(h.messages.length).toBe(41)
193
+ expect(h.warnings.some((w) => w.includes('split a tool pair'))).toBe(true)
194
+ })
195
+
196
+ it('ignores a reducer that returns the history unchanged', async () => {
197
+ const h = harness({ contextReducer: ({ messages }) => messages })
198
+
199
+ await runCompactionCheck(h.ctx)
200
+
201
+ expect(h.messages.length).toBe(41)
202
+ })
203
+
204
+ it('leaves the history alone when the reducer declines', async () => {
205
+ const h = harness({ contextReducer: () => undefined })
206
+
207
+ await runCompactionCheck(h.ctx)
208
+
209
+ expect(h.messages.length).toBe(41)
210
+ })
211
+ })
212
+
213
+ describe('the threshold still governs a reducer', () => {
214
+ it('does not run one below the trigger', async () => {
215
+ const reduce = vi.fn(() => undefined)
216
+ const h = harness({
217
+ contextReducer: reduce,
218
+ // A window far larger than the history.
219
+ compaction: { contextWindowTokens: 10_000_000 },
220
+ })
221
+
222
+ await runCompactionCheck(h.ctx)
223
+
224
+ expect(reduce).not.toHaveBeenCalled()
225
+ })
226
+
227
+ it('runs one below the trigger when the provider forced the issue', async () => {
228
+ const reduce = vi.fn(() => undefined)
229
+ const h = harness({
230
+ contextReducer: reduce,
231
+ compaction: { contextWindowTokens: 10_000_000 },
232
+ })
233
+
234
+ await runCompactionCheck(h.ctx, { force: true })
235
+
236
+ expect(reduce).toHaveBeenCalledOnce()
237
+ })
238
+ })
@@ -1,6 +1,7 @@
1
1
  import type { AdvisoryContext } from '../../../../advisory/context.js'
2
2
  import type { AgentBus } from '../../../../bus/index.js'
3
3
  import type { WorkingStateManager } from '../../../../compaction/manager.js'
4
+ import type { ContextReducer } from '../../../../compaction/reducer.js'
4
5
  import type { CompactionConfig } from '../../../../config/runtime.js'
5
6
  import type { PlanManager } from '../../../../manager/plan/lifecycle.js'
6
7
  import type { RunPersistence } from '../../../../manager/run/persistence.js'
@@ -14,6 +15,7 @@ import type {
14
15
  } from '../../../../types/hitl/index.js'
15
16
  import type { TaskId } from '../../../../types/ids/index.js'
16
17
  import type { LLMProvider } from '../../../../types/provider/index.js'
18
+ import type { TaskRouterConfig } from '../../../../types/router/index.js'
17
19
  import type { ReviewAnswer } from '../../../../types/run/answer-review.js'
18
20
  import type {
19
21
  AgentRunConfig,
@@ -104,10 +106,19 @@ export interface IterationContext {
104
106
  */
105
107
  readonly toolGrants?: ToolGrantSet
106
108
 
109
+ /** Per-task model overrides. Consulted for the compaction summary call. */
110
+ readonly taskRouter?: TaskRouterConfig
111
+
107
112
  readonly compactionConfig?: CompactionConfig
108
113
 
109
114
  readonly workingStateManager?: WorkingStateManager
110
115
 
116
+ /**
117
+ * Host-supplied context reduction. Outranks `compactionConfig.strategy`
118
+ * and replaces the structured pass for this run.
119
+ */
120
+ readonly contextReducer?: ContextReducer
121
+
111
122
  readonly workingMemoryProvider?: WorkingMemoryProvider
112
123
 
113
124
  readonly advisoryCtx?: AdvisoryContext
@@ -0,0 +1,93 @@
1
+ import type { PendingDecision } from '../../types/hitl/index.js'
2
+ import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
3
+ import type { Run } from '../../types/run/entity.js'
4
+ import type { RunState } from '../../types/run/state.js'
5
+ import { type QueryParams, drainQuery } from './index.js'
6
+ import { type RunStateScope, loadRunState } from './run-state.js'
7
+
8
+ /**
9
+ * What a resume attempt found.
10
+ *
11
+ * Three outcomes rather than a nullable `Run`, because the two failures
12
+ * mean opposite things to a caller. "No checkpoint" is a dead end — there
13
+ * is nothing to continue and starting fresh would be a different run
14
+ * wearing the same id. "Awaiting decision" is the run working as designed:
15
+ * it parked on a question, and continuing means answering it first.
16
+ */
17
+ export type ResumeOutcome =
18
+ | { readonly resumed: false; readonly reason: 'no-checkpoint' }
19
+ | {
20
+ readonly resumed: false
21
+ readonly reason: 'awaiting-decision'
22
+ /** What to put in front of a human. */
23
+ readonly pending: PendingDecision
24
+ readonly state: RunState
25
+ }
26
+ | { readonly resumed: true; readonly run: Run; readonly state: RunState }
27
+
28
+ /**
29
+ * The half of a run that cannot be serialized, plus where to look.
30
+ *
31
+ * `messages` is deliberately absent. On resume the history comes from the
32
+ * checkpoint and any passed messages would be discarded, so accepting them
33
+ * would invite a caller to hand over a transcript that is silently ignored.
34
+ */
35
+ export interface ResumeRunParams
36
+ extends Omit<QueryParams, 'messages' | 'runId' | 'resumeFromCheckpoint'> {
37
+ /** Identifies the run inside the checkpoint store. */
38
+ readonly scope: RunStateScope
39
+ /** Required to find the checkpoint; also threaded into the resumed run. */
40
+ readonly checkpointStore: CheckpointStore
41
+ /**
42
+ * Resume a specific checkpoint instead of the one the store would pick.
43
+ * Absent means the parked checkpoint if there is one, else the newest.
44
+ */
45
+ readonly checkpointId?: RunState['checkpointId']
46
+ }
47
+
48
+ /**
49
+ * Continue a run that a different process started.
50
+ *
51
+ * Everything this needs already existed — `CheckpointManager` writes the
52
+ * history, budgets, working state, trace context and any park;
53
+ * `loadRunState` reads them back; `query` accepts `runId` +
54
+ * `resumeFromCheckpoint` and restores all of it. What was missing was the
55
+ * few lines that join them, so every host wrote their own — and in this
56
+ * repo nothing outside the SDK ever did, which means the whole path shipped
57
+ * untravelled.
58
+ *
59
+ * The division of labour is the one the mechanism already implies: the
60
+ * CALLER brings what cannot be serialized — the provider client, the tool
61
+ * registry, the sandbox, the working directory — and the STORE brings the
62
+ * state. A snapshot deliberately holds no socket, no client and no open
63
+ * file, so it could never have carried the first half.
64
+ *
65
+ * Refusing beats guessing at both failure points. A missing checkpoint does
66
+ * not silently become a fresh run under a recycled id, and a park is not
67
+ * resumed past without the answer it is waiting for.
68
+ */
69
+ export async function resumeRun(params: ResumeRunParams): Promise<ResumeOutcome> {
70
+ const { scope, checkpointStore, checkpointId, pendingDecision, ...rest } = params
71
+
72
+ const state = await loadRunState(checkpointStore, scope, checkpointId)
73
+ if (!state?.checkpointId) return { resumed: false, reason: 'no-checkpoint' }
74
+
75
+ // A park is outstanding until it is answered. `resolvedAt` is what marks
76
+ // an answered one, so a checkpoint that carries a resolved decision is an
77
+ // ordinary resume, not a question waiting on anybody.
78
+ const outstanding = state.pending && !state.pending.resolvedAt ? state.pending : undefined
79
+ if (outstanding && !pendingDecision) {
80
+ return { resumed: false, reason: 'awaiting-decision', pending: outstanding, state }
81
+ }
82
+
83
+ const run = await drainQuery({
84
+ ...rest,
85
+ messages: [],
86
+ runId: state.runId,
87
+ resumeFromCheckpoint: state.checkpointId,
88
+ checkpointStore,
89
+ ...(pendingDecision ? { pendingDecision } : {}),
90
+ } as QueryParams)
91
+
92
+ return { resumed: true, run, state }
93
+ }