@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,153 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ /**
5
+ * A span that never ends is a trace that never closes, and the export is
6
+ * incomplete for exactly the run that failed.
7
+ *
8
+ * Both sites had the same shape: an `end()` call at every exit the author
9
+ * could see. The iteration loop had seventeen of them and the tool executor
10
+ * three plus a `finally` that opened below them. That is a rule every future
11
+ * edit has to remember, and it was already broken — anything throwing between
12
+ * the span's creation and the `try` left it open, and a generator abandoned
13
+ * by its consumer reached no exit at all.
14
+ */
15
+
16
+ const ended: string[] = []
17
+ const started: string[] = []
18
+
19
+ function fakeSpan(name: string) {
20
+ return {
21
+ setAttributes: () => undefined,
22
+ setAttribute: () => undefined,
23
+ setStatus: () => undefined,
24
+ recordException: () => undefined,
25
+ addEvent: () => undefined,
26
+ end: () => ended.push(name),
27
+ spanContext: () => ({ traceId: 't', spanId: 's', traceFlags: 1 }),
28
+ isRecording: () => true,
29
+ updateName: () => undefined,
30
+ }
31
+ }
32
+
33
+ vi.mock('../runtime-accessors.js', () => ({
34
+ getTracer: () => ({
35
+ startSpan: (name: string) => {
36
+ started.push(name)
37
+ return fakeSpan(name)
38
+ },
39
+ startActiveSpan: (
40
+ name: string,
41
+ _opts: unknown,
42
+ _ctx: unknown,
43
+ fn: (span: ReturnType<typeof fakeSpan>) => unknown,
44
+ ) => {
45
+ started.push(name)
46
+ return fn(fakeSpan(name))
47
+ },
48
+ }),
49
+ getMeter: () => ({
50
+ createCounter: () => ({ add: () => undefined }),
51
+ createHistogram: () => ({ record: () => undefined }),
52
+ }),
53
+ }))
54
+
55
+ function toolContext() {
56
+ return {
57
+ runId: 'run_span',
58
+ workingDirectory: '.',
59
+ abortSignal: new AbortController().signal,
60
+ env: {},
61
+ log: () => undefined,
62
+ } as never
63
+ }
64
+
65
+ beforeEach(() => {
66
+ started.length = 0
67
+ ended.length = 0
68
+ })
69
+
70
+ afterEach(() => {
71
+ vi.clearAllMocks()
72
+ })
73
+
74
+ describe('a tool span closes however the call leaves', () => {
75
+ it('closes on the ordinary path', async () => {
76
+ const { ToolRegistry } = await import('../../registry/tool/execute.js')
77
+ const tools = new ToolRegistry()
78
+ tools.register({
79
+ name: 'echo',
80
+ description: 'echo',
81
+ inputSchema: z.object({}),
82
+ execute: async () => ({ success: true, output: 'ok' }),
83
+ })
84
+
85
+ await tools.execute('echo', {}, toolContext())
86
+
87
+ expect(started).toHaveLength(1)
88
+ expect(ended).toEqual(started)
89
+ })
90
+
91
+ it('closes when the tool throws', async () => {
92
+ const { ToolRegistry } = await import('../../registry/tool/execute.js')
93
+ const tools = new ToolRegistry()
94
+ tools.register({
95
+ name: 'boom',
96
+ description: 'boom',
97
+ inputSchema: z.object({}),
98
+ execute: async () => {
99
+ throw new Error('kaboom')
100
+ },
101
+ })
102
+
103
+ await tools.execute('boom', {}, toolContext())
104
+
105
+ expect(ended).toEqual(started)
106
+ })
107
+
108
+ it('closes when input validation refuses the call', async () => {
109
+ const { ToolRegistry } = await import('../../registry/tool/execute.js')
110
+ const tools = new ToolRegistry()
111
+ tools.register({
112
+ name: 'strict',
113
+ description: 'strict',
114
+ inputSchema: z.object({ required: z.string() }),
115
+ execute: async () => ({ success: true, output: 'ok' }),
116
+ })
117
+
118
+ await tools.execute('strict', { wrong: 1 }, toolContext())
119
+
120
+ expect(ended).toEqual(started)
121
+ })
122
+
123
+ it('closes when the tool is not active', async () => {
124
+ const { ToolRegistry } = await import('../../registry/tool/execute.js')
125
+ const tools = new ToolRegistry()
126
+ tools.register(
127
+ {
128
+ name: 'later',
129
+ description: 'later',
130
+ inputSchema: z.object({}),
131
+ execute: async () => ({ success: true, output: 'ok' }),
132
+ },
133
+ 'deferred',
134
+ )
135
+
136
+ await tools.execute('later', {}, toolContext())
137
+
138
+ expect(ended).toEqual(started)
139
+ })
140
+
141
+ it('closes when the registry does not hold the name at all', async () => {
142
+ const { ToolRegistry } = await import('../../registry/tool/execute.js')
143
+ const tools = new ToolRegistry()
144
+
145
+ // `getOrThrow` sat OUTSIDE the try that owned the finally, so this
146
+ // path — the one where the model invents a tool name — opened a span
147
+ // and never closed it.
148
+ await expect(tools.execute('nonexistent', {}, toolContext())).rejects.toThrow()
149
+
150
+ expect(started).toHaveLength(1)
151
+ expect(ended).toEqual(started)
152
+ })
153
+ })
@@ -104,7 +104,7 @@ export function buildAdvisoryTools(opts: AdvisoryToolsOptions): ToolDefinition[]
104
104
  const executionResult = await advisoryCtx.executor.consult(
105
105
  advisor,
106
106
  { advisorId: advisor.id, question, domain, urgency, includeContext: include_context },
107
- { messages: [], iteration: 0 },
107
+ advisoryCtx.callContext(),
108
108
  )
