@namzu/sdk 3.0.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 (307) hide show
  1. package/CHANGELOG.md +223 -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 +5 -3
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +11 -3
  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__/capability-negotiation.test.js +59 -0
  121. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  122. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  125. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  128. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  129. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  132. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  133. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  134. package/dist/runtime/query/executor.d.ts +1 -0
  135. package/dist/runtime/query/executor.d.ts.map +1 -1
  136. package/dist/runtime/query/executor.js +10 -1
  137. package/dist/runtime/query/executor.js.map +1 -1
  138. package/dist/runtime/query/index.d.ts +10 -0
  139. package/dist/runtime/query/index.d.ts.map +1 -1
  140. package/dist/runtime/query/index.js +26 -0
  141. package/dist/runtime/query/index.js.map +1 -1
  142. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  143. package/dist/runtime/query/iteration/index.js +118 -34
  144. package/dist/runtime/query/iteration/index.js.map +1 -1
  145. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  146. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  147. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  148. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  149. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  150. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  151. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  153. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  154. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  155. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  156. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  157. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  158. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  159. package/dist/runtime/query/resume-run.d.ts +70 -0
  160. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  161. package/dist/runtime/query/resume-run.js +46 -0
  162. package/dist/runtime/query/resume-run.js.map +1 -0
  163. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  164. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  165. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  166. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  167. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  168. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  169. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  170. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  171. package/dist/tools/advisory/index.js +1 -1
  172. package/dist/tools/advisory/index.js.map +1 -1
  173. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
  174. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
  175. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
  176. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
  177. package/dist/tools/builtins/__tests__/edit.test.js +242 -1
  178. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  179. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
  180. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
  181. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
  182. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
  183. package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
  184. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
  185. package/dist/tools/builtins/atomic-write-file.js +31 -2
  186. package/dist/tools/builtins/atomic-write-file.js.map +1 -1
  187. package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
  188. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
  189. package/dist/tools/builtins/content-fingerprint.js +30 -0
  190. package/dist/tools/builtins/content-fingerprint.js.map +1 -0
  191. package/dist/tools/builtins/edit.d.ts.map +1 -1
  192. package/dist/tools/builtins/edit.js +172 -31
  193. package/dist/tools/builtins/edit.js.map +1 -1
  194. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  195. package/dist/tools/builtins/read-file.js +5 -2
  196. package/dist/tools/builtins/read-file.js.map +1 -1
  197. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  198. package/dist/tools/builtins/write-file.js +75 -27
  199. package/dist/tools/builtins/write-file.js.map +1 -1
  200. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
  201. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
  202. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  203. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  204. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  205. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  206. package/dist/tools/coordinator/index.d.ts.map +1 -1
  207. package/dist/tools/coordinator/index.js +75 -5
  208. package/dist/tools/coordinator/index.js.map +1 -1
  209. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  210. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  211. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  212. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  213. package/dist/types/agent/supervisor.d.ts +15 -0
  214. package/dist/types/agent/supervisor.d.ts.map +1 -1
  215. package/dist/types/connector/core.d.ts +34 -0
  216. package/dist/types/connector/core.d.ts.map +1 -1
  217. package/dist/types/connector/definition.d.ts +10 -0
  218. package/dist/types/connector/definition.d.ts.map +1 -1
  219. package/dist/types/connector/mcp.d.ts +13 -0
  220. package/dist/types/connector/mcp.d.ts.map +1 -1
  221. package/dist/types/rag/retrieval.d.ts +16 -0
  222. package/dist/types/rag/retrieval.d.ts.map +1 -1
  223. package/dist/types/rag/storage.d.ts +9 -0
  224. package/dist/types/rag/storage.d.ts.map +1 -1
  225. package/dist/types/rag/vector.d.ts +11 -0
  226. package/dist/types/rag/vector.d.ts.map +1 -1
  227. package/dist/types/router/task-router.d.ts +19 -0
  228. package/dist/types/router/task-router.d.ts.map +1 -1
  229. package/dist/types/run/prepare-step.d.ts +56 -3
  230. package/dist/types/run/prepare-step.d.ts.map +1 -1
  231. package/dist/types/tool/index.d.ts +17 -1
  232. package/dist/types/tool/index.d.ts.map +1 -1
  233. package/dist/types/tool/index.js.map +1 -1
  234. package/dist/types/toolset/index.d.ts +22 -0
  235. package/dist/types/toolset/index.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  238. package/src/advisory/context.ts +32 -0
  239. package/src/advisory/executor.ts +30 -3
  240. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  241. package/src/compaction/factory.ts +7 -0
  242. package/src/compaction/index.ts +8 -0
  243. package/src/compaction/interface.ts +13 -0
  244. package/src/compaction/managers/null.ts +3 -0
  245. package/src/compaction/managers/slidingWindow.ts +6 -0
  246. package/src/compaction/managers/structured.ts +10 -0
  247. package/src/compaction/reducer.ts +154 -0
  248. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  249. package/src/connector/builtins/http.test.ts +28 -2
  250. package/src/connector/builtins/http.ts +26 -2
  251. package/src/connector/index.ts +6 -2
  252. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  253. package/src/connector/mcp/client.ts +95 -0
  254. package/src/connector/mcp/discovery.ts +19 -4
  255. package/src/connector/mcp/index.ts +6 -2
  256. package/src/connector/mcp/server.ts +101 -3
  257. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  258. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  259. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  260. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  261. package/src/plugin/lifecycle.ts +57 -1
  262. package/src/plugin/loader.ts +57 -3
  263. package/src/public-runtime.ts +16 -1
  264. package/src/public-types.ts +5 -0
  265. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  266. package/src/rag/ingestion.ts +1 -0
  267. package/src/rag/retriever.ts +2 -0
  268. package/src/rag/vector-store.ts +5 -0
  269. package/src/registry/tool/execute.ts +123 -119
  270. package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
  271. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  272. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  273. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  274. package/src/runtime/query/executor.ts +9 -1
  275. package/src/runtime/query/index.ts +39 -0
  276. package/src/runtime/query/iteration/index.ts +130 -34
  277. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  278. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  279. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  280. package/src/runtime/query/iteration/phases/context.ts +11 -0
  281. package/src/runtime/query/resume-run.ts +93 -0
  282. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  283. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  284. package/src/tools/advisory/index.ts +1 -1
  285. package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
  286. package/src/tools/builtins/__tests__/edit.test.ts +324 -1
  287. package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
  288. package/src/tools/builtins/atomic-write-file.ts +30 -2
  289. package/src/tools/builtins/content-fingerprint.ts +33 -0
  290. package/src/tools/builtins/edit.ts +188 -36
  291. package/src/tools/builtins/read-file.ts +5 -2
  292. package/src/tools/builtins/write-file.ts +79 -28
  293. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
  294. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  295. package/src/tools/coordinator/index.ts +116 -42
  296. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  297. package/src/types/agent/supervisor.ts +15 -0
  298. package/src/types/connector/core.ts +34 -0
  299. package/src/types/connector/definition.ts +10 -0
  300. package/src/types/connector/mcp.ts +14 -0
  301. package/src/types/rag/retrieval.ts +16 -0
  302. package/src/types/rag/storage.ts +9 -0
  303. package/src/types/rag/vector.ts +11 -0
  304. package/src/types/router/task-router.ts +19 -0
  305. package/src/types/run/prepare-step.ts +58 -3
  306. package/src/types/tool/index.ts +17 -1
  307. package/src/types/toolset/index.ts +22 -0
