@namzu/sdk 0.6.0 → 1.0.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 (302) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/dist/advisory/executor.d.ts.map +1 -1
  3. package/dist/advisory/executor.js +9 -2
  4. package/dist/advisory/executor.js.map +1 -1
  5. package/dist/advisory/executor.test.d.ts +2 -1
  6. package/dist/advisory/executor.test.d.ts.map +1 -1
  7. package/dist/advisory/executor.test.js +7 -4
  8. package/dist/advisory/executor.test.js.map +1 -1
  9. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  10. package/dist/agents/ReactiveAgent.js +2 -0
  11. package/dist/agents/ReactiveAgent.js.map +1 -1
  12. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  13. package/dist/agents/SupervisorAgent.js +7 -0
  14. package/dist/agents/SupervisorAgent.js.map +1 -1
  15. package/dist/bridge/sse/mapper.test.js +2 -2
  16. package/dist/constants/compaction/index.d.ts.map +1 -1
  17. package/dist/constants/compaction/index.js +8 -3
  18. package/dist/constants/compaction/index.js.map +1 -1
  19. package/dist/constants/sandbox/index.d.ts +21 -0
  20. package/dist/constants/sandbox/index.d.ts.map +1 -1
  21. package/dist/constants/sandbox/index.js +30 -0
  22. package/dist/constants/sandbox/index.js.map +1 -1
  23. package/dist/constants/tools/index.d.ts.map +1 -1
  24. package/dist/constants/tools/index.js +33 -2
  25. package/dist/constants/tools/index.js.map +1 -1
  26. package/dist/manager/run/persistence.d.ts.map +1 -1
  27. package/dist/manager/run/persistence.js +35 -5
  28. package/dist/manager/run/persistence.js.map +1 -1
  29. package/dist/persona/assembler.d.ts +1 -0
  30. package/dist/persona/assembler.d.ts.map +1 -1
  31. package/dist/persona/assembler.js +28 -6
  32. package/dist/persona/assembler.js.map +1 -1
  33. package/dist/provider/collect.test.js +2 -2
  34. package/dist/public-runtime.d.ts +5 -4
  35. package/dist/public-runtime.d.ts.map +1 -1
  36. package/dist/public-runtime.js +5 -4
  37. package/dist/public-runtime.js.map +1 -1
  38. package/dist/public-tools.d.ts +2 -0
  39. package/dist/public-tools.d.ts.map +1 -1
  40. package/dist/public-tools.js +2 -0
  41. package/dist/public-tools.js.map +1 -1
  42. package/dist/public-types.d.ts +3 -0
  43. package/dist/public-types.d.ts.map +1 -1
  44. package/dist/registry/index.d.ts +2 -0
  45. package/dist/registry/index.d.ts.map +1 -1
  46. package/dist/registry/index.js +1 -0
  47. package/dist/registry/index.js.map +1 -1
  48. package/dist/registry/tool/execute.d.ts.map +1 -1
  49. package/dist/registry/tool/execute.js +87 -5
  50. package/dist/registry/tool/execute.js.map +1 -1
  51. package/dist/registry/tool/execute.test.d.ts +4 -2
  52. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  53. package/dist/registry/tool/execute.test.js +112 -3
  54. package/dist/registry/tool/execute.test.js.map +1 -1
  55. package/dist/registry/toolset/catalog.d.ts +42 -0
  56. package/dist/registry/toolset/catalog.d.ts.map +1 -0
  57. package/dist/registry/toolset/catalog.js +217 -0
  58. package/dist/registry/toolset/catalog.js.map +1 -0
  59. package/dist/registry/toolset/catalog.test.d.ts +2 -0
  60. package/dist/registry/toolset/catalog.test.d.ts.map +1 -0
  61. package/dist/registry/toolset/catalog.test.js +85 -0
  62. package/dist/registry/toolset/catalog.test.js.map +1 -0
  63. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts +2 -0
  64. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts.map +1 -0
  65. package/dist/runtime/query/__tests__/deferred-tools.test.js +147 -0
  66. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -0
  67. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts +2 -0
  68. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts.map +1 -0
  69. package/dist/runtime/query/__tests__/executor-concurrency.test.js +98 -0
  70. package/dist/runtime/query/__tests__/executor-concurrency.test.js.map +1 -0
  71. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +38 -3
  72. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -1
  73. package/dist/runtime/query/__tests__/prompt.test.js +47 -2
  74. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  75. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts +2 -0
  76. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts.map +1 -0
  77. package/dist/runtime/query/__tests__/stream-recovery.test.js +126 -0
  78. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -0
  79. package/dist/runtime/query/continuation.d.ts +16 -0
  80. package/dist/runtime/query/continuation.d.ts.map +1 -0
  81. package/dist/runtime/query/continuation.js +16 -0
  82. package/dist/runtime/query/continuation.js.map +1 -0
  83. package/dist/runtime/query/executor.d.ts +3 -0
  84. package/dist/runtime/query/executor.d.ts.map +1 -1
  85. package/dist/runtime/query/executor.js +71 -3
  86. package/dist/runtime/query/executor.js.map +1 -1
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +19 -3
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts +22 -0
  91. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/index.js +227 -60
  93. package/dist/runtime/query/iteration/index.js.map +1 -1
  94. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  95. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  97. package/dist/runtime/query/prompt.d.ts.map +1 -1
  98. package/dist/runtime/query/prompt.js +21 -1
  99. package/dist/runtime/query/prompt.js.map +1 -1
  100. package/dist/runtime/query/tooling.d.ts +1 -0
  101. package/dist/runtime/query/tooling.d.ts.map +1 -1
  102. package/dist/runtime/query/tooling.js +1 -0
  103. package/dist/runtime/query/tooling.js.map +1 -1
  104. package/dist/sandbox/provider/local.d.ts.map +1 -1
  105. package/dist/sandbox/provider/local.js +32 -1
  106. package/dist/sandbox/provider/local.js.map +1 -1
  107. package/dist/session/workspace/__tests__/shared-run.test.d.ts +2 -0
  108. package/dist/session/workspace/__tests__/shared-run.test.d.ts.map +1 -0
  109. package/dist/session/workspace/__tests__/shared-run.test.js +147 -0
  110. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -0
  111. package/dist/session/workspace/index.d.ts +2 -0
  112. package/dist/session/workspace/index.d.ts.map +1 -1
  113. package/dist/session/workspace/index.js +1 -0
  114. package/dist/session/workspace/index.js.map +1 -1
  115. package/dist/session/workspace/shared-run.d.ts +81 -0
  116. package/dist/session/workspace/shared-run.d.ts.map +1 -0
  117. package/dist/session/workspace/shared-run.js +251 -0
  118. package/dist/session/workspace/shared-run.js.map +1 -0
  119. package/dist/skills/loader.d.ts.map +1 -1
  120. package/dist/skills/loader.js +36 -6
  121. package/dist/skills/loader.js.map +1 -1
  122. package/dist/skills/loader.test.d.ts +2 -0
  123. package/dist/skills/loader.test.d.ts.map +1 -0
  124. package/dist/skills/loader.test.js +65 -0
  125. package/dist/skills/loader.test.js.map +1 -0
  126. package/dist/streaming/coalesce.test.js +1 -1
  127. package/dist/tools/builtins/__tests__/edit.test.d.ts +2 -0
  128. package/dist/tools/builtins/__tests__/edit.test.d.ts.map +1 -0
  129. package/dist/tools/builtins/__tests__/edit.test.js +38 -0
  130. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -0
  131. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts +2 -0
  132. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts.map +1 -0
  133. package/dist/tools/builtins/__tests__/payload-budget.test.js +22 -0
  134. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -0
  135. package/dist/tools/builtins/__tests__/read-file.test.d.ts +2 -0
  136. package/dist/tools/builtins/__tests__/read-file.test.d.ts.map +1 -0
  137. package/dist/tools/builtins/__tests__/read-file.test.js +24 -0
  138. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -0
  139. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts +2 -0
  140. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts.map +1 -0
  141. package/dist/tools/builtins/__tests__/verify-outputs.test.js +52 -0
  142. package/dist/tools/builtins/__tests__/verify-outputs.test.js.map +1 -0
  143. package/dist/tools/builtins/__tests__/write-file.test.d.ts +2 -0
  144. package/dist/tools/builtins/__tests__/write-file.test.d.ts.map +1 -0
  145. package/dist/tools/builtins/__tests__/write-file.test.js +74 -0
  146. package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -0
  147. package/dist/tools/builtins/bash.d.ts.map +1 -1
  148. package/dist/tools/builtins/bash.js +40 -7
  149. package/dist/tools/builtins/bash.js.map +1 -1
  150. package/dist/tools/builtins/edit.d.ts +5 -2
  151. package/dist/tools/builtins/edit.d.ts.map +1 -1
  152. package/dist/tools/builtins/edit.js +114 -18
  153. package/dist/tools/builtins/edit.js.map +1 -1
  154. package/dist/tools/builtins/index.d.ts +1 -0
  155. package/dist/tools/builtins/index.d.ts.map +1 -1
  156. package/dist/tools/builtins/index.js +13 -13
  157. package/dist/tools/builtins/index.js.map +1 -1
  158. package/dist/tools/builtins/read-file.d.ts +1 -0
  159. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  160. package/dist/tools/builtins/read-file.js +23 -8
  161. package/dist/tools/builtins/read-file.js.map +1 -1
  162. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  163. package/dist/tools/builtins/search-tools.js +4 -1
  164. package/dist/tools/builtins/search-tools.js.map +1 -1
  165. package/dist/tools/builtins/verify-outputs.d.ts +5 -0
  166. package/dist/tools/builtins/verify-outputs.d.ts.map +1 -0
  167. package/dist/tools/builtins/verify-outputs.js +103 -0
  168. package/dist/tools/builtins/verify-outputs.js.map +1 -0
  169. package/dist/tools/builtins/write-file.d.ts +3 -2
  170. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  171. package/dist/tools/builtins/write-file.js +72 -12
  172. package/dist/tools/builtins/write-file.js.map +1 -1
  173. package/dist/tools/coordinator/__tests__/agent.test.d.ts +15 -0
  174. package/dist/tools/coordinator/__tests__/agent.test.d.ts.map +1 -0
  175. package/dist/tools/coordinator/__tests__/agent.test.js +142 -0
  176. package/dist/tools/coordinator/__tests__/agent.test.js.map +1 -0
  177. package/dist/tools/coordinator/__tests__/task-list.test.d.ts +13 -0
  178. package/dist/tools/coordinator/__tests__/task-list.test.d.ts.map +1 -0
  179. package/dist/tools/coordinator/__tests__/task-list.test.js +162 -0
  180. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -0
  181. package/dist/tools/coordinator/agent.d.ts +34 -0
  182. package/dist/tools/coordinator/agent.d.ts.map +1 -0
  183. package/dist/tools/coordinator/agent.js +107 -0
  184. package/dist/tools/coordinator/agent.js.map +1 -0
  185. package/dist/tools/coordinator/index.d.ts +7 -0
  186. package/dist/tools/coordinator/index.d.ts.map +1 -1
  187. package/dist/tools/coordinator/index.js +111 -21
  188. package/dist/tools/coordinator/index.js.map +1 -1
  189. package/dist/types/agent/base.d.ts +8 -0
  190. package/dist/types/agent/base.d.ts.map +1 -1
  191. package/dist/types/agent/reactive.d.ts +23 -0
  192. package/dist/types/agent/reactive.d.ts.map +1 -1
  193. package/dist/types/agent/supervisor.d.ts +14 -0
  194. package/dist/types/agent/supervisor.d.ts.map +1 -1
  195. package/dist/types/message/index.d.ts +22 -1
  196. package/dist/types/message/index.d.ts.map +1 -1
  197. package/dist/types/message/index.js +7 -2
  198. package/dist/types/message/index.js.map +1 -1
  199. package/dist/types/provider/chat.d.ts +2 -9
  200. package/dist/types/provider/chat.d.ts.map +1 -1
  201. package/dist/types/run/events.d.ts +6 -0
  202. package/dist/types/run/events.d.ts.map +1 -1
  203. package/dist/types/run/events.js.map +1 -1
  204. package/dist/types/sandbox/index.d.ts +193 -0
  205. package/dist/types/sandbox/index.d.ts.map +1 -1
  206. package/dist/types/sandbox/index.js.map +1 -1
  207. package/dist/types/skills/index.d.ts +2 -0
  208. package/dist/types/skills/index.d.ts.map +1 -1
  209. package/dist/types/tool/index.d.ts +22 -0
  210. package/dist/types/tool/index.d.ts.map +1 -1
  211. package/dist/types/toolset/index.d.ts +71 -0
  212. package/dist/types/toolset/index.d.ts.map +1 -0
  213. package/dist/types/toolset/index.js +2 -0
  214. package/dist/types/toolset/index.js.map +1 -0
  215. package/dist/types/workspace/index.d.ts +1 -0
  216. package/dist/types/workspace/index.d.ts.map +1 -1
  217. package/dist/types/workspace/shared-run.d.ts +61 -0
  218. package/dist/types/workspace/shared-run.d.ts.map +1 -0
  219. package/dist/types/workspace/shared-run.js +2 -0
  220. package/dist/types/workspace/shared-run.js.map +1 -0
  221. package/dist/verification/index.d.ts +1 -0
  222. package/dist/verification/index.d.ts.map +1 -1
  223. package/dist/verification/index.js +1 -0
  224. package/dist/verification/index.js.map +1 -1
  225. package/dist/verification/presets.d.ts +53 -0
  226. package/dist/verification/presets.d.ts.map +1 -0
  227. package/dist/verification/presets.js +70 -0
  228. package/dist/verification/presets.js.map +1 -0
  229. package/dist/verification/presets.test.d.ts +16 -0
  230. package/dist/verification/presets.test.d.ts.map +1 -0
  231. package/dist/verification/presets.test.js +79 -0
  232. package/dist/verification/presets.test.js.map +1 -0
  233. package/package.json +3 -2
  234. package/src/advisory/executor.test.ts +7 -4
  235. package/src/advisory/executor.ts +11 -2
  236. package/src/agents/ReactiveAgent.ts +2 -0
  237. package/src/agents/SupervisorAgent.ts +7 -0
  238. package/src/bridge/sse/mapper.test.ts +2 -2
  239. package/src/constants/compaction/index.ts +8 -3
  240. package/src/constants/sandbox/index.ts +37 -0
  241. package/src/constants/tools/index.ts +33 -2
  242. package/src/manager/run/persistence.ts +34 -6
  243. package/src/persona/assembler.ts +31 -8
  244. package/src/provider/collect.test.ts +2 -2
  245. package/src/public-runtime.ts +14 -1
  246. package/src/public-tools.ts +2 -0
  247. package/src/public-types.ts +7 -0
  248. package/src/registry/index.ts +7 -0
  249. package/src/registry/tool/execute.test.ts +132 -3
  250. package/src/registry/tool/execute.ts +94 -9
  251. package/src/registry/toolset/catalog.test.ts +97 -0
  252. package/src/registry/toolset/catalog.ts +283 -0
  253. package/src/runtime/query/__tests__/deferred-tools.test.ts +183 -0
  254. package/src/runtime/query/__tests__/executor-concurrency.test.ts +122 -0
  255. package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +48 -3
  256. package/src/runtime/query/__tests__/prompt.test.ts +51 -2
  257. package/src/runtime/query/__tests__/stream-recovery.test.ts +156 -0
  258. package/src/runtime/query/continuation.ts +16 -0
  259. package/src/runtime/query/executor.ts +82 -13
  260. package/src/runtime/query/index.ts +24 -3
  261. package/src/runtime/query/iteration/index.ts +263 -68
  262. package/src/runtime/query/iteration/phases/context.ts +10 -0
  263. package/src/runtime/query/prompt.ts +17 -1
  264. package/src/runtime/query/tooling.ts +2 -0
  265. package/src/sandbox/provider/local.ts +33 -0
  266. package/src/session/workspace/__tests__/shared-run.test.ts +181 -0
  267. package/src/session/workspace/index.ts +6 -0
  268. package/src/session/workspace/shared-run.ts +316 -0
  269. package/src/skills/loader.test.ts +89 -0
  270. package/src/skills/loader.ts +37 -6
  271. package/src/streaming/coalesce.test.ts +1 -1
  272. package/src/tools/builtins/__tests__/edit.test.ts +57 -0
  273. package/src/tools/builtins/__tests__/payload-budget.test.ts +29 -0
  274. package/src/tools/builtins/__tests__/read-file.test.ts +31 -0
  275. package/src/tools/builtins/__tests__/verify-outputs.test.ts +71 -0
  276. package/src/tools/builtins/__tests__/write-file.test.ts +97 -0
  277. package/src/tools/builtins/bash.ts +48 -7
  278. package/src/tools/builtins/edit.ts +162 -27
  279. package/src/tools/builtins/index.ts +13 -13
  280. package/src/tools/builtins/read-file.ts +31 -8
  281. package/src/tools/builtins/search-tools.ts +5 -1
  282. package/src/tools/builtins/verify-outputs.ts +126 -0
  283. package/src/tools/builtins/write-file.ts +83 -14
  284. package/src/tools/coordinator/__tests__/agent.test.ts +172 -0
  285. package/src/tools/coordinator/__tests__/task-list.test.ts +182 -0
  286. package/src/tools/coordinator/agent.ts +157 -0
  287. package/src/tools/coordinator/index.ts +128 -22
  288. package/src/types/agent/base.ts +8 -0
  289. package/src/types/agent/reactive.ts +25 -0
  290. package/src/types/agent/supervisor.ts +16 -0
  291. package/src/types/message/index.ts +32 -2
  292. package/src/types/provider/chat.ts +2 -9
  293. package/src/types/run/events.ts +6 -0
  294. package/src/types/sandbox/index.ts +219 -0
  295. package/src/types/skills/index.ts +4 -0
  296. package/src/types/tool/index.ts +24 -0
  297. package/src/types/toolset/index.ts +86 -0
  298. package/src/types/workspace/index.ts +9 -0
  299. package/src/types/workspace/shared-run.ts +65 -0
  300. package/src/verification/index.ts +1 -0
  301. package/src/verification/presets.test.ts +112 -0
  302. package/src/verification/presets.ts +72 -0
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ import type { ToolDefinition } from '../../types/tool/index.js'
5
+ import { ToolRegistry } from '../tool/execute.js'
6
+ import { ToolCatalog, createToolCatalogFromRegistry } from './catalog.js'
7
+
8
+ function makeTool(name: string, description = `${name} tool`): ToolDefinition {
9
+ return {
10
+ name,
11
+ description,
12
+ inputSchema: z.object({ query: z.string().optional() }),
13
+ async execute() {
14
+ return { success: true, output: `${name}-ran` }
15
+ },
16
+ }
17
+ }
18
+
19
+ describe('ToolCatalog', () => {
20
+ it('keeps sources, toolsets, and tools as separate records', () => {
21
+ const catalog = new ToolCatalog()
22
+ catalog.registerSource({
23
+ id: 'mcp:microsoft-learn',
24
+ kind: 'mcp_server',
25
+ name: 'Microsoft Learn',
26
+ mcpServer: {
27
+ name: 'microsoft-learn',
28
+ url: 'https://learn.microsoft.com/api/mcp',
29
+ transport: 'streamable_http',
30
+ },
31
+ })
32
+ catalog.registerToolset({
33
+ id: 'mcp-toolset:microsoft-learn',
34
+ sourceId: 'mcp:microsoft-learn',
35
+ name: 'Microsoft Learn toolset',
36
+ defaultPolicy: {
37
+ enabled: true,
38
+ loading: 'deferred',
39
+ preferred: true,
40
+ permissionPolicy: 'always_allow',
41
+ },
42
+ })
43
+ catalog.registerTool({
44
+ name: 'microsoft_docs_search',
45
+ description: 'Search Microsoft documentation',
46
+ sourceId: 'mcp:microsoft-learn',
47
+ toolsetId: 'mcp-toolset:microsoft-learn',
48
+ policy: { enabled: true, loading: 'deferred', preferred: true },
49
+ })
50
+
51
+ expect(catalog.listSources()).toHaveLength(1)
52
+ expect(catalog.listToolsets()).toHaveLength(1)
53
+ expect(catalog.getToolsByLoading(['deferred']).map((t) => t.name)).toEqual([
54
+ 'microsoft_docs_search',
55
+ ])
56
+ })
57
+
58
+ it('searches deferred tools by name, description, source, and toolset', () => {
59
+ const registry = new ToolRegistry()
60
+ registry.register(makeTool('github_search_issues', 'Search repository issues'), 'deferred')
61
+ registry.register(makeTool('slack_search_messages', 'Search team messages'), 'deferred')
62
+ registry.register(makeTool('bash', 'Run a shell command'))
63
+
64
+ const catalog = createToolCatalogFromRegistry(registry, {
65
+ source: {
66
+ id: 'host',
67
+ kind: 'host_tool',
68
+ name: 'Host runtime',
69
+ description: 'Local shell and collaboration tools',
70
+ },
71
+ toolset: {
72
+ id: 'default-host',
73
+ name: 'Default host tools',
74
+ defaultPolicy: { enabled: true, loading: 'eager' },
75
+ },
76
+ })
77
+
78
+ expect(
79
+ catalog.searchTools('repository', { loading: ['deferred'] }).map((r) => r.tool.name),
80
+ ).toEqual(['github_search_issues'])
81
+ expect(catalog.toLLMTools().map((t) => t.function.name)).toEqual(['bash'])
82
+ })
83
+
84
+ it('preserves registry availability as catalog loading policy', () => {
85
+ const registry = new ToolRegistry()
86
+ registry.register(makeTool('read_file'))
87
+ registry.register(makeTool('web_search'), 'deferred')
88
+ registry.register(makeTool('write_file'), 'suspended')
89
+
90
+ const catalog = createToolCatalogFromRegistry(registry)
91
+
92
+ expect(catalog.getTool('read_file')?.policy.loading).toBe('eager')
93
+ expect(catalog.getTool('web_search')?.policy.loading).toBe('deferred')
94
+ expect(catalog.getTool('write_file')?.policy.loading).toBe('suspended')
95
+ expect(catalog.toLLMTools({ loading: ['suspended'] })).toEqual([])
96
+ })
97
+ })
@@ -0,0 +1,283 @@
1
+ import { zodToJsonSchema } from 'zod-to-json-schema'
2
+ import type { ToolRegistryContract } from '../../types/tool/index.js'
3
+ import type { LLMToolSchema, ToolAvailability, ToolDefinition } from '../../types/tool/index.js'
4
+ import type {
5
+ ToolCatalogEntry,
6
+ ToolCatalogSearchResult,
7
+ ToolCatalogSnapshot,
8
+ ToolLoadingMode,
9
+ ToolSource,
10
+ ToolsetDefinition,
11
+ ToolsetPolicy,
12
+ } from '../../types/toolset/index.js'
13
+
14
+ export interface ToolCatalogSearchOptions {
15
+ readonly loading?: readonly ToolLoadingMode[]
16
+ readonly limit?: number
17
+ }
18
+
19
+ export interface ToolCatalogFromRegistryOptions {
20
+ readonly source?: ToolSource
21
+ readonly toolset?: Omit<ToolsetDefinition, 'sourceId'> & {
22
+ readonly sourceId?: string
23
+ }
24
+ }
25
+
26
+ const DEFAULT_HOST_SOURCE: ToolSource = {
27
+ id: 'host-tools',
28
+ kind: 'host_tool',
29
+ name: 'Host tools',
30
+ description: 'Tools executed by the host runtime.',
31
+ }
32
+
33
+ const DEFAULT_HOST_TOOLSET: ToolsetDefinition = {
34
+ id: 'host-tools',
35
+ sourceId: DEFAULT_HOST_SOURCE.id,
36
+ name: 'Host tools',
37
+ defaultPolicy: {
38
+ enabled: true,
39
+ loading: 'eager',
40
+ permissionPolicy: 'default',
41
+ },
42
+ }
43
+
44
+ export class ToolCatalog {
45
+ private sources = new Map<string, ToolSource>()
46
+ private toolsets = new Map<string, ToolsetDefinition>()
47
+ private tools = new Map<string, ToolCatalogEntry>()
48
+
49
+ registerSource(source: ToolSource): void {
50
+ this.sources.set(source.id, source)
51
+ }
52
+
53
+ registerToolset(toolset: ToolsetDefinition): void {
54
+ if (!this.sources.has(toolset.sourceId)) {
55
+ throw new Error(`Toolset "${toolset.id}" references unknown source "${toolset.sourceId}"`)
56
+ }
57
+ this.toolsets.set(toolset.id, toolset)
58
+ }
59
+
60
+ registerTool(tool: ToolCatalogEntry): void {
61
+ if (!this.sources.has(tool.sourceId)) {
62
+ throw new Error(`Tool "${tool.name}" references unknown source "${tool.sourceId}"`)
63
+ }
64
+ if (!this.toolsets.has(tool.toolsetId)) {
65
+ throw new Error(`Tool "${tool.name}" references unknown toolset "${tool.toolsetId}"`)
66
+ }
67
+ this.tools.set(tool.name, tool)
68
+ }
69
+
70
+ getSource(id: string): ToolSource | undefined {
71
+ return this.sources.get(id)
72
+ }
73
+
74
+ getToolset(id: string): ToolsetDefinition | undefined {
75
+ return this.toolsets.get(id)
76
+ }
77
+
78
+ getTool(name: string): ToolCatalogEntry | undefined {
79
+ return this.tools.get(name)
80
+ }
81
+
82
+ listSources(): ToolSource[] {
83
+ return [...this.sources.values()]
84
+ }
85
+
86
+ listToolsets(): ToolsetDefinition[] {
87
+ return [...this.toolsets.values()]
88
+ }
89
+
90
+ listTools(): ToolCatalogEntry[] {
91
+ return [...this.tools.values()]
92
+ }
93
+
94
+ snapshot(): ToolCatalogSnapshot {
95
+ return {
96
+ sources: this.listSources(),
97
+ toolsets: this.listToolsets(),
98
+ tools: this.listTools(),
99
+ }
100
+ }
101
+
102
+ getToolsByLoading(loading: readonly ToolLoadingMode[]): ToolCatalogEntry[] {
103
+ const wanted = new Set(loading)
104
+ return this.listTools().filter((tool) => wanted.has(resolveToolLoading(tool.policy)))
105
+ }
106
+
107
+ searchTools(query: string, options: ToolCatalogSearchOptions = {}): ToolCatalogSearchResult[] {
108
+ const normalized = query.trim().toLowerCase()
109
+ const terms = normalized.split(/\s+/).filter(Boolean)
110
+ const loading = options.loading ? new Set(options.loading) : null
111
+ const results: ToolCatalogSearchResult[] = []
112
+
113
+ for (const tool of this.listTools()) {
114
+ if (loading && !loading.has(resolveToolLoading(tool.policy))) continue
115
+ if (tool.policy.enabled === false) continue
116
+
117
+ const source = this.sources.get(tool.sourceId)
118
+ const toolset = this.toolsets.get(tool.toolsetId)
119
+ if (!source || !toolset) continue
120
+
121
+ const scored = scoreToolSearch({
122
+ terms,
123
+ tool,
124
+ source,
125
+ toolset,
126
+ })
127
+ if (scored.score <= 0) continue
128
+
129
+ results.push({
130
+ tool,
131
+ source,
132
+ toolset,
133
+ score: scored.score,
134
+ matched: scored.matched,
135
+ })
136
+ }
137
+
138
+ results.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name))
139
+ return results.slice(0, options.limit ?? 5)
140
+ }
141
+
142
+ toLLMTools(options: { readonly loading?: readonly ToolLoadingMode[] } = {}): LLMToolSchema[] {
143
+ const loading = options.loading ?? ['eager']
144
+ return this.getToolsByLoading(loading)
145
+ .filter((tool) => tool.policy.enabled !== false)
146
+ .map((tool) => tool.llmSchema ?? toolDefinitionToLLMTool(tool.definition))
147
+ .filter((tool): tool is LLMToolSchema => Boolean(tool))
148
+ }
149
+ }
150
+
151
+ export function createToolCatalogFromRegistry(
152
+ registry: ToolRegistryContract,
153
+ options: ToolCatalogFromRegistryOptions = {},
154
+ ): ToolCatalog {
155
+ const source = options.source ?? DEFAULT_HOST_SOURCE
156
+ const toolset: ToolsetDefinition = {
157
+ ...DEFAULT_HOST_TOOLSET,
158
+ ...options.toolset,
159
+ sourceId: options.toolset?.sourceId ?? source.id,
160
+ }
161
+ const catalog = new ToolCatalog()
162
+ catalog.registerSource(source)
163
+ catalog.registerToolset(toolset)
164
+
165
+ for (const definition of registry.getAll()) {
166
+ const availability = registry.getAvailability(definition.name)
167
+ catalog.registerTool(
168
+ toolDefinitionToCatalogEntry(definition, {
169
+ availability,
170
+ sourceId: source.id,
171
+ toolsetId: toolset.id,
172
+ toolsetPolicy: toolset.defaultPolicy,
173
+ }),
174
+ )
175
+ }
176
+
177
+ return catalog
178
+ }
179
+
180
+ export function toolDefinitionToCatalogEntry(
181
+ definition: ToolDefinition,
182
+ input: {
183
+ readonly availability?: ToolAvailability
184
+ readonly sourceId: string
185
+ readonly toolsetId: string
186
+ readonly toolsetPolicy?: ToolsetPolicy
187
+ },
188
+ ): ToolCatalogEntry {
189
+ const loading = loadingFromAvailability(input.availability ?? 'active')
190
+ return {
191
+ name: definition.name,
192
+ description: definition.description,
193
+ sourceId: input.sourceId,
194
+ toolsetId: input.toolsetId,
195
+ definition,
196
+ permissions: definition.permissions,
197
+ category: definition.category,
198
+ policy: {
199
+ ...input.toolsetPolicy,
200
+ enabled: input.toolsetPolicy?.enabled === false ? false : loading !== 'suspended',
201
+ loading,
202
+ },
203
+ }
204
+ }
205
+
206
+ export function loadingFromAvailability(availability: ToolAvailability): ToolLoadingMode {
207
+ switch (availability) {
208
+ case 'deferred':
209
+ return 'deferred'
210
+ case 'suspended':
211
+ return 'suspended'
212
+ default:
213
+ return 'eager'
214
+ }
215
+ }
216
+
217
+ function resolveToolLoading(policy: ToolsetPolicy): ToolLoadingMode {
218
+ if (policy.enabled === false) return 'disabled'
219
+ return policy.loading ?? 'eager'
220
+ }
221
+
222
+ function toolDefinitionToLLMTool(definition: ToolDefinition | undefined): LLMToolSchema | null {
223
+ if (!definition) return null
224
+ return {
225
+ type: 'function',
226
+ function: {
227
+ name: definition.name,
228
+ description: definition.description,
229
+ parameters: zodToJsonSchema(definition.inputSchema, {
230
+ target: 'jsonSchema7',
231
+ $refStrategy: 'none',
232
+ }) as Record<string, unknown>,
233
+ },
234
+ }
235
+ }
236
+
237
+ function scoreToolSearch(input: {
238
+ readonly terms: readonly string[]
239
+ readonly tool: ToolCatalogEntry
240
+ readonly source: ToolSource
241
+ readonly toolset: ToolsetDefinition
242
+ }): { score: number; matched: string[] } {
243
+ const terms = input.terms.length > 0 ? input.terms : ['']
244
+ const matched = new Set<string>()
245
+ let score = 0
246
+
247
+ for (const term of terms) {
248
+ if (!term) continue
249
+
250
+ const toolName = input.tool.name.toLowerCase()
251
+ const description = input.tool.description.toLowerCase()
252
+ const sourceText =
253
+ `${input.source.name} ${input.source.description ?? ''} ${input.source.kind}`.toLowerCase()
254
+ const toolsetText = `${input.toolset.name} ${input.toolset.description ?? ''}`.toLowerCase()
255
+
256
+ if (toolName === term) {
257
+ score += 12
258
+ matched.add('name')
259
+ } else if (toolName.includes(term)) {
260
+ score += 8
261
+ matched.add('name')
262
+ }
263
+ if (description.includes(term)) {
264
+ score += 5
265
+ matched.add('description')
266
+ }
267
+ if (sourceText.includes(term)) {
268
+ score += 3
269
+ matched.add('source')
270
+ }
271
+ if (toolsetText.includes(term)) {
272
+ score += 2
273
+ matched.add('toolset')
274
+ }
275
+ }
276
+
277
+ if (input.tool.policy.preferred && score > 0) score += 1
278
+
279
+ return {
280
+ score,
281
+ matched: [...matched],
282
+ }
283
+ }
@@ -0,0 +1,183 @@
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 { ToolRegistry } from '../../../registry/tool/execute.js'
8
+ import { SearchToolsTool } from '../../../tools/builtins/search-tools.js'
9
+ import type { RunId, SessionId, TenantId } from '../../../types/ids/index.js'
10
+ import { createUserMessage } from '../../../types/message/index.js'
11
+ import type {
12
+ ChatCompletionParams,
13
+ LLMProvider,
14
+ StreamChunk,
15
+ } from '../../../types/provider/index.js'
16
+ import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
17
+ import { drainQuery } from '../index.js'
18
+
19
+ const ZERO_USAGE = {
20
+ promptTokens: 0,
21
+ completionTokens: 0,
22
+ totalTokens: 0,
23
+ cachedTokens: 0,
24
+ cacheWriteTokens: 0,
25
+ }
26
+
27
+ class CapturingProvider implements LLMProvider {
28
+ readonly id = 'capturing'
29
+ readonly name = 'Capturing Provider'
30
+ lastParams?: ChatCompletionParams
31
+
32
+ async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
33
+ this.lastParams = params
34
+ yield {
35
+ id: 'msg_1',
36
+ delta: { content: 'done' },
37
+ }
38
+ yield {
39
+ id: 'msg_1',
40
+ delta: {},
41
+ finishReason: 'stop',
42
+ usage: ZERO_USAGE,
43
+ }
44
+ }
45
+ }
46
+
47
+ function registerDeferredDocumentTool(tools: ToolRegistry, name = 'generate_document'): void {
48
+ tools.register(
49
+ {
50
+ name,
51
+ description: 'Generate a project document by document id.',
52
+ inputSchema: z.object({
53
+ documentId: z.string(),
54
+ }),
55
+ execute: async () => ({ success: true, output: 'generated' }),
56
+ },
57
+ 'deferred',
58
+ )
59
+ }
60
+
61
+ describe('query deferred tool discovery', () => {
62
+ let workdirs: string[] = []
63
+
64
+ afterEach(async () => {
65
+ await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
66
+ workdirs = []
67
+ })
68
+
69
+ it('auto-exposes search_tools when deferred tools are registered', async () => {
70
+ const provider = new CapturingProvider()
71
+ const tools = new ToolRegistry()
72
+ registerDeferredDocumentTool(tools)
73
+
74
+ const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-deferred-tools-'))
75
+ workdirs.push(workingDirectory)
76
+
77
+ const run = await drainQuery({
78
+ provider,
79
+ tools,
80
+ runConfig: {
81
+ model: 'mock-model',
82
+ timeoutMs: 5_000,
83
+ tokenBudget: 100_000,
84
+ maxIterations: 1,
85
+ maxResponseTokens: 256,
86
+ },
87
+ agentId: 'agent_test',
88
+ agentName: 'Test Agent',
89
+ messages: [createUserMessage('what tools can you use?')],
90
+ workingDirectory,
91
+ sessionId: 'ses_deferred_tools' as SessionId,
92
+ threadId: 'thd_deferred_tools' as ThreadId,
93
+ projectId: 'prj_deferred_tools' as ProjectId,
94
+ tenantId: 'tnt_deferred_tools' as TenantId,
95
+ })
96
+
97
+ expect(run.status).toBe('completed')
98
+ expect(tools.has(SearchToolsTool.name)).toBe(true)
99
+ expect(tools.getAvailability(SearchToolsTool.name)).toBe('active')
100
+ expect(tools.getAvailability('generate_document')).toBe('deferred')
101
+
102
+ const toolNames = provider.lastParams?.tools?.map((tool) => tool.function.name).sort() ?? []
103
+ expect(toolNames).toEqual([SearchToolsTool.name])
104
+
105
+ const systemPrompt = (provider.lastParams?.messages ?? [])
106
+ .filter((message) => message.role === 'system')
107
+ .map((message) => message.content)
108
+ .join('\n')
109
+ expect(systemPrompt).toContain('Use search_tools to load these before use:')
110
+ expect(systemPrompt).toContain('- generate_document')
111
+ expect(systemPrompt).not.toContain(
112
+ 'Deferred tools are discoverable but not executable until the runtime activates them',
113
+ )
114
+ })
115
+
116
+ it('keeps search_tools executable when allowedTools names a deferred tool', async () => {
117
+ const provider = new CapturingProvider()
118
+ const tools = new ToolRegistry()
119
+ registerDeferredDocumentTool(tools)
120
+
121
+ const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-deferred-tools-'))
122
+ workdirs.push(workingDirectory)
123
+
124
+ const run = await drainQuery({
125
+ provider,
126
+ tools,
127
+ allowedTools: ['generate_document'],
128
+ runConfig: {
129
+ model: 'mock-model',
130
+ timeoutMs: 5_000,
131
+ tokenBudget: 100_000,
132
+ maxIterations: 1,
133
+ maxResponseTokens: 256,
134
+ },
135
+ agentId: 'agent_test',
136
+ agentName: 'Test Agent',
137
+ messages: [createUserMessage('generate D-01')],
138
+ workingDirectory,
139
+ sessionId: 'ses_deferred_allowed_tools' as SessionId,
140
+ threadId: 'thd_deferred_allowed_tools' as ThreadId,
141
+ projectId: 'prj_deferred_allowed_tools' as ProjectId,
142
+ tenantId: 'tnt_deferred_allowed_tools' as TenantId,
143
+ })
144
+
145
+ expect(run.status).toBe('completed')
146
+ expect(tools.getAvailability('generate_document')).toBe('deferred')
147
+
148
+ const toolNames = provider.lastParams?.tools?.map((tool) => tool.function.name).sort() ?? []
149
+ expect(toolNames).toEqual([SearchToolsTool.name])
150
+
151
+ const systemPrompt = (provider.lastParams?.messages ?? [])
152
+ .filter((message) => message.role === 'system')
153
+ .map((message) => message.content)
154
+ .join('\n')
155
+ expect(systemPrompt).toContain('Use search_tools to load these before use:')
156
+ expect(systemPrompt).toContain('- generate_document')
157
+ })
158
+
159
+ it('does not let search_tools reveal or activate deferred tools outside allowedTools', async () => {
160
+ const tools = new ToolRegistry()
161
+ registerDeferredDocumentTool(tools)
162
+ registerDeferredDocumentTool(tools, 'dangerous_delete_document')
163
+
164
+ const result = await SearchToolsTool.execute(
165
+ { query: 'delete' },
166
+ {
167
+ runId: 'run_deferred_allowed_tools' as RunId,
168
+ workingDirectory: '/tmp',
169
+ abortSignal: new AbortController().signal,
170
+ env: {},
171
+ log: () => undefined,
172
+ toolRegistry: tools,
173
+ allowedTools: ['generate_document', SearchToolsTool.name],
174
+ },
175
+ )
176
+
177
+ expect(result.success).toBe(true)
178
+ expect(result.output).toContain('No deferred tools matching "delete"')
179
+ expect(result.output).not.toContain('dangerous_delete_document')
180
+ expect(tools.getAvailability('generate_document')).toBe('deferred')
181
+ expect(tools.getAvailability('dangerous_delete_document')).toBe('deferred')
182
+ })
183
+ })
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { ActivityStore } from '../../../store/activity/memory.js'
3
+ import type { RunId } from '../../../types/ids/index.js'
4
+ import type { ChatCompletionResponse } from '../../../types/provider/index.js'
5
+ import type { RunEvent } from '../../../types/run/index.js'
6
+ import type { ToolRegistryContract } from '../../../types/tool/index.js'
7
+ import type { Logger } from '../../../utils/logger.js'
8
+ import { ToolExecutor } from '../executor.js'
9
+
10
+ const mockRunId = 'run_test' as RunId
11
+
12
+ function makeLogger(): Logger {
13
+ const stub = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
14
+ return { ...stub, child: vi.fn(() => ({ ...stub, child: vi.fn() })) } as unknown as Logger
15
+ }
16
+
17
+ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))
18
+
19
+ /** Two tool calls in one assistant message (one batch). */
20
+ function twoCallResponse(name: string, a: object, b: object): ChatCompletionResponse {
21
+ return {
22
+ message: {
23
+ role: 'assistant',
24
+ content: null,
25
+ toolCalls: [
26
+ { id: 'c1', type: 'function', function: { name, arguments: JSON.stringify(a) } },
27
+ { id: 'c2', type: 'function', function: { name, arguments: JSON.stringify(b) } },
28
+ ],
29
+ },
30
+ finishReason: 'tool_calls',
31
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
32
+ } as ChatCompletionResponse
33
+ }
34
+
35
+ describe('ToolExecutor — concurrencySafe batching', () => {
36
+ let activityStore: ActivityStore
37
+ let emitEvent: (e: RunEvent) => Promise<void>
38
+
39
+ beforeEach(() => {
40
+ activityStore = new ActivityStore(mockRunId, {
41
+ enabled: true,
42
+ trackToolCalls: true,
43
+ trackLlmTurns: true,
44
+ })
45
+ emitEvent = async () => {}
46
+ })
47
+
48
+ it('serializes concurrency-unsafe tools so read-modify-write does not race', async () => {
49
+ // Shared mutable state, mutated via read → await → write (like `edit`).
50
+ let file = 'A'
51
+ const execute = vi.fn(async (_name: string, input: unknown) => {
52
+ const current = file // read
53
+ await delay(10) // window a parallel run would exploit
54
+ file = current + (input as { add: string }).add // write
55
+ return { success: true, output: 'ok' }
56
+ })
57
+ const tools = {
58
+ register: vi.fn(),
59
+ unregister: vi.fn(),
60
+ execute,
61
+ // edit/write/bash declare concurrencySafe:false → isConcurrencySafe()=>false
62
+ get: vi.fn(() => ({ isConcurrencySafe: () => false })),
63
+ has: vi.fn(() => true),
64
+ listNames: vi.fn(() => []),
65
+ getAvailability: vi.fn(),
66
+ } as unknown as ToolRegistryContract
67
+
68
+ const exec = new ToolExecutor(
69
+ {
70
+ tools,
71
+ runId: mockRunId,
72
+ workingDirectory: '/tmp',
73
+ permissionMode: 'auto',
74
+ env: {},
75
+ abortSignal: new AbortController().signal,
76
+ },
77
+ activityStore,
78
+ emitEvent,
79
+ makeLogger(),
80
+ )
81
+ await exec.executeBatch(twoCallResponse('edit', { add: 'B' }, { add: 'C' }))
82
+ // Serialized: A→AB→ABC. A racing run would lose one append (e.g. 'AC').
83
+ expect(file).toBe('ABC')
84
+ })
85
+
86
+ it('runs concurrency-safe tools in parallel', async () => {
87
+ let active = 0
88
+ let maxActive = 0
89
+ const execute = vi.fn(async () => {
90
+ active++
91
+ maxActive = Math.max(maxActive, active)
92
+ await delay(10)
93
+ active--
94
+ return { success: true, output: 'ok' }
95
+ })
96
+ const tools = {
97
+ register: vi.fn(),
98
+ unregister: vi.fn(),
99
+ execute,
100
+ get: vi.fn(() => ({ isConcurrencySafe: () => true })),
101
+ has: vi.fn(() => true),
102
+ listNames: vi.fn(() => []),
103
+ getAvailability: vi.fn(),
104
+ } as unknown as ToolRegistryContract
105
+
106
+ const exec = new ToolExecutor(
107
+ {
108
+ tools,
109
+ runId: mockRunId,
110
+ workingDirectory: '/tmp',
111
+ permissionMode: 'auto',
112
+ env: {},
113
+ abortSignal: new AbortController().signal,
114
+ },
115
+ activityStore,
116
+ emitEvent,
117
+ makeLogger(),
118
+ )
119
+ await exec.executeBatch(twoCallResponse('grep', { p: '1' }, { p: '2' }))
120
+ expect(maxActive).toBe(2) // both ran at once
121
+ })
122
+ })