109
109
 
110
110
  advisoryCtx.recordCall({
@@ -0,0 +1,103 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+
6
+ import type { ToolContext } from '../../../types/tool/index.js'
7
+
8
+ /**
9
+ * That the atomic writer works is tested next door. This tests the thing
10
+ * that actually regressed: whether the TOOL goes through it.
11
+ *
12
+ * `edit` and `write` were reverted to a bare `writeFile`, and the existing
13
+ * atomicity test kept passing because it called `atomicWriteFile` directly —
14
+ * proving the module, never the caller. A write that fails partway then
15
+ * truncates the user's file, which is the one outcome `edit` exists to
16
+ * avoid.
17
+ */
18
+
19
+ const atomicWriteFile = vi.fn(async () => {})
20
+ vi.mock('../atomic-write-file.js', () => ({ atomicWriteFile }))
21
+
22
+ function makeContext(workingDirectory: string): ToolContext {
23
+ return {
24
+ runId: 'run_atomic' as ToolContext['runId'],
25
+ workingDirectory,
26
+ abortSignal: new AbortController().signal,
27
+ env: {},
28
+ log: () => {},
29
+ }
30
+ }
31
+
32
+ describe('the file-mutating tools commit through the atomic writer', () => {
33
+ it('edit routes its local write through it', async () => {
34
+ const { EditTool } = await import('../edit.js')
35
+ atomicWriteFile.mockClear()
36
+
37
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
38
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
39
+
40
+ const result = await EditTool.execute(
41
+ { path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
42
+ makeContext(dir),
43
+ )
44
+
45
+ expect(result.success).toBe(true)
46
+ expect(atomicWriteFile).toHaveBeenCalledTimes(1)
47
+ expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'doc.md'), 'alpha\ngamma\n')
48
+ // The mock swallowed the write, so the file is untouched — which is
49
+ // itself the proof that nothing else wrote it.
50
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nbeta\n')
51
+ })
52
+
53
+ it('write routes its local write through it', async () => {
54
+ const { WriteFileTool } = await import('../write-file.js')
55
+ atomicWriteFile.mockClear()
56
+
57
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
58
+
59
+ const result = await WriteFileTool.execute(
60
+ { path: 'out.md', content: 'a complete body' },
61
+ makeContext(dir),
62
+ )
63
+
64
+ expect(result.success).toBe(true)
65
+ expect(atomicWriteFile).toHaveBeenCalledTimes(1)
66
+ expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'out.md'), 'a complete body')
67
+ })
68
+
69
+ it('edit does not use it for a sandboxed write, which the sandbox owns', async () => {
70
+ const { EditTool } = await import('../edit.js')
71
+ atomicWriteFile.mockClear()
72
+
73
+ let body = 'alpha\nbeta\n'
74
+ const sandbox = {
75
+ id: 'sbx',
76
+ status: 'ready',
77
+ rootDir: '/workspace',
78
+ environment: 'basic',
79
+ async readFile() {
80
+ return Buffer.from(body)
81
+ },
82
+ async writeFile(_path: string, content: string | Buffer) {
83
+ body = content.toString()
84
+ },
85
+ async exec() {
86
+ throw new Error('not used')
87
+ },
88
+ async listFiles() {
89
+ return []
90
+ },
91
+ async destroy() {},
92
+ }
93
+
94
+ const result = await EditTool.execute(
95
+ { path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
96
+ { ...makeContext('/workspace'), sandbox } as ToolContext,
97
+ )
98
+
99
+ expect(result.success).toBe(true)
100
+ expect(body).toBe('alpha\ngamma\n')
101
+ expect(atomicWriteFile).not.toHaveBeenCalled()
102
+ })
103
+ })
@@ -1,8 +1,11 @@
1
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { describe, expect, it } from 'vitest'
5
+ import { ToolRegistry } from '../../../registry/tool/execute.js'
6
+ import type { Sandbox } from '../../../types/sandbox/index.js'
5
7
  import type { ToolContext } from '../../../types/tool/index.js'