@@ -5,11 +5,13 @@ import {
5
5
  DEFAULT_STRUCTURED_OUTPUT_RETRIES,
6
6
  STRUCTURED_OUTPUT_REPROMPT,
7
7
  } from '../../../constants/tools/index.js'
8
+ import { renderSkillsSection } from '../../../persona/assembler.js'
8
9
  import { collect } from '../../../provider/collect.js'
9
10
  import {
10
11
  GENAI,
11
12
  NAMZU,
12
13
  agentIterationSpanName,
14
+ chatSpanName,
13
15
  parentContext,
14
16
  } from '../../../telemetry/attributes.js'
15
17
  import { getTracer } from '../../../telemetry/runtime-accessors.js'
@@ -21,6 +23,7 @@ import {
21
23
  createSystemMessage,
22
24
  createUserMessage,
23
25
  } from '../../../types/message/index.js'
26
+ import type { ToolChoice } from '../../../types/provider/chat.js'
24
27
  import { classifyProviderError } from '../../../types/provider/errors.js'
25
28
  import type { ChatCompletionResponse } from '../../../types/provider/index.js'
26
29
  import type { AnswerReview } from '../../../types/run/answer-review.js'
@@ -30,6 +33,8 @@ import type {
30
33
  StepResult,
31
34
  StopReason,
32
35
  } from '../../../types/run/index.js'
