@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
@@ -0,0 +1,180 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { z } from 'zod'
6
+
7
+ import { MockLLMProvider } from '../../../provider/mock.js'
8
+ import { ToolRegistry } from '../../../registry/tool/execute.js'
9
+ import type { SessionId, TenantId } from '../../../types/ids/index.js'
10
+ import { createUserMessage } from '../../../types/message/index.js'
11
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
12
+ import { drainQuery } from '../index.js'
13
+
14
+ /**
15
+ * Forcing a tool is the one step-shaping knob that can hang an agent.
16
+ *
17
+ * A forced choice that PERSISTS makes the model call a tool, read the
18
+ * result, and be forced again — forever. The peer SDK that puts
19
+ * `tool_choice` on persistent model settings carries three moving parts to
20
+ * undo that: a tool-use tracker, an opt-out flag, and a reset applied at
21
+ * two separate call sites. Its flag defaults to on precisely because
22
+ * turning it off hangs the agent.
23
+ *
24
+ * Putting the knob on `prepareStep` removes the failure instead of
25
+ * managing it: the next step is prepared from scratch, so a force cannot
26
+ * outlive the step that asked for it. There is no flag to get wrong.
27
+ */
28
+
29
+ let workdirs: string[] = []
30
+
31
+ afterEach(async () => {
32
+ await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
33
+ workdirs = []
34
+ })
35
+
36
+ async function mkWorkdir(): Promise<string> {
37
+ const dir = await mkdtemp(join(tmpdir(), 'namzu-tool-choice-'))
38
+ workdirs.push(dir)
39
+ return dir
40
+ }
41
+
42
+ function registerEcho(tools: ToolRegistry): void {
43
+ tools.register({
44
+ name: 'echo',
45
+ description: 'Echo the text back.',
46
+ inputSchema: z.object({ text: z.string().optional() }),
47
+ execute: async () => ({ success: true, output: 'ok' }),
48
+ })
49
+ }
50
+
51
+ async function baseParams(provider: MockLLMProvider, tools: ToolRegistry) {
52
+ return {
53
+ provider,
54
+ tools,
55
+ runConfig: {
56
+ model: 'mock-model',
57
+ timeoutMs: 30_000,
58
+ tokenBudget: 100_000,
59
+ maxIterations: 4,
60
+ maxResponseTokens: 256,
61
+ },
62
+ agentId: 'agent_tc',
63
+ agentName: 'Tool Choice Agent',
64
+ workingDirectory: await mkWorkdir(),
65
+ sessionId: 'ses_tc' as SessionId,
66
+ threadId: 'thd_tc' as ThreadId,
67
+ projectId: 'prj_tc' as ProjectId,
68
+ tenantId: 'tnt_tc' as TenantId,
69
+ messages: [createUserMessage('go')],
70
+ }
71
+ }
72
+
73
+ describe('a step can force the model to call a tool', () => {
74
+ it('puts the caller choice on that step request', async () => {
75
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
76
+ const tools = new ToolRegistry()
77
+ registerEcho(tools)
78
+
79
+ await drainQuery({
80
+ ...(await baseParams(provider, tools)),
81
+ prepareStep: () => ({ toolChoice: 'required' as const }),
82
+ })
83
+
84
+ expect(provider.requests.at(0)?.toolChoice).toBe('required')
85
+ })
86
+
87
+ it('carries a named function through unchanged', async () => {
88
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
89
+ const tools = new ToolRegistry()
90
+ registerEcho(tools)
91
+
92
+ await drainQuery({
93
+ ...(await baseParams(provider, tools)),
94
+ prepareStep: () => ({
95
+ toolChoice: { type: 'function' as const, function: { name: 'echo' } },
96
+ }),
97
+ })
98
+
99
+ expect(provider.requests.at(0)?.toolChoice).toEqual({
100
+ type: 'function',
101
+ function: { name: 'echo' },
102
+ })
103
+ })
104
+
105
+ it('leaves the request alone when no step asks', async () => {
106
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
107
+ const tools = new ToolRegistry()
108
+ registerEcho(tools)
109
+
110
+ await drainQuery(await baseParams(provider, tools))
111
+
112
+ expect(provider.requests.at(0)?.toolChoice).toBeUndefined()
113
+ })
114
+
115
+ it('says nothing when there are no tools to choose between', async () => {
116
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
117
+
118
+ await drainQuery({
119
+ ...(await baseParams(provider, new ToolRegistry())),
120
+ prepareStep: () => ({ toolChoice: 'required' as const }),
121
+ })
122
+
123
+ // `tool_choice` alongside an absent tool list is rejected by the
124
+ // providers, so a forced choice with nothing registered has to drop.
125
+ expect(provider.requests.at(0)?.toolChoice).toBeUndefined()
126
+ })
127
+ })
128
+
129
+ /**
130
+ * NOT covered here, and deliberately not faked: the forced-final turn takes
131
+ * precedence over a step's choice (`forceFinalize` wins in
132
+ * `iteration/index.ts`), so a budget-exhausted run can still stop asking
133
+ * for tools and answer. Reaching that branch needs the guard to raise a
134
+ * budget warning, which needs real usage the mock provider does not report.
135
+ * The precedence is implemented and read in review; it is not pinned.
136
+ */
137
+ describe('a forced choice cannot outlive the step that asked for it', () => {
138
+ it('applies to the named step only, leaving later steps free', async () => {
139
+ const provider = new MockLLMProvider({
140
+ turns: [
141
+ { toolCalls: [{ id: 'c1', name: 'echo', rawArguments: '{}' }] },
142
+ { text: 'finished' },
143
+ ],
144
+ })
145
+ const tools = new ToolRegistry()
146
+ registerEcho(tools)
147
+
148
+ await drainQuery({
149
+ ...(await baseParams(provider, tools)),
150
+ // Only the first step is forced. Without per-step scoping this
151
+ // would keep forcing and the run would never reach a text answer.
152
+ prepareStep: ({ stepNumber }) =>
153
+ stepNumber === 1 ? { toolChoice: 'required' as const } : {},
154
+ })
155
+
156
+ expect(provider.requests.at(0)?.toolChoice).toBe('required')
157
+ expect(provider.requests.at(1)?.toolChoice).toBeUndefined()
158
+ })
159
+
160
+ it('a run whose every step forces a tool still ends, bounded by the loop', async () => {
161
+ const provider = new MockLLMProvider({
162
+ turns: [
163
+ { toolCalls: [{ id: 'c1', name: 'echo', rawArguments: '{}' }] },
164
+ { toolCalls: [{ id: 'c2', name: 'echo', rawArguments: '{}' }] },
165
+ { text: 'finished' },
166
+ ],
167
+ })
168
+ const tools = new ToolRegistry()
169
+ registerEcho(tools)
170
+
171
+ const run = await drainQuery({
172
+ ...(await baseParams(provider, tools)),
173
+ prepareStep: () => ({ toolChoice: 'required' as const }),
174
+ })
175
+
176
+ // The knob cannot hang a run on its own: the iteration cap is still
177
+ // the backstop, and it settles rather than spinning.
178
+ expect(['completed', 'failed']).toContain(run.status)
179
+ })
180
+ })
@@ -0,0 +1,262 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import { MockLLMProvider } from '../../../provider/mock.js'
7
+ import { ToolRegistry } from '../../../registry/tool/execute.js'
8
+ import type { CheckpointId, IterationCheckpoint } from '../../../types/hitl/index.js'
9
+ import type { RunId, SessionId, TenantId } from '../../../types/ids/index.js'
10
+ import { createUserMessage } from '../../../types/message/index.js'
11
+ import type { CheckpointRunScope, CheckpointStore } from '../../../types/run/checkpoint-store.js'
12
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
13
+ import { resumeRun } from '../resume-run.js'
14
+ import type { RunStateScope } from '../run-state.js'
15
+
16
+ /**
17
+ * The pieces of a cross-process resume all existed and nothing joined them.
18
+ * `CheckpointManager` wrote the history, budgets, working state and any
19
+ * park; `loadRunState` read them back; `query` accepted `runId` +
20
+ * `resumeFromCheckpoint` and restored all of it. But `resumeFromCheckpoint`
21
+ * had no caller anywhere outside `packages/sdk/src`, so the whole path
22
+ * shipped untravelled — every host was expected to write the same wiring
23
+ * and none did.
24
+ *
25
+ * These cover the join, and especially its two refusals: a resume must not
26
+ * quietly become a fresh run under a recycled id, and it must not step past
27
+ * a park without the answer that park is waiting for.
28
+ */
29
+
30
+ const SCOPE: RunStateScope = {
31
+ tenantId: 'tnt_resume' as TenantId,
32
+ projectId: 'prj_resume' as ProjectId,
33
+ sessionId: 'ses_resume' as SessionId,
34
+ runId: 'run_resume' as RunId,
35
+ threadId: 'thd_resume' as ThreadId,
36
+ }
37
+
38
+ const ZERO_USAGE = {
39
+ promptTokens: 0,
40
+ completionTokens: 0,
41
+ totalTokens: 0,
42
+ cachedTokens: 0,
43
+ cacheWriteTokens: 0,
44
+ }
45
+
46
+ const ZERO_COST = {
47
+ inputCostPer1M: 0,
48
+ outputCostPer1M: 0,
49
+ totalCost: 0,
50
+ cacheDiscount: 0,
51
+ }
52
+
53
+ class InMemoryCheckpointStore implements CheckpointStore {
54
+ readonly rows = new Map<string, IterationCheckpoint>()
55
+
56
+ private key(scope: CheckpointRunScope, id: CheckpointId): string {
57
+ return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId, id].join('/')
58
+ }
59
+
60
+ async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
61
+ this.rows.set(this.key(scope, checkpoint.id), checkpoint)
62
+ }
63
+
64
+ async readCheckpoint(
65
+ scope: CheckpointRunScope,
66
+ id: CheckpointId,
67
+ ): Promise<IterationCheckpoint | null> {
68
+ return this.rows.get(this.key(scope, id)) ?? null
69
+ }
70
+
71
+ async listCheckpoints(scope: CheckpointRunScope): Promise<IterationCheckpoint[]> {
72
+ const prefix = `${[scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/')}/`
73
+ return [...this.rows.entries()]
74
+ .filter(([key]) => key.startsWith(prefix))
75
+ .map(([, cp]) => cp)
76
+ .sort((a, b) => a.createdAt - b.createdAt)
77
+ }
78
+
79
+ async deleteCheckpoint(scope: CheckpointRunScope, id: CheckpointId): Promise<void> {
80
+ this.rows.delete(this.key(scope, id))
81
+ }
82
+ }
83
+
84
+ function checkpoint(overrides: Partial<IterationCheckpoint> = {}): IterationCheckpoint {
85
+ return {
86
+ id: 'ckpt_1' as CheckpointId,
87
+ runId: SCOPE.runId,
88
+ iteration: 2,
89
+ messages: [createUserMessage('the work so far')],
90
+ tokenUsage: { ...ZERO_USAGE, promptTokens: 120, totalTokens: 120 },
91
+ costInfo: { ...ZERO_COST, totalCost: 0.4 },
92
+ guardState: { iterationCount: 2, elapsedMs: 9_000 },
93
+ createdAt: Date.now(),
94
+ ...overrides,
95
+ } as IterationCheckpoint
96
+ }
97
+
98
+ let workdirs: string[] = []
99
+
100
+ afterEach(async () => {
101
+ await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
102
+ workdirs = []
103
+ })
104
+
105
+ async function mkWorkdir(): Promise<string> {
106
+ const dir = await mkdtemp(join(tmpdir(), 'namzu-resume-run-'))
107
+ workdirs.push(dir)
108
+ return dir
109
+ }
110
+
111
+ async function baseParams(store: CheckpointStore) {
112
+ return {
113
+ scope: SCOPE,
114
+ checkpointStore: store,
115
+ provider: new MockLLMProvider({ turns: [{ text: 'continued' }] }),
116
+ tools: new ToolRegistry(),
117
+ runConfig: {
118
+ model: 'mock-model',
119
+ timeoutMs: 30_000,
120
+ tokenBudget: 100_000,
121
+ maxIterations: 2,
122
+ maxResponseTokens: 256,
123
+ },
124
+ agentId: 'agent_resume',
125
+ agentName: 'Resume Agent',
126
+ workingDirectory: await mkWorkdir(),
127
+ sessionId: SCOPE.sessionId,
128
+ threadId: SCOPE.threadId,
129
+ projectId: SCOPE.projectId,
130
+ tenantId: SCOPE.tenantId,
131
+ // Required by the contract, and rightly so: a resume that lands on a
132
+ // park has to have somewhere to ask.
133
+ resumeHandler: async () => ({ action: 'continue' as const }),
134
+ }
135
+ }
136
+
137
+ describe('a run is picked back up from its store', () => {
138
+ it('continues the same run id rather than starting a new one', async () => {
139
+ const store = new InMemoryCheckpointStore()
140
+ await store.writeCheckpoint(SCOPE, checkpoint())
141
+
142
+ const outcome = await resumeRun(await baseParams(store))
143
+
144
+ expect(outcome.resumed).toBe(true)
145
+ if (!outcome.resumed) return
146
+ // The whole point: a resume is the same run in a different process,
147
+ // so its id, budgets and trace all have to carry across.
148
+ expect(outcome.run.id).toBe(SCOPE.runId)
149
+ expect(outcome.state.checkpointId).toBe('ckpt_1')
150
+ })
151
+
152
+ it('carries the spent budget forward instead of granting a fresh one', async () => {
153
+ const store = new InMemoryCheckpointStore()
154
+ await store.writeCheckpoint(SCOPE, checkpoint())
155
+
156
+ const outcome = await resumeRun(await baseParams(store))
157
+
158
+ expect(outcome.resumed).toBe(true)
159
+ if (!outcome.resumed) return
160
+ // A run recalled at 120 tokens must not come back at zero — the
161
+ // budget belongs to the run, not to the process hosting it.
162
+ expect(outcome.run.tokenUsage.totalTokens).toBeGreaterThanOrEqual(120)
163
+ })
164
+
165
+ it('picks the newest checkpoint when the caller names none', async () => {
166
+ const store = new InMemoryCheckpointStore()
167
+ await store.writeCheckpoint(SCOPE, checkpoint({ id: 'ckpt_old' as CheckpointId, createdAt: 1 }))
168
+ await store.writeCheckpoint(
169
+ SCOPE,
170
+ checkpoint({ id: 'ckpt_new' as CheckpointId, createdAt: 2_000 }),
171
+ )
172
+
173
+ const outcome = await resumeRun(await baseParams(store))
174
+
175
+ expect(outcome.resumed).toBe(true)
176
+ if (!outcome.resumed) return
177
+ expect(outcome.state.checkpointId).toBe('ckpt_new')
178
+ })
179
+
180
+ it('honours an explicitly named checkpoint', async () => {
181
+ const store = new InMemoryCheckpointStore()
182
+ await store.writeCheckpoint(SCOPE, checkpoint({ id: 'ckpt_old' as CheckpointId, createdAt: 1 }))
183
+ await store.writeCheckpoint(
184
+ SCOPE,
185
+ checkpoint({ id: 'ckpt_new' as CheckpointId, createdAt: 2_000 }),
186
+ )
187
+
188
+ const outcome = await resumeRun({
189
+ ...(await baseParams(store)),
190
+ checkpointId: 'ckpt_old' as CheckpointId,
191
+ })
192
+
193
+ expect(outcome.resumed).toBe(true)
194
+ if (!outcome.resumed) return
195
+ expect(outcome.state.checkpointId).toBe('ckpt_old')
196
+ })
197
+ })
198
+
199
+ describe('it refuses rather than guessing', () => {
200
+ it('reports no checkpoint instead of silently starting fresh', async () => {
201
+ const outcome = await resumeRun(await baseParams(new InMemoryCheckpointStore()))
202
+
203
+ // Starting a new run here would be the worst outcome: a different
204
+ // run wearing a recycled id, with the original's budget reset.
205
+ expect(outcome).toEqual({ resumed: false, reason: 'no-checkpoint' })
206
+ })
207
+
208
+ it('hands back the outstanding question instead of resuming past it', async () => {
209
+ const store = new InMemoryCheckpointStore()
210
+ await store.writeCheckpoint(
211
+ SCOPE,
212
+ checkpoint({
213
+ pending: {
214
+ request: {
215
+ type: 'tool_review',
216
+ runId: SCOPE.runId,
217
+ checkpointId: 'ckpt_1' as CheckpointId,
218
+ toolCalls: [{ id: 'call_1', name: 'write', input: {} }],
219
+ },
220
+ parkedAt: Date.now(),
221
+ deadlineAt: Date.now() + 60_000,
222
+ },
223
+ } as unknown as Partial<IterationCheckpoint>),
224
+ )
225
+
226
+ const outcome = await resumeRun(await baseParams(store))
227
+
228
+ expect(outcome.resumed).toBe(false)
229
+ if (outcome.resumed || outcome.reason !== 'awaiting-decision') {
230
+ throw new Error(`expected awaiting-decision, got ${JSON.stringify(outcome)}`)
231
+ }
232
+ // The host needs the request itself to put in front of a person.
233
+ expect(outcome.pending.request.type).toBe('tool_review')
234
+ expect(outcome.state.runId).toBe(SCOPE.runId)
235
+ })
236
+
237
+ it('treats an already-answered park as an ordinary resume', async () => {
238
+ const store = new InMemoryCheckpointStore()
239
+ await store.writeCheckpoint(
240
+ SCOPE,
241
+ checkpoint({
242
+ pending: {
243
+ request: {
244
+ type: 'tool_review',
245
+ runId: SCOPE.runId,
246
+ checkpointId: 'ckpt_1' as CheckpointId,
247
+ toolCalls: [{ id: 'call_1', name: 'write', input: {} }],
248
+ },
249
+ parkedAt: Date.now(),
250
+ deadlineAt: Date.now() + 60_000,
251
+ resolvedAt: Date.now(),
252
+ },
253
+ } as unknown as Partial<IterationCheckpoint>),
254
+ )
255
+
256
+ const outcome = await resumeRun(await baseParams(store))
257
+
258
+ // `resolvedAt` is what makes a park answered. Blocking on one that
259
+ // already has its answer would strand the run permanently.
260
+ expect(outcome.resumed).toBe(true)
261
+ })
262
+ })
@@ -7,6 +7,7 @@ import { ProbeVetoError } from '../../probe/errors.js'
7
7
  import { type ProbeRegistry, probe as defaultProbeRegistry } from '../../probe/registry.js'