8
+ import { atomicWriteFile } from '../atomic-write-file.js'
6
9
  import { EditTool } from '../edit.js'
7
10
 
8
11
  function makeContext(workingDirectory: string): ToolContext {
@@ -16,6 +19,326 @@ function makeContext(workingDirectory: string): ToolContext {
16
19
  }
17
20
 
18
21
  describe('EditTool', () => {
22
+ it('publishes one closed canonical replacement contract', () => {
23
+ const schema = EditTool.modelInputSchema
24
+ expect(schema).toEqual({
25
+ type: 'object',
26
+ properties: {
27
+ path: {
28
+ type: 'string',
29
+ description: 'Path to the file to edit. Must not be empty.',
30
+ },
31
+ old_string: {
32
+ type: 'string',
33
+ description:
34
+ 'Exact unique text from the file, without read-tool line-number prefixes. Must not be empty.',
35
+ },
36
+ new_string: {
37
+ type: 'string',
38
+ description:
39
+ 'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
40
+ },
41
+ replace_all: {
42
+ type: 'boolean',
43
+ description: 'Replace every occurrence instead of requiring one unique match.',
44
+ },
45
+ },
46
+ required: ['path', 'old_string', 'new_string'],
47
+ additionalProperties: false,
48
+ })
49
+
50
+ expect(
51
+ EditTool.inputSchema.safeParse({
52
+ path: 'doc.md',
53
+ old_string: 'old',
54
+ new_string: 'new',
55
+ replace_all: true,
56
+ }).success,
57
+ ).toBe(true)
58
+
59
+ // The MODEL schema above is the closed canonical one. The HOST schema
60
+ // is wider on purpose — aliases and line insertion are declared — and
61
+ // the point of `.strict()` is that wider still is not open.
62
+ for (const supported of [
63
+ { path: 'doc.md', oldStr: 'old', newStr: 'new' },
64
+ { path: 'doc.md', insertLine: 'end', new_string: 'append' },
65
+ { path: 'doc.md', old_string: 'old', new_string: 'new', newStr: 'legacy' },
66
+ ]) {
67
+ expect(EditTool.inputSchema.safeParse(supported).success, JSON.stringify(supported)).toBe(
68
+ true,
69
+ )
70
+ }
71
+
72
+ for (const unknown of [
73
+ { path: 'doc.md', old_str: 'old', new_string: 'new' },
74
+ { path: 'doc.md', old_string: 'old', new_string: 'new', replaceAll: true },
75
+ ]) {
76
+ expect(EditTool.inputSchema.safeParse(unknown).success, JSON.stringify(unknown)).toBe(false)
77
+ }
78
+ expect(
79
+ EditTool.inputSchema.safeParse({
80
+ path: ' ',
81
+ old_string: 'old',
82
+ new_string: 'new',
83
+ }).success,
84
+ ).toBe(false)
85
+ })
86
+
87
+ it('replaces one exact unique string', async () => {
88
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
89
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
90
+
91
+ const result = await EditTool.execute(
92
+ {
93
+ path: 'doc.md',
94
+ old_string: 'beta',
95
+ new_string: 'gamma',
96
+ replace_all: false,
97
+ },
98
+ makeContext(dir),
99
+ )
100
+
101
+ expect(result.success).toBe(true)
102
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\ngamma\n')
103
+ })
104
+
105
+ it('validates a path without rewriting edge whitespace', async () => {
106
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
107
+ writeFileSync(join(dir, ' doc.md '), 'alpha\n')
108
+
109
+ const result = await EditTool.execute(
110
+ {
111
+ path: ' doc.md ',
112
+ old_string: 'alpha',
113
+ new_string: 'beta',
114
+ replace_all: false,
115
+ },
116
+ makeContext(dir),
117
+ )
118
+
119
+ expect(result.success).toBe(true)
120
+ expect(readFileSync(join(dir, ' doc.md '), 'utf-8')).toBe('beta\n')
121
+ expect(existsSync(join(dir, 'doc.md'))).toBe(false)
122
+ })
123
+
124
+ it('allows deletion with an empty new_string', async () => {
125
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
126
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nremove me\nomega\n')
127
+
128
+ const result = await EditTool.execute(
129
+ {
130
+ path: 'doc.md',
131
+ old_string: 'remove me\n',
132
+ new_string: '',
133
+ replace_all: false,
134
+ },
135
+ makeContext(dir),
136
+ )
137
+
138
+ expect(result.success).toBe(true)
139
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nomega\n')
140
+ })
141
+
142
+ it('replaces every exact occurrence only when replace_all is true', async () => {
143
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
144
+ writeFileSync(join(dir, 'doc.md'), 'alpha alpha alpha')
145
+
146
+ const result = await EditTool.execute(
147
+ {
148
+ path: 'doc.md',
149
+ old_string: 'alpha',
150
+ new_string: 'beta',
151
+ replace_all: true,
152
+ },
153
+ makeContext(dir),
154
+ )
155
+
156
+ expect(result.success).toBe(true)
157
+ expect(result.data).toMatchObject({ replacements: 3 })
158
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta beta beta')
159
+ })
160
+
161
+ it('refuses ambiguous exact matches instead of guessing', async () => {
162
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
163
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nalpha\n')
164
+
165
+ const result = await EditTool.execute(
166
+ {
167
+ path: 'doc.md',
168
+ old_string: 'alpha',
169
+ new_string: 'beta',
170
+ replace_all: false,
171
+ },
172
+ makeContext(dir),
173
+ )
174
+
175
+ expect(result.success).toBe(false)
176
+ expect(result.error).toContain('is not unique')
177
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nalpha\n')
178
+ })
179
+
180
+ it('normalizes consistent CRLF/LF boundaries without fuzzy matching', async () => {
181
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
182
+ writeFileSync(join(dir, 'doc.md'), 'alpha\r\nbeta\r\nomega\r\n')
183
+
184
+ const result = await EditTool.execute(
185
+ {
186
+ path: 'doc.md',
187
+ old_string: 'alpha\nbeta',
188
+ new_string: 'gamma\ndelta',
189
+ replace_all: false,
190
+ },
191
+ makeContext(dir),
192
+ )
193
+
194
+ expect(result.success).toBe(true)
195
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('gamma\r\ndelta\r\nomega\r\n')
196
+ })
197
+
198
+ it('rejects a field it does not accept, even when execute is called directly', async () => {
199
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
200
+ writeFileSync(join(dir, 'doc.md'), 'alpha\n')
201
+
202
+ // Without `.strict()` zod STRIPS an unrecognised key, so each of these
203
+ // would run as if the caller had never written it — a misspelling
204
+ // becomes a silent no-op rather than an error anyone can act on.
205
+ for (const input of [
206
+ { path: 'doc.md', old_str: 'alpha', new_string: 'beta' },
207
+ { path: 'doc.md', old_string: 'alpha', new_string: 'beta', overwrite: true },
208
+ { path: 'doc.md', old_string: 'alpha', new_string: 'beta', replaceAll: true },
209
+ ]) {
210
+ const result = await EditTool.execute(input as never, makeContext(dir))
211
+ expect(result.success, JSON.stringify(input)).toBe(false)
212
+ expect(result.error).toContain('Invalid edit input')
213
+ }
214
+
215
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\n')
216
+ })
217
+
218
+ it('accepts the host aliases the schema does declare', async () => {
219
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
220
+ writeFileSync(join(dir, 'doc.md'), 'alpha\n')
221
+
222
+ // The contract is closed, not narrow. `oldStr`/`newStr` and
223
+ // `insertLine` are in the schema for hosts that speak those names, so
224
+ // strictness must reject the unknown without rejecting these.
225
+ const result = await EditTool.execute(
226
+ { path: 'doc.md', oldStr: 'alpha', newStr: 'beta' } as never,
227
+ makeContext(dir),
228
+ )
229
+
230
+ expect(result.success).toBe(true)
231
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta\n')
232
+ })
233
+
234
+ it('returns one canonical recovery shape after registry validation fails', async () => {
235
+ const registry = new ToolRegistry()
236
+ registry.register(EditTool)
237
+
238
+ const result = await registry.execute(
239
+ 'edit',
240
+ { insertLine: '"end"', newStr: 'content' },
241
+ makeContext('/tmp'),
242
+ )
243
+
244
+ expect(result.success).toBe(false)
245
+ expect(result.error).toContain('Validation failed for "edit":')
246
+ expect(result.error).toContain('Required: path: string')
247
+ expect(result.error).toContain(
248
+ '{"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}',
249
+ )
250
+ expect(result.error).not.toContain('Accepted shapes')
251
+ })
252
+
253
+ it('serializes independent EditTool read-modify-write cycles for one sandbox path', async () => {
254
+ let body = Buffer.from('alpha')
255
+ let reads = 0
256
+ let writes = 0
257
+ let releaseFirstWrite = () => {}
258
+ let markFirstWriteStarted = () => {}
259
+ const firstWriteStarted = new Promise<void>((resolve) => {
260
+ markFirstWriteStarted = resolve
261
+ })
262
+ const firstWriteGate = new Promise<void>((resolve) => {
263
+ releaseFirstWrite = resolve
264
+ })
265
+ const sandbox = {
266
+ id: 'sbx_edit_lock',
267
+ status: 'ready',
268
+ rootDir: '/workspace',
269
+ environment: 'basic',
270
+ async readFile() {
271
+ reads += 1
272
+ return Buffer.from(body)
273
+ },
274
+ async writeFile(_path: string, content: string | Buffer) {
275
+ writes += 1
276
+ if (writes === 1) {
277
+ markFirstWriteStarted()
278
+ await firstWriteGate
279
+ }
280
+ body = Buffer.isBuffer(content) ? Buffer.from(content) : Buffer.from(content)
281
+ },
282
+ async exec() {
283
+ throw new Error('not used')
284
+ },
285
+ async listFiles() {
286
+ return []
287
+ },
288
+ async destroy() {},
289
+ } as Sandbox
290
+ const firstContext = { ...makeContext('/workspace'), sandbox }
291
+ const secondContext = { ...makeContext('/workspace'), sandbox }
292
+
293
+ const first = EditTool.execute(
294
+ {
295
+ path: 'doc.md',
296
+ old_string: 'alpha',
297
+ new_string: 'beta',
298
+ replace_all: false,
299
+ },
300
+ firstContext,
301
+ )
302
+ await firstWriteStarted
303
+ const second = EditTool.execute(
304
+ {
305
+ path: 'doc.md',
306
+ old_string: 'beta',
307
+ new_string: 'gamma',
308
+ replace_all: false,
309
+ },
310
+ secondContext,
311
+ )
312
+ await Promise.resolve()
313
+ expect(reads).toBe(1)
314
+
315
+ releaseFirstWrite()
316
+ const results = await Promise.all([first, second])
317
+ expect(results.every((result) => result.success)).toBe(true)
318
+ expect(reads).toBe(2)
319
+ expect(writes).toBe(2)
320
+ expect(body.toString('utf-8')).toBe('gamma')
321
+ })
322
+
323
+ it('leaves the original intact when an atomic local write fails before commit', async () => {
324
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-write-'))
325
+ const filePath = join(dir, 'doc.md')
326
+ writeFileSync(filePath, 'original')
327
+
328
+ await expect(
329
+ atomicWriteFile(filePath, 'replacement', {
330
+ beforeCommit: async () => {
331
+ throw new Error('injected pre-commit failure')
332
+ },
333
+ }),
334
+ ).rejects.toThrow('injected pre-commit failure')
335
+
336
+ expect(readFileSync(filePath, 'utf-8')).toBe('original')
337
+ expect(readdirSync(dir).filter((entry) => entry.includes('.namzu-'))).toEqual([])
338
+ })
339
+
340
+ // ── the alias + insertion surface this build adds on top ──────────
341
+
19
342
  it('accepts oldStr/newStr aliases for string replacement', async () => {
20
343
  const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
21
344
  writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')