36
+ import type { Skill } from '../../../types/skills/index.js'
37
+ import type { LLMToolSchema, ToolRegistryContract } from '../../../types/tool/index.js'
33
38
  import { toErrorMessage } from '../../../utils/error.js'
34
39
  import { generateMessageId } from '../../../utils/id.js'
35
40
  import type { ToolCallOutcome } from '../executor.js'
@@ -149,23 +154,30 @@ export class IterationOrchestrator {
149
154
  {},
150
155
  parentContext(this.ctx.rootSpan),
151
156
  )
152
- // Tool spans for this turn belong under this iteration.
153
- this.ctx.toolExecutor.setParentSpan(iterSpan)
157
+ // Declared out here so the iteration's own finally can close it on
158
+ // any path that does not reach its success branch.
159
+ let chatSpan: Span | undefined
160
+ try {
161
+ // Tool spans for this turn belong under this iteration. Inside
162
+ // the try rather than before it: a throw from any of these left
163
+ // the span open, and an iteration span that never ends is a
164
+ // trace that never closes — the export is incomplete for exactly
165
+ // the run that failed.
166
+ this.ctx.toolExecutor.setParentSpan(iterSpan)
154
167
 
155
- iterSpan.setAttributes({
156
- [NAMZU.ITERATION]: iterationNum,
157
- [NAMZU.RUN_ID]: runMgr.id,
158
- [GENAI.REQUEST_MODEL]: model,
159
- })
168
+ iterSpan.setAttributes({
169
+ [NAMZU.ITERATION]: iterationNum,
170
+ [NAMZU.RUN_ID]: runMgr.id,
171
+ [GENAI.REQUEST_MODEL]: model,
172
+ })
160
173
 
161
- await this.ctx.emitEvent({
162
- type: 'iteration_started',
163
- runId: runMgr.id,
164
- iteration: iterationNum,
165
- })
166
- yield* this.ctx.drainPending()
174
+ await this.ctx.emitEvent({
175
+ type: 'iteration_started',
176
+ runId: runMgr.id,
177
+ iteration: iterationNum,
178
+ })
179
+ yield* this.ctx.drainPending()
167
180
 
168
- try {
169
181
  if (this.ctx.pluginManager) {
170
182
  const hookResults = await this.ctx.pluginManager.executeHooks(
171
183
  'iteration_start',
@@ -200,6 +212,7 @@ export class IterationOrchestrator {
200
212
  const step = await this.prepareStep(iterationNum)
201
213
 
202
214
  const llmTools = this.ctx.tools.toLLMTools(step.allowedTools ?? this.ctx.allowedTools)
215
+ const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
203
216
  const stepModel = step.model ?? model
204
217
 
205
218
  const baseMessages = forceFinalize
@@ -223,8 +236,17 @@ export class IterationOrchestrator {
223
236
  // exactly this reason. Shallow is enough: the defect is array
224
237
  // mutation, and per-iteration this is trivial next to the model
225
238
  // call it precedes.
226
- const messages = step.system
227
- ? [...baseMessages, createSystemMessage(step.system)]
239
+ // A step's skills and its guidance ride the same ephemeral
240
+ // trailing system message. Appending leaves the cached prefix
241
+ // intact; rewriting the run's own prompt to carry a phase's
242
+ // skills would invalidate it on every iteration.
243
+ // `renderSkillsSection` already answers null for an empty list, so
244
+ // there is no length check here — a second guard for the same
245
+ // case is one more thing to keep in agreement with the first.
246
+ const stepSkills = step.skills ? renderSkillsSection([...step.skills]) : null
247
+ const stepPreamble = [step.system, stepSkills].filter(Boolean).join('\n\n')
248
+ const messages = stepPreamble
249
+ ? [...baseMessages, createSystemMessage(stepPreamble)]
228
250
  : [...baseMessages]
229
251
 
230
252
  if (this.ctx.pluginManager) {
@@ -260,13 +282,45 @@ export class IterationOrchestrator {
260
282
  // aggregated `ChatCompletionResponse` for the legacy
261
283
  // downstream paths (assistantMsg construction, working
262
284
  // state extraction, telemetry attribute stamping).
285
+ // The model call gets its own span. There was none at all —
286
+ // `chatSpanName` shipped with zero call sites — so a run's traces
287
+ // carried no LLM latency whatsoever, and the one thing anybody
288
+ // opens a trace to find (which turn was slow, and why) was the
289
+ // one thing not in it.
290
+ chatSpan = tracer.startSpan(chatSpanName(stepModel), {}, parentContext(iterSpan))
291
+ chatSpan.setAttributes({
292
+ [GENAI.OPERATION_NAME]: 'chat',
293
+ [GENAI.SYSTEM]: this.ctx.provider.id,
294
+ [GENAI.REQUEST_MODEL]: stepModel,
295
+ ...((step.temperature ?? runConfig.temperature) !== undefined
296
+ ? { [GENAI.REQUEST_TEMPERATURE]: (step.temperature ?? runConfig.temperature) as number }
297
+ : {}),
298
+ ...((step.maxResponseTokens ?? runConfig.maxResponseTokens) !== undefined
299
+ ? {
300
+ [GENAI.REQUEST_MAX_TOKENS]: (step.maxResponseTokens ??
301
+ runConfig.maxResponseTokens) as number,
302
+ }
303
+ : {}),
304
+ })
305
+
263
306
  const { response, messageId } = yield* streamProviderTurn(
264
307
  this.ctx.provider,
265
308
  {
266
309
  model: stepModel,
267
310
  messages,
268
311
  tools: llmTools.length > 0 ? llmTools : undefined,
269
- toolChoice: forceFinalize && llmTools.length > 0 ? 'none' : undefined,
312
+ ...(enforceToolInputSchema ? { enforceToolInputSchema } : {}),
313
+ // The forced-final turn wins: a step that asked to force a
314
+ // tool cannot override the loop's own decision to stop
315
+ // asking for them. Otherwise the step's choice applies —
316
+ // and only to this step, because the next one is prepared
317
+ // from scratch.
318
+ toolChoice:
319
+ forceFinalize && llmTools.length > 0
320
+ ? 'none'
321
+ : llmTools.length > 0
322
+ ? step.toolChoice
323
+ : undefined,
270
324
  temperature: step.temperature ?? runConfig.temperature,
271
325
  maxTokens: step.maxResponseTokens ?? runConfig.maxResponseTokens,
272
326
  cacheControl: { type: 'auto' },
@@ -285,6 +339,27 @@ export class IterationOrchestrator {
285
339
  iterSpan,
286
340
  )
287
341
 
342
+ // Stamped on the call that produced them. The token counts also
343
+ // stay on the iteration span below, where they have always been:
344
+ // moving them would silently break whatever reads them today,
345
+ // and one turn per iteration makes the two agree.
346
+ chatSpan.setAttributes({
347
+ [GENAI.RESPONSE_MODEL]: response.model || stepModel,
348
+ [GENAI.RESPONSE_ID]: response.id,
349
+ [GENAI.USAGE_INPUT_TOKENS]: response.usage.promptTokens,
350
+ [GENAI.USAGE_OUTPUT_TOKENS]: response.usage.completionTokens,
351
+ // An array, per the semantic convention: one call can finish
352
+ // several ways when a provider returns more than one choice.
353
+ [GENAI.RESPONSE_FINISH_REASONS]: [response.finishReason ?? 'stop'],
354
+ [NAMZU.CACHE_READ_TOKENS]: response.usage.cachedTokens ?? 0,
355
+ [NAMZU.CACHE_WRITE_TOKENS]: response.usage.cacheWriteTokens ?? 0,
356
+ })
357
+ chatSpan.setStatus({ code: SpanStatusCode.OK })
358
+ chatSpan.end()
359
+ // Closed here for an accurate duration, and cleared so the
360
+ // iteration finally does not close it a second time.
361
+ chatSpan = undefined
362
+
288
363
  // Main-loop turn: also records the prompt size compaction reads.
289
364
  runMgr.recordTurnUsage(response.usage)
290
365
 
@@ -443,7 +518,6 @@ export class IterationOrchestrator {
443
518
  hasToolCalls: false,
444
519
  })
445
520
  yield* this.ctx.drainPending()
446
- iterSpan.end()
447
521
  continue
448
522
  }
449
523
 
@@ -461,7 +535,6 @@ export class IterationOrchestrator {
461
535
  attempts: attempt - 1,
462
536
  })
463
537
  runMgr.setStopReason('structured_output_failed')
464
- iterSpan.end()
465
538
  break
466
539
  }
467
540
  this.ctx.log.info('Re-prompting for structured output', {
@@ -477,7 +550,6 @@ export class IterationOrchestrator {
477
550
  hasToolCalls: false,
478
551
  })
479
552
  yield* this.ctx.drainPending()
480
- iterSpan.end()
481
553
  continue
482
554
  }
483
555
 
@@ -506,7 +578,6 @@ export class IterationOrchestrator {
506
578
  limit,
507
579
  })
508
580
  runMgr.setStopReason('answer_rejected')
509
- iterSpan.end()
510
581
  break
511
582
  }
512
583
  this.ctx.log.info('Answer rejected — returning it to the model', {
@@ -522,7 +593,6 @@ export class IterationOrchestrator {
522
593
  hasToolCalls: false,
523
594
  })
524
595
  yield* this.ctx.drainPending()
525
- iterSpan.end()
526
596
  continue
527
597
  }
528
598
  }
@@ -550,11 +620,9 @@ export class IterationOrchestrator {
550
620
  if (this.ctx.abortController.signal.aborted) {
551
621
  runMgr.setStopReason('cancelled')
552
622
  runMgr.markCancelled()
553
- iterSpan.end()
554
623
  break
555
624
  }
556
625
  runMgr.setStopReason('end_turn')
557
- iterSpan.end()
558
626
  break
559
627
  }
560
628
 
@@ -576,12 +644,10 @@ export class IterationOrchestrator {
576
644
  })
577
645
 
578
646
  if (reviewOutcome.decision === 'stop') {
579
- iterSpan.end()
580
647
  return
581
648
  }
582
649
 
583
650
  if (reviewOutcome.decision === 'rejected') {
584
- iterSpan.end()
585
651
  continue
586
652
  }
587
653
 
@@ -601,7 +667,6 @@ export class IterationOrchestrator {
601
667
  hasToolCalls: true,
602
668
  })
603
669
  yield* this.ctx.drainPending()
604
- iterSpan.end()
605
670
  break
606
671
  }