8
8
  import { renderToolSchema } from '../../registry/tool/schema.js'
9
9
  import type { ActivityStore } from '../../store/activity/memory.js'
10
+ import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
10
11
  import type { RunId, ToolUseId } from '../../types/ids/index.js'
11
12
  import type { InvocationState } from '../../types/invocation/index.js'
12
13
  import {
@@ -205,11 +206,18 @@ export class ToolExecutor {
205
206
  private probes: ProbeRegistry
206
207
  private parentSpan?: Span
207
208
  private readonly readPaths: Set<string> = new Set()
209
+ private readonly readFingerprints: Map<string, string> = new Map()
208
210
  private readonly fileReadTracker: FileReadTracker = {
209
- recordRead: (key: string) => {
211
+ recordRead: (key: string, content?: string) => {
210
212
  this.readPaths.add(key)
213
+ // Only when the reader had the body. A tool that records a read
214
+ // without one leaves the previous fingerprint alone rather than
215
+ // clearing it, so a later write is still checked against the last
216
+ // body anyone actually saw.
217
+ if (content !== undefined) this.readFingerprints.set(key, fingerprintContent(content))
211
218
  },
212
219
  hasRead: (key: string) => this.readPaths.has(key),
220
+ fingerprint: (key: string) => this.readFingerprints.get(key),
213
221
  }
214
222
 
215
223
  constructor(
@@ -9,6 +9,8 @@ import {
9
9
  import { findDanglingMessages, removeDanglingMessages } from '../../compaction/dangling.js'
10
10
  import { extractFromUserMessage } from '../../compaction/extractor.js'
11
11
  import { WorkingStateManager } from '../../compaction/manager.js'
12
+ import type { ContextReducer } from '../../compaction/reducer.js'
13
+ import { serializeState as serializeWorkingState } from '../../compaction/serializer.js'
12
14
  import { restoreWorkingState, snapshotWorkingState } from '../../compaction/wire.js'
13
15
  import type { CompactionConfig } from '../../config/runtime.js'
14
16
  import { TOOL_OUTPUT_DIR_NAME } from '../../constants/tools/index.js'
@@ -421,6 +423,16 @@ export interface QueryParams {
421
423
  */
422
424
  workingMemoryProvider?: WorkingMemoryProvider
423
425
 
426
+ /**
427
+ * Replace context reduction for this run.
428
+ *
429
+ * Outranks `compactionConfig.strategy`, and the built-in structured pass
430
+ * does not also run: two mechanisms editing one history in the same pass
431
+ * cannot both be reasoned about. See `ContextReducer` for the invariants a
432
+ * reducer is expected to keep.
433
+ */
434
+ contextReducer?: ContextReducer
435
+
424
436
  agentBus?: import('../../bus/index.js').AgentBus
425
437
 
426
438
  verificationGate?: VerificationGateConfig
@@ -752,6 +764,31 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
752
764
  params.advisory.budget,
753
765
  )
754
766
 
767
+ // What the run looks like when the MODEL consults an advisor, as
768
+ // opposed to when a trigger does. The trigger path has always passed
769
+ // this; the tool path passed an empty context, so an advisor the model
770
+ // asked for help saw the question and nothing else.
771
+ //
772
+ // `includeToolCatalog` and `useCompactedContext` are read here and
773
+ // nowhere else. Both were declared on `AdvisoryConfig` /
774
+ // `AdvisorDefinition` and consulted by nothing, so a host who turned
775
+ // the catalogue off still paid for it in every advisory prompt.
776
+ const advisoryConfig = params.advisory
777
+ advisoryCtx.setCallContextProvider(() => {
778
+ const summary =
779
+ workingStateManager && advisoryConfig.advisors.some((a) => a.useCompactedContext)
780
+ ? serializeWorkingState(workingStateManager.getState())
781
+ : undefined
782
+ return {
783
+ messages: ctx.runMgr.messages,
784
+ ...(summary !== undefined ? { workingStateSummary: summary } : {}),
785
+ ...(advisoryConfig.includeToolCatalog
786
+ ? { toolCatalog: params.tools.toLLMTools(effectiveAllowedTools) }
787
+ : {}),
788
+ iteration: ctx.runMgr.currentIteration,
789
+ }
790
+ })
791
+
755
792
  if (params.advisory.enableAgentTool) {
756
793
  const advisoryTools = buildAdvisoryTools({ advisoryCtx })
757
794
  const overrides = params.runtimeToolOverrides
@@ -800,6 +837,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
800
837
  toolGrants: new ToolGrantSet(),
801
838
  compactionConfig: params.compactionConfig,
802
839
  workingStateManager,
840
+ taskRouter: params.taskRouter,
841
+ contextReducer: params.contextReducer,
803
842
  workingMemoryProvider: params.workingMemoryProvider,
804
843
  advisoryCtx,
805
844
  agentBus: params.agentBus,