@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
@@ -134,6 +134,7 @@ export type {
134
134
  HybridExecutionContextOptions,
135
135
  LocalExecutionContextOptions,
136
136
  MCPServerResourceProvider,
137
+ MCPServerPromptProvider,
137
138
  MCPServerToolProvider,
138
139
  RemoteExecutionContextOptions,
139
140
  TenantConnectorManagerConfig,
@@ -157,6 +158,10 @@ export type { ToolCallContext } from './verification/index.js'
157
158
  export type { DiskSessionStoreConfig, LinkageView } from './store/session/index.js'
158
159
 
159
160
  export type {
161
+ ContextReducer,
162
+ ContextReduction,
163
+ ContextReductionReason,
164
+ SlidingWindowOptions,
160
165
  ConversationManager,
161
166
  CompactionStrategy,
162
167
  DanglingResult,
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { EmbeddingProvider, TenantScope } from '../../types/rag/index.js'
4
+ import { DefaultIngestionPipeline } from '../ingestion.js'
5
+ import { DefaultRetriever } from '../retriever.js'
6
+ import { InMemoryVectorStore } from '../vector-store.js'
7
+
8
+ /**
9
+ * `TenantScope.namespace` and `KnowledgeBaseConfig.namespace` were declared
10
+ * from the start and neither reached storage. Ingestion copied
11
+ * `scope.tenantId` onto every chunk and dropped the namespace; the store
12
+ * filtered on tenant alone. So a partition a host asked for did not exist,
13
+ * and every namespace in a tenant saw every other one's documents.
14
+ */
15
+
16
+ /** Deterministic and content-derived, so similarity is stable across runs. */
17
+ const embedder: EmbeddingProvider = {
18
+ async embed(texts: string[]) {
19
+ return texts.map((t) => [t.length % 7, t.charCodeAt(0) % 5, 1])
20
+ },
21
+ async embedQuery(text: string) {
22
+ return [text.length % 7, text.charCodeAt(0) % 5, 1]
23
+ },
24
+ dimensions: 3,
25
+ model: 'test-embedder',
26
+ id: 'test-embedder',
27
+ }
28
+
29
+ const tenant = 'tnt_one' as TenantScope['tenantId']
30
+
31
+ function scope(namespace?: string): TenantScope {
32
+ return { tenantId: tenant, ...(namespace !== undefined ? { namespace } : {}) }
33
+ }
34
+
35
+ async function seed() {
36
+ const store = new InMemoryVectorStore()
37
+ const ingestion = new DefaultIngestionPipeline(store, embedder)
38
+ const kb = 'kb_one' as never
39
+
40
+ await ingestion.ingest('alpha partition content', {}, scope('alpha'), kb)
41
+ await ingestion.ingest('beta partition content', {}, scope('beta'), kb)
42
+ await ingestion.ingest('default partition content', {}, scope(), kb)
43
+
44
+ return { store, retriever: new DefaultRetriever(store, embedder), kb }
45
+ }
46
+
47
+ const texts = (r: { chunks: { chunk: { content: string } }[] }) =>
48
+ r.chunks.map((c) => c.chunk.content)
49
+
50
+ describe('a namespace partitions what a query can see', () => {
51
+ it('returns only its own partition', async () => {
52
+ const { retriever, kb } = await seed()
53
+
54
+ const found = await retriever.retrieve({ text: 'partition content' }, scope('alpha'), kb)
55
+
56
+ expect(texts(found)).toEqual(['alpha partition content'])
57
+ })
58
+
59
+ it('does not leak the other partition', async () => {
60
+ const { retriever, kb } = await seed()
61
+
62
+ const found = await retriever.retrieve({ text: 'partition content' }, scope('beta'), kb)
63
+
64
+ expect(texts(found).join()).not.toContain('alpha')
65
+ })
66
+
67
+ it('treats an absent namespace as the default partition, not as no filter', async () => {
68
+ const { retriever, kb } = await seed()
69
+
70
+ const found = await retriever.retrieve({ text: 'partition content' }, scope(), kb)
71
+
72
+ // The whole point. Reading absence as "no filter" is how the boundary
73
+ // leaks: a caller who never asked for a namespace would see every
74
+ // namespaced chunk in the tenant.
75
+ expect(texts(found)).toEqual(['default partition content'])
76
+ })
77
+
78
+ it('keeps tenants apart as it always did', async () => {
79
+ const { retriever, kb } = await seed()
80
+
81
+ const found = await retriever.retrieve(
82
+ { text: 'partition content' },
83
+ {
84
+ tenantId: 'tnt_other' as TenantScope['tenantId'],
85
+ namespace: 'alpha',
86
+ },
87
+ kb,
88
+ )
89
+
90
+ expect(found.chunks).toEqual([])
91
+ })
92
+
93
+ it('stamps the namespace onto the chunk at ingest', async () => {
94
+ const { store } = await seed()
95
+
96
+ const results = await store.search({
97
+ embedding: await embedder.embedQuery('partition content'),
98
+ topK: 10,
99
+ tenantId: tenant,
100
+ namespace: 'alpha',
101
+ })
102
+
103
+ // Ingest is where it has to land — filtering at search against a value
104
+ // nothing writes returns zero rows, and "no results" reads as "nothing
105
+ // matched" rather than "this scope was never stored".
106
+ expect(results.every((r) => r.chunk.content.startsWith('alpha'))).toBe(true)
107
+ expect(results.length).toBeGreaterThan(0)
108
+ })
109
+ })
@@ -48,6 +48,7 @@ export class DefaultIngestionPipeline implements IngestionPipeline {
48
48
  documentId,
49
49
  knowledgeBaseId,
50
50
  tenantId: scope.tenantId,
51
+ ...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
51
52
  content: cc.content,
52
53
  index: cc.index,
53
54
  tokenCount: estimateTokens(cc.content),
@@ -76,6 +76,7 @@ export class DefaultRetriever implements Retriever {
76
76
  embedding,
77
77
  topK: config.topK,
78
78
  tenantId: scope.tenantId,
79
+ ...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
79
80
  knowledgeBaseId,
80
81
  minScore: config.minScore,
81
82
  })
@@ -92,6 +93,7 @@ export class DefaultRetriever implements Retriever {
92
93
  embedding,
93
94
  topK: config.topK * 2,
94
95
  tenantId: scope.tenantId,
96
+ ...(scope.namespace !== undefined ? { namespace: scope.namespace } : {}),
95
97
  knowledgeBaseId,
96
98
  minScore: 0,
97
99
  })
@@ -20,6 +20,11 @@ export class InMemoryVectorStore implements VectorStore {
20
20
 
21
21
  for (const chunk of this.chunks.values()) {
22
22
  if (chunk.tenantId !== query.tenantId) continue
23
+ // Equality INCLUDING absence: an omitted namespace is the default
24
+ // partition, not the absence of a filter. Treating it as no filter
25
+ // is how the boundary leaks — a caller who never asked for a
26
+ // namespace would see every namespaced chunk in the tenant.
27
+ if (chunk.namespace !== query.namespace) continue
23
28
  if (query.knowledgeBaseId && chunk.knowledgeBaseId !== query.knowledgeBaseId) continue
24
29
  if (!chunk.embedding) continue
25
30
 
@@ -375,151 +375,155 @@ Executable tool names, descriptions, and JSON input schemas are attached through
375
375
  : otelContext.active()
376
376
 
377
377
  return tracer.startActiveSpan(toolSpanName(toolName), {}, parentCtx, async (span) => {
378
- span.setAttributes({
379
- [GENAI.TOOL_NAME]: toolName,
380
- [GENAI.TOOL_TYPE]: 'function',
381
- })
382
-
383
- const tool = this.getOrThrow(toolName)
384
-
385
- const availability = this.getAvailability(toolName)
386
- if (availability !== 'active') {
387
- const msg = `Tool "${toolName}" is ${availability} and cannot be executed`
388
- this.log.warn(msg)
378
+ try {
389
379
  span.setAttributes({
390
- [NAMZU.TOOL_SUCCESS]: false,
391
- [NAMZU.TOOL_ERROR]: msg,
380
+ [GENAI.TOOL_NAME]: toolName,
381
+ [GENAI.TOOL_TYPE]: 'function',
392
382
  })
393
- span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
394
- span.end()
395
- return {
396
- success: false,
397
- output: '',
398
- error: msg,
399
- }
400
- }
401
383
 
402
- const mode = context.permissionContext?.mode ?? 'auto'
403
- if (mode === 'plan') {
404
- const isReadOnly = tool.isReadOnly ? tool.isReadOnly(rawInput) : false
405
- if (!isReadOnly) {
406
- const msg = `plan mode: non-read-only tool "${toolName}" blocked`
384
+ const tool = this.getOrThrow(toolName)
385
+
386
+ const availability = this.getAvailability(toolName)
387
+ if (availability !== 'active') {
388
+ const msg = `Tool "${toolName}" is ${availability} and cannot be executed`
389
+ this.log.warn(msg)
407
390
  span.setAttributes({
408
391
  [NAMZU.TOOL_SUCCESS]: false,
409
392
  [NAMZU.TOOL_ERROR]: msg,
410
393
  })
411
394
  span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
412
- span.end()
413
395
  return {
414
396
  success: false,
415
397
  output: '',
416
398
  error: msg,
417
- permissionDenied: true,
418
- permissionMessage: msg,
419
399
  }
420
400
  }
421
- }
422
401
 
423
- const parseResult = tool.inputSchema.safeParse(rawInput)
424
- if (!parseResult.success) {
425
- const errorMessage = parseResult.error.issues
426
- .map((i) => `${i.path.join('.')}: ${i.message}`)
427
- .join('; ')
428
-
429
- // Distinguish "model sent an empty/no-arg call" from
430
- // "model sent partial args" — the first is most often a
431
- // streaming hiccup or a definition-test ping (a provider
432
- // occasionally pings tool surfaces with `{}` while the
433
- // schema is still loading), the second is a genuine
434
- // programming mistake by the model. The model self-
435
- // corrects MUCH more reliably when the error tells it
436
- // (a) which fields are required, (b) their types, and
437
- // (c) a minimal example call. Without these hints the
438
- // downstream UI just shows a red "Failed" row and the
439
- // model rarely retries with the right args.
440
- const isEmptyInput =
441
- rawInput === null ||
442
- rawInput === undefined ||
443
- (typeof rawInput === 'object' &&
444
- !Array.isArray(rawInput) &&
445
- Object.keys(rawInput as Record<string, unknown>).length === 0)
446
-
447
- const requiredHint = describeRequiredInput(tool.inputSchema)
448
- // A conditional schema's required shape cannot be reconstructed
449
- // from JSON Schema's top-level `required`, so the author gets to
450
- // say what a valid retry looks like.
451
- const recoveryHint = tool.validationErrorHint?.trim()
452
- ? ` ${tool.validationErrorHint.trim()}`
453
- : ''
454
-
455
- const enrichedMessage = isEmptyInput
456
- ? `Tool "${toolName}" was called with no arguments. ${requiredHint}${recoveryHint} Retry the call with the required parameters populated.`
457
- : `Validation failed for "${toolName}": ${errorMessage}. ${requiredHint}${recoveryHint}`
458
-
459
- this.log.error(`Tool input validation failed: ${toolName}`, {
460
- errors: errorMessage,
461
- empty: isEmptyInput,
462
- })
402
+ const mode = context.permissionContext?.mode ?? 'auto'
403
+ if (mode === 'plan') {
404
+ const isReadOnly = tool.isReadOnly ? tool.isReadOnly(rawInput) : false
405
+ if (!isReadOnly) {
406
+ const msg = `plan mode: non-read-only tool "${toolName}" blocked`
407
+ span.setAttributes({
408
+ [NAMZU.TOOL_SUCCESS]: false,
409
+ [NAMZU.TOOL_ERROR]: msg,
410
+ })
411
+ span.setStatus({ code: SpanStatusCode.ERROR, message: msg })
412
+ return {
413
+ success: false,
414
+ output: '',
415
+ error: msg,
416
+ permissionDenied: true,
417
+ permissionMessage: msg,
418
+ }
419
+ }
420
+ }
463
421
 
464
- span.setAttributes({
465
- [NAMZU.TOOL_SUCCESS]: false,
466
- [NAMZU.TOOL_ERROR]: `Validation: ${errorMessage}`,
467
- })
468
- span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
469
- span.end()
422
+ const parseResult = tool.inputSchema.safeParse(rawInput)
423
+ if (!parseResult.success) {
424
+ const errorMessage = parseResult.error.issues
425
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
426
+ .join('; ')
427
+
428
+ // Distinguish "model sent an empty/no-arg call" from
429
+ // "model sent partial args" — the first is most often a
430
+ // streaming hiccup or a definition-test ping (a provider
431
+ // occasionally pings tool surfaces with `{}` while the
432
+ // schema is still loading), the second is a genuine
433
+ // programming mistake by the model. The model self-
434
+ // corrects MUCH more reliably when the error tells it
435
+ // (a) which fields are required, (b) their types, and
436
+ // (c) a minimal example call. Without these hints the
437
+ // downstream UI just shows a red "Failed" row and the
438
+ // model rarely retries with the right args.
439
+ const isEmptyInput =
440
+ rawInput === null ||
441
+ rawInput === undefined ||
442
+ (typeof rawInput === 'object' &&
443
+ !Array.isArray(rawInput) &&
444
+ Object.keys(rawInput as Record<string, unknown>).length === 0)
445
+
446
+ const requiredHint = describeRequiredInput(tool.inputSchema)
447
+ // A conditional schema's required shape cannot be reconstructed
448
+ // from JSON Schema's top-level `required`, so the author gets to
449
+ // say what a valid retry looks like.
450
+ const recoveryHint = tool.validationErrorHint?.trim()
451
+ ? ` ${tool.validationErrorHint.trim()}`
452
+ : ''
453
+
454
+ const enrichedMessage = isEmptyInput
455
+ ? `Tool "${toolName}" was called with no arguments. ${requiredHint}${recoveryHint} Retry the call with the required parameters populated.`
456
+ : `Validation failed for "${toolName}": ${errorMessage}. ${requiredHint}${recoveryHint}`
457
+
458
+ this.log.error(`Tool input validation failed: ${toolName}`, {
459
+ errors: errorMessage,
460
+ empty: isEmptyInput,
461
+ })
470
462
 
471
- return {
472
- success: false,
473
- output: '',
474
- error: enrichedMessage,
463
+ span.setAttributes({
464
+ [NAMZU.TOOL_SUCCESS]: false,
465
+ [NAMZU.TOOL_ERROR]: `Validation: ${errorMessage}`,
466
+ })
467
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
468
+
469
+ return {
470
+ success: false,
471
+ output: '',
472
+ error: enrichedMessage,
473
+ }
475
474
  }
476
- }
477
475
 
478
- const finalInput = parseResult.data
476
+ const finalInput = parseResult.data
479
477
 
480
- try {
481
- this.log.debug(`Executing tool: ${toolName}`)
482
- const startedAt = Date.now()
483
- const result = await tool.execute(finalInput, context)
484
- const durationMs = Date.now() - startedAt
485
- this.log.debug(`Tool completed: ${toolName}`, {
486
- success: result.success,
487
- })
478
+ try {
479
+ this.log.debug(`Executing tool: ${toolName}`)
480
+ const startedAt = Date.now()
481
+ const result = await tool.execute(finalInput, context)
482
+ const durationMs = Date.now() - startedAt
483
+ this.log.debug(`Tool completed: ${toolName}`, {
484
+ success: result.success,
485
+ })
488
486
 
489
- span.setAttribute(NAMZU.TOOL_SUCCESS, result.success)
490
- recordToolCall(
491
- toolName,
492
- result.success,
493
- result.success ? undefined : result.error,
494
- durationMs,
495
- )
496
- if (!result.success && result.error) {
497
- span.setAttribute(NAMZU.TOOL_ERROR, result.error)
498
- span.setStatus({ code: SpanStatusCode.ERROR, message: result.error })
499
- } else {
500
- span.setStatus({ code: SpanStatusCode.OK })
501
- }
487
+ span.setAttribute(NAMZU.TOOL_SUCCESS, result.success)
488
+ recordToolCall(
489
+ toolName,
490
+ result.success,
491
+ result.success ? undefined : result.error,
492
+ durationMs,
493
+ )
494
+ if (!result.success && result.error) {
495
+ span.setAttribute(NAMZU.TOOL_ERROR, result.error)
496
+ span.setStatus({ code: SpanStatusCode.ERROR, message: result.error })
497
+ } else {
498
+ span.setStatus({ code: SpanStatusCode.OK })
499
+ }
502
500
 
503
- return result
504
- } catch (err) {
505
- const errorMessage = toErrorMessage(err)
506
- this.log.error(`Tool execution error: ${toolName}`, {
507
- error: errorMessage,
508
- })
501
+ return result
502
+ } catch (err) {
503
+ const errorMessage = toErrorMessage(err)
504
+ this.log.error(`Tool execution error: ${toolName}`, {
505
+ error: errorMessage,
506
+ })
509
507
 
510
- span.setAttributes({
511
- [NAMZU.TOOL_SUCCESS]: false,
512
- [NAMZU.TOOL_ERROR]: errorMessage,
513
- })
514
- span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
515
- span.recordException(err instanceof Error ? err : new Error(errorMessage))
508
+ span.setAttributes({
509
+ [NAMZU.TOOL_SUCCESS]: false,
510
+ [NAMZU.TOOL_ERROR]: errorMessage,
511
+ })
512
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
513
+ span.recordException(err instanceof Error ? err : new Error(errorMessage))
516
514
 
517
- return {
518
- success: false,
519
- output: '',
520
- error: `Tool "${toolName}" execution failed: ${errorMessage}`,
515
+ return {
516
+ success: false,
517
+ output: '',
518
+ error: `Tool "${toolName}" execution failed: ${errorMessage}`,
519
+ }
521
520
  }
522
521
  } finally {
522
+ // The only place this span ends. It used to be ended at three
523
+ // early returns and in a finally that opened below them, so
524
+ // anything throwing before that try — `getOrThrow` on a name the
525
+ // registry does not hold, for one — left the span open and the
526
+ // tool's trace unclosed.
523
527
  span.end()
524
528
  }
525
529
  })
@@ -44,6 +44,26 @@ const NO_VISION_CAPABILITIES: ProviderCapabilities = {
44
44
  supportsVision: false,
45
45
  }
46
46
 
47
+ /**
48
+ * A tool that opts in to constrained generation, which is the only thing
49
+ * that makes `enforceToolInputSchema` non-empty on the wire.
50
+ */
51
+ function registerEnforcedTool(tools: ToolRegistry, name: string): void {
52
+ tools.register({
53
+ name,
54
+ description: `${name} tool`,
55
+ inputSchema: z.object({ new_string: z.string().optional(), newStr: z.string().optional() }),
56
+ modelInputSchema: {
57
+ type: 'object',
58
+ properties: { new_string: { type: 'string' } },
59
+ required: ['new_string'],
60
+ additionalProperties: false,
61
+ },
62
+ enforceModelInput: true,
63
+ execute: async () => ({ success: true, output: 'ok' }),
64
+ })
65
+ }
66
+
47
67
  function registerEchoTool(tools: ToolRegistry): void {
48
68
  tools.register({
49
69
  name: 'echo',
@@ -229,4 +249,53 @@ describe('query() capability negotiation', () => {
229
249
  }),
230
250
  ).rejects.toThrow(/supportsVision: false/)
231
251
  })
252
+
253
+ it('names the enforced tools on the request, so a driver can constrain them', async () => {
254
+ const provider = capturingProvider()
255
+ const tools = new ToolRegistry()
256
+ registerEnforcedTool(tools, 'edit')
257
+ registerEnforcedTool(tools, 'write')
258
+ registerEchoTool(tools)
259
+
260
+ const run = await drainQuery({
261
+ ...baseParams(provider, tools, await mkWorkdir()),
262
+ messages: [createUserMessage('hello')],
263
+ })
264
+
265
+ expect(run.status).toBe('completed')
266
+ // The producer was deleted, so this was undefined on every request and
267
+ // the three drivers that read it were reading a field nothing ever
268
+ // set — `enforceModelInput` on a tool meant nothing end to end.
269
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit', 'write'])
270
+ })
271
+
272
+ it('omits the field entirely when no tool opts in', async () => {
273
+ const provider = capturingProvider()
274
+ const tools = new ToolRegistry()
275
+ registerEchoTool(tools)
276
+
277
+ await drainQuery({
278
+ ...baseParams(provider, tools, await mkWorkdir()),
279
+ messages: [createUserMessage('hello')],
280
+ })
281
+
282
+ // An empty array would read as "enforce nothing" rather than "nothing
283
+ // asked", and a driver cannot tell those two apart.
284
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toBeUndefined()
285
+ })
286
+
287
+ it('follows the allowed set rather than everything registered', async () => {
288
+ const provider = capturingProvider()
289
+ const tools = new ToolRegistry()
290
+ registerEnforcedTool(tools, 'edit')
291
+ registerEnforcedTool(tools, 'excluded')
292
+
293
+ await drainQuery({
294
+ ...baseParams(provider, tools, await mkWorkdir()),
295
+ allowedTools: ['edit'],
296
+ messages: [createUserMessage('hello')],
297
+ })
298
+
299
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit'])
300
+ })
232
301
  })
@@ -0,0 +1,154 @@
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 { SessionId, TenantId } from '../../../types/ids/index.js'
9
+ import { createUserMessage } from '../../../types/message/index.js'
10
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
11
+ import type { Skill } from '../../../types/skills/index.js'
12
+ import { drainQuery } from '../index.js'
13
+
14
+ /**
15
+ * A run's skills are fixed at `query()` time and rendered into the cached
16
+ * system prefix, so every skill a run might ever need is paid for on every
17
+ * turn. A phased agent rarely needs them all at once.
18
+ *
19
+ * A peer runtime resolves instructions, model, tools, skills and subagents
20
+ * from context at run time. namzu had the first three; this is the fourth.
21
+ * The fifth is deliberately absent — see the note at the bottom.
22
+ */
23
+
24
+ let workdirs: string[] = []
25
+
26
+ afterEach(async () => {
27
+ await Promise.all(workdirs.map((d) => rm(d, { recursive: true, force: true })))
28
+ workdirs = []
29
+ })
30
+
31
+ function skill(name: string, body?: string): Skill {
32
+ return {
33
+ metadata: { name, description: `${name} description` },
34
+ dirPath: `/skills/${name}`,
35
+ ...(body !== undefined ? { body } : {}),
36
+ }
37
+ }
38
+
39
+ async function run(provider: MockLLMProvider, over: Record<string, unknown> = {}): Promise<void> {
40
+ const dir = await mkdtemp(join(tmpdir(), 'namzu-skills-'))
41
+ workdirs.push(dir)
42
+ await drainQuery({
43
+ provider,
44
+ tools: new ToolRegistry(),
45
+ runConfig: {
46
+ model: 'mock-model',
47
+ timeoutMs: 30_000,
48
+ tokenBudget: 100_000,
49
+ maxIterations: 4,
50
+ maxResponseTokens: 256,
51
+ },
52
+ agentId: 'agent_sk',
53
+ agentName: 'Skill Agent',
54
+ workingDirectory: dir,
55
+ sessionId: 'ses_sk' as SessionId,
56
+ threadId: 'thd_sk' as ThreadId,
57
+ projectId: 'prj_sk' as ProjectId,
58
+ tenantId: 'tnt_sk' as TenantId,
59
+ messages: [createUserMessage('go')],
60
+ ...over,
61
+ } as never)
62
+ }
63
+
64
+ const sent = (provider: MockLLMProvider, index = 0): string =>
65
+ JSON.stringify(provider.requests.at(index)?.messages ?? [])
66
+
67
+ describe('a step can put a skill in front of the model', () => {
68
+ it('renders the skill it named', async () => {
69
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
70
+
71
+ await run(provider, { prepareStep: () => ({ skills: [skill('search-the-web')] }) })
72
+
73
+ expect(sent(provider)).toContain('search-the-web')
74
+ })
75
+
76
+ it('sends nothing extra when the step names none', async () => {
77
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
78
+
79
+ await run(provider)
80
+
81
+ expect(sent(provider)).not.toContain('Available Skills')
82
+ })
83
+
84
+ it('treats an empty list as naming none', async () => {
85
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
86
+
87
+ await run(provider, { prepareStep: () => ({ skills: [] }) })
88
+
89
+ // An empty list is a caller saying "no skills this step", not a
90
+ // request for an empty section header.
91
+ expect(sent(provider)).not.toContain('Available Skills')
92
+ })
93
+
94
+ it('carries the step guidance alongside it', async () => {
95
+ const provider = new MockLLMProvider({ turns: [{ text: 'done' }] })
96
+
97
+ await run(provider, {
98
+ prepareStep: () => ({ system: 'You are researching.', skills: [skill('search-the-web')] }),
99
+ })
100
+
101
+ // Both ride the same ephemeral trailing message; neither should
102
+ // displace the other.
103
+ const body = sent(provider)
104
+ expect(body).toContain('You are researching.')
105
+ expect(body).toContain('search-the-web')
106
+ })
107
+ })
108
+
109
+ describe('a step skill does not outlive its step', () => {
110
+ it('is absent from the next step that does not ask for it', async () => {
111
+ const provider = new MockLLMProvider({
112
+ turns: [{ toolCalls: [] as never, text: 'thinking' }, { text: 'done' }],
113
+ })
114
+
115
+ await run(provider, {
116
+ prepareStep: ({ stepNumber }: { stepNumber: number }) =>
117
+ stepNumber === 1 ? { skills: [skill('search-the-web')] } : {},
118
+ })
119
+
120
+ expect(sent(provider, 0)).toContain('search-the-web')
121
+ if (provider.requests.length > 1) {
122
+ expect(sent(provider, 1)).not.toContain('search-the-web')
123
+ }
124
+ })
125
+
126
+ it('does not accumulate across steps', async () => {
127
+ const provider = new MockLLMProvider({
128
+ turns: [{ text: 'still working' }, { text: 'done' }],
129
+ })
130
+
131
+ await run(provider, {
132
+ maxIterations: 2,
133
+ prepareStep: () => ({ skills: [skill('search-the-web')] }),
134
+ })
135
+
136
+ // Appended for the call, never retained. Were it written into the run's
137
+ // history the section would stack up turn after turn — the same
138
+ // message repeated, growing the prompt and invalidating the cached
139
+ // prefix every iteration.
140
+ const systemCounts = provider.requests.map(
141
+ (r) => (r.messages ?? []).filter((m) => m.role === 'system').length,
142
+ )
143
+ expect(new Set(systemCounts).size).toBe(1)
144
+ })
145
+ })
146
+
147
+ /**
148
+ * Sub-agents are deliberately NOT per-step, and this is the note rather than
149
+ * a test that would pretend otherwise. Which agents `create_task` can reach
150
+ * is baked into that tool's input schema, so varying it per step would
151
+ * rebuild the tool catalogue every turn — a worse prompt-cache trade than
152
+ * moving tools around, for a narrowing a step can already express by
153
+ * withholding `create_task` through `activeTools`.
154
+ */