607
672
 
@@ -628,7 +693,6 @@ export class IterationOrchestrator {
628
693
  hasToolCalls: true,
629
694
  })
630
695
  yield* this.ctx.drainPending()
631
- iterSpan.end()
632
696
  break
633
697
  }
634
698
 
@@ -648,13 +712,11 @@ export class IterationOrchestrator {
648
712
  hasToolCalls: true,
649
713
  })
650
714
  yield* this.ctx.drainPending()
651
- iterSpan.end()
652
715
  break
653
716
  }
654
717
 
655
718
  const checkpointSignal = yield* runIterationCheckpoint(this.ctx, iterationNum)
656
719
  if (checkpointSignal === 'stop') {
657
- iterSpan.end()
658
720
  return
659
721
  }
660
722
 
@@ -677,7 +739,6 @@ export class IterationOrchestrator {
677
739
  hasToolCalls: true,
678
740
  })
679
741
  yield* this.ctx.drainPending()
680
- iterSpan.end()
681
742
  } catch (err) {
682
743
  // A Stop that aborted the in-flight turn surfaces here as a
683
744
  // thrown abort (the provider stream was raced against the run
@@ -689,7 +750,6 @@ export class IterationOrchestrator {
689
750
  if (this.ctx.abortController.signal.aborted) {
690
751
  runMgr.setStopReason('cancelled')
691
752
  runMgr.markCancelled()
692
- iterSpan.end()
693
753
  break
694
754
  }
695
755
 
@@ -719,7 +779,6 @@ export class IterationOrchestrator {
719
779
  if (iterationActivity) {
720
780
  this.ctx.activityStore.complete(iterationActivity.id)
721
781
  }
722
- iterSpan.end()
723
782
  continue
724
783
  }
725
784
  }
@@ -733,8 +792,15 @@ export class IterationOrchestrator {
733
792
  message: toErrorMessage(err),
734
793
  })
735
794
  iterSpan.recordException(err instanceof Error ? err : new Error(String(err)))
736
- iterSpan.end()
737
795
  throw err
796
+ } finally {
797
+ // A model call that threw never reached its own close above.
798
+ chatSpan?.end()
799
+ // The only place the iteration span ends. It used to be ended at each of
800
+ // seventeen exits, which is a rule every future edit has to
801
+ // remember; a generator abandoned by its consumer never reached
802
+ // any of them.
803
+ iterSpan.end()
738
804
  }
739
805
  }
740
806
  }
@@ -749,8 +815,10 @@ export class IterationOrchestrator {
749
815
  */
750
816
  private async prepareStep(stepNumber: number): Promise<{
751
817
  allowedTools?: string[]
818
+ toolChoice?: ToolChoice
752
819
  model?: string
753
820
  system?: string
821
+ skills?: readonly Skill[]
754
822
  temperature?: number
755
823
  maxResponseTokens?: number
756
824
  }> {
@@ -786,8 +854,10 @@ export class IterationOrchestrator {
786
854
 
787
855
  const prepared: {
788
856
  allowedTools?: string[]
857
+ toolChoice?: ToolChoice
789
858
  model?: string
790
859
  system?: string
860
+ skills?: readonly Skill[]
791
861
  temperature?: number
792
862
  maxResponseTokens?: number
793
863
  } = {}
@@ -806,8 +876,10 @@ export class IterationOrchestrator {
806
876
  }
807
877
  prepared.allowedTools = known
808
878
  }
879
+ if (result.toolChoice !== undefined) prepared.toolChoice = result.toolChoice
809
880
  if (result.model !== undefined) prepared.model = result.model
810
881
  if (result.system !== undefined) prepared.system = result.system
882
+ if (result.skills !== undefined) prepared.skills = result.skills
811
883
  if (result.temperature !== undefined) prepared.temperature = result.temperature
812
884
  if (result.maxResponseTokens !== undefined) {
813
885
  prepared.maxResponseTokens = result.maxResponseTokens
@@ -1042,11 +1114,13 @@ export class IterationOrchestrator {
1042
1114
  // tools param identical to prior iterations (cache prefix intact,
1043
1115
  // no 400 on tool blocks in history) and forbid use via tool_choice.
1044
1116
  const finalTools = this.ctx.tools.toLLMTools(this.ctx.allowedTools)
1117
+ const finalEnforced = enforcedModelInputToolNames(this.ctx.tools, finalTools)
1045
1118
  const response = await collect(
1046
1119
  this.ctx.provider.chatStream({
1047
1120
  model,
1048
1121
  messages: finalMessages,
1049
1122
  tools: finalTools.length > 0 ? finalTools : undefined,
1123
+ ...(finalEnforced ? { enforceToolInputSchema: finalEnforced } : {}),
1050
1124
  toolChoice: finalTools.length > 0 ? 'none' : undefined,
1051
1125
  temperature: this.ctx.runConfig.temperature,
1052
1126
  maxTokens: this.ctx.runConfig.maxResponseTokens,
@@ -1102,3 +1176,25 @@ function subtractUsage(after: TokenUsage, before: TokenUsage): TokenUsage {
1102
1176
  function round6(n: number): number {
1103
1177
  return Math.round(n * 1e6) / 1e6
1104
1178
  }
1179
+
1180
+ /**
1181
+ * Which of the tools going out on this request have a closed model schema.
1182
+ *
1183
+ * A driver reads `enforceToolInputSchema` to decide which tool schemas to
1184
+ * constrain generation against. Nothing populated it, so every driver that
1185
+ * consumed it — three of them — was reading a permanently undefined field
1186
+ * and `enforceModelInput: true` on a tool meant nothing end to end.
1187
+ *
1188
+ * Computed per request rather than once, because the allowed set changes:
1189
+ * a deferred tool activated mid-run has to start being enforced from the
1190
+ * next call, not the next process.
1191
+ */
1192
+ function enforcedModelInputToolNames(
1193
+ registry: ToolRegistryContract,
1194
+ tools: readonly LLMToolSchema[],
1195
+ ): readonly string[] | undefined {
1196
+ const names = tools
1197
+ .map((tool) => tool.function.name)
1198
+ .filter((name) => registry.get(name)?.enforceModelInput === true)
1199
+ return names.length > 0 ? names : undefined
1200
+ }
@@ -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())