@open-mercato/ai-assistant 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (273) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +361 -0
  3. package/README.md +5 -0
  4. package/dist/index.js +154 -0
  5. package/dist/index.js.map +2 -2
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
  12. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
  13. package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
  14. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
  15. package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
  16. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
  17. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
  18. package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
  19. package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
  20. package/dist/modules/ai_assistant/ai-tools.js +14 -0
  21. package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
  22. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
  23. package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
  24. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
  25. package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
  26. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
  27. package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
  28. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
  29. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
  30. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
  31. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
  32. package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
  33. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
  34. package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
  35. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
  36. package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
  37. package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
  38. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
  39. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
  40. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
  41. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
  42. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
  43. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
  44. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
  45. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
  46. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
  47. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
  48. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
  49. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
  50. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
  51. package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
  52. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
  53. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
  54. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
  55. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
  56. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
  57. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
  58. package/dist/modules/ai_assistant/cli.js +78 -12
  59. package/dist/modules/ai_assistant/cli.js.map +2 -2
  60. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
  61. package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
  62. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
  63. package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
  64. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
  65. package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
  66. package/dist/modules/ai_assistant/data/entities.js +228 -0
  67. package/dist/modules/ai_assistant/data/entities.js.map +7 -0
  68. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
  69. package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
  70. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
  71. package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
  72. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
  73. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
  74. package/dist/modules/ai_assistant/events.js +33 -0
  75. package/dist/modules/ai_assistant/events.js.map +7 -0
  76. package/dist/modules/ai_assistant/i18n/de.json +252 -0
  77. package/dist/modules/ai_assistant/i18n/en.json +252 -0
  78. package/dist/modules/ai_assistant/i18n/es.json +252 -0
  79. package/dist/modules/ai_assistant/i18n/pl.json +252 -0
  80. package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
  81. package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
  82. package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
  83. package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
  84. package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
  85. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
  86. package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
  87. package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
  88. package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
  89. package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
  90. package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
  91. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
  92. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
  93. package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
  94. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
  95. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
  96. package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
  97. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
  98. package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
  99. package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
  100. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
  101. package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
  102. package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
  103. package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
  104. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
  105. package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
  106. package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
  107. package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
  108. package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
  109. package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
  110. package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
  111. package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
  112. package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
  113. package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
  114. package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
  115. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
  116. package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
  117. package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
  118. package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
  119. package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
  120. package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
  121. package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
  122. package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
  123. package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
  124. package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
  125. package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
  126. package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
  127. package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
  128. package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
  129. package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
  130. package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
  131. package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
  132. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
  133. package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
  134. package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
  135. package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
  136. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
  137. package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
  138. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
  139. package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
  140. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
  141. package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
  142. package/dist/modules/ai_assistant/setup.js +36 -0
  143. package/dist/modules/ai_assistant/setup.js.map +2 -2
  144. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
  145. package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
  146. package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
  147. package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
  148. package/generated/entities/ai_pending_action/index.ts +24 -0
  149. package/generated/entities.ids.generated.ts +13 -0
  150. package/generated/entity-fields-registry.ts +57 -0
  151. package/jest.config.cjs +7 -0
  152. package/package.json +4 -4
  153. package/src/index.ts +215 -0
  154. package/src/modules/ai_assistant/__integration__/README.md +5 -0
  155. package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
  156. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
  157. package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
  158. package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
  159. package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
  160. package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
  161. package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
  162. package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
  163. package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
  164. package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
  165. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
  166. package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
  167. package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
  168. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
  169. package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
  170. package/src/modules/ai_assistant/ai-tools.ts +21 -0
  171. package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
  172. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
  173. package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
  174. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
  175. package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
  176. package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
  177. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
  178. package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
  179. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
  180. package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
  181. package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
  182. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
  183. package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
  184. package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
  185. package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
  186. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
  187. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
  188. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
  189. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
  190. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
  191. package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
  192. package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
  193. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
  194. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
  195. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
  196. package/src/modules/ai_assistant/cli.ts +99 -24
  197. package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
  198. package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
  199. package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
  200. package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
  201. package/src/modules/ai_assistant/data/entities.ts +270 -0
  202. package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
  203. package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
  204. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
  205. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
  206. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
  207. package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
  208. package/src/modules/ai_assistant/events.ts +112 -0
  209. package/src/modules/ai_assistant/i18n/de.json +252 -0
  210. package/src/modules/ai_assistant/i18n/en.json +252 -0
  211. package/src/modules/ai_assistant/i18n/es.json +252 -0
  212. package/src/modules/ai_assistant/i18n/pl.json +252 -0
  213. package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
  214. package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
  215. package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
  216. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
  217. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
  218. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
  219. package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
  220. package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
  221. package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
  222. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
  223. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
  224. package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
  225. package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
  226. package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
  227. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
  228. package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
  229. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
  230. package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
  231. package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
  232. package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
  233. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
  234. package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
  235. package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
  236. package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
  237. package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
  238. package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
  239. package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
  240. package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
  241. package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
  242. package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
  243. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
  244. package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
  245. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
  246. package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
  247. package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
  248. package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
  249. package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
  250. package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
  251. package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
  252. package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
  253. package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
  254. package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
  255. package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
  256. package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
  257. package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
  258. package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
  259. package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
  260. package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
  261. package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
  262. package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
  263. package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
  264. package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
  265. package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
  266. package/src/modules/ai_assistant/lib/types.ts +105 -2
  267. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
  268. package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
  269. package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
  270. package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
  271. package/src/modules/ai_assistant/setup.ts +53 -0
  272. package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
  273. package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
@@ -0,0 +1,378 @@
1
+ import { z } from 'zod'
2
+ import type { AwilixContainer } from 'awilix'
3
+ import {
4
+ checkAgentAndFeatures,
5
+ checkAttachmentScope,
6
+ checkRecordVersion,
7
+ checkStatusAndExpiry,
8
+ checkToolWhitelist,
9
+ runPendingActionRechecks,
10
+ type PendingActionAuthContext,
11
+ } from '../pending-action-recheck'
12
+ import type { AiAgentDefinition } from '../ai-agent-definition'
13
+ import type { AiToolDefinition } from '../types'
14
+ import type { AiPendingAction } from '../../data/entities'
15
+
16
+ const findWithDecryptionMock = jest.fn()
17
+
18
+ jest.mock('@open-mercato/shared/lib/encryption/find', () => ({
19
+ findWithDecryption: (...args: unknown[]) => findWithDecryptionMock(...args),
20
+ findOneWithDecryption: jest.fn(),
21
+ }))
22
+
23
+ jest.mock('@open-mercato/core/modules/attachments/data/entities', () => ({
24
+ Attachment: class Attachment {},
25
+ AttachmentPartition: class AttachmentPartition {},
26
+ }))
27
+
28
+ function makeAction(overrides: Partial<AiPendingAction> = {}): AiPendingAction {
29
+ return {
30
+ id: 'pa_1',
31
+ tenantId: 'tenant-1',
32
+ organizationId: 'org-1',
33
+ agentId: 'catalog.merchandising_assistant',
34
+ toolName: 'catalog.update_product',
35
+ status: 'pending',
36
+ fieldDiff: [{ field: 'title', before: 'Old', after: 'New' }],
37
+ records: null,
38
+ failedRecords: null,
39
+ sideEffectsSummary: null,
40
+ recordVersion: 'v-1',
41
+ attachmentIds: [],
42
+ normalizedInput: { productId: 'p-1', patch: { title: 'New' } },
43
+ queueMode: 'inline',
44
+ executionResult: null,
45
+ targetEntityType: 'product',
46
+ targetRecordId: 'p-1',
47
+ conversationId: null,
48
+ idempotencyKey: 'idem_1',
49
+ createdByUserId: 'user-1',
50
+ createdAt: new Date('2026-04-18T10:00:00.000Z'),
51
+ expiresAt: new Date('2026-04-18T11:00:00.000Z'),
52
+ resolvedAt: null,
53
+ resolvedByUserId: null,
54
+ ...overrides,
55
+ } as unknown as AiPendingAction
56
+ }
57
+
58
+ function makeAgent(overrides: Partial<AiAgentDefinition> = {}): AiAgentDefinition {
59
+ return {
60
+ id: 'catalog.merchandising_assistant',
61
+ moduleId: 'catalog',
62
+ label: 'Catalog Agent',
63
+ description: '...',
64
+ systemPrompt: '...',
65
+ allowedTools: ['catalog.update_product'],
66
+ readOnly: false,
67
+ mutationPolicy: 'confirm-required',
68
+ ...overrides,
69
+ }
70
+ }
71
+
72
+ function makeTool(overrides: Partial<AiToolDefinition> = {}): AiToolDefinition {
73
+ return {
74
+ name: 'catalog.update_product',
75
+ description: 'Update a product',
76
+ inputSchema: z.object({
77
+ productId: z.string(),
78
+ patch: z.object({ title: z.string() }).partial(),
79
+ }),
80
+ handler: async () => ({ ok: true, recordId: 'p-1' }),
81
+ isMutation: true,
82
+ loadBeforeRecord: async () => ({
83
+ recordId: 'p-1',
84
+ entityType: 'catalog.product',
85
+ recordVersion: 'v-1',
86
+ before: { title: 'Old' },
87
+ }),
88
+ ...overrides,
89
+ } as AiToolDefinition
90
+ }
91
+
92
+ function makeCtx(overrides: Partial<PendingActionAuthContext> = {}): PendingActionAuthContext {
93
+ return {
94
+ tenantId: 'tenant-1',
95
+ organizationId: 'org-1',
96
+ userId: 'user-1',
97
+ userFeatures: ['ai_assistant.view', 'catalog.view', 'catalog.manage'],
98
+ isSuperAdmin: false,
99
+ container: {
100
+ resolve: (name: string) => {
101
+ if (name === 'em') return {}
102
+ throw new Error(`unknown dep ${name}`)
103
+ },
104
+ } as unknown as AwilixContainer,
105
+ em: {} as never,
106
+ ...overrides,
107
+ }
108
+ }
109
+
110
+ describe('pending-action-recheck guards', () => {
111
+ beforeEach(() => {
112
+ findWithDecryptionMock.mockReset()
113
+ })
114
+
115
+ describe('checkStatusAndExpiry', () => {
116
+ it('passes for pending + unexpired', () => {
117
+ const result = checkStatusAndExpiry(makeAction(), {
118
+ now: new Date('2026-04-18T10:30:00.000Z'),
119
+ })
120
+ expect(result.ok).toBe(true)
121
+ })
122
+
123
+ it('rejects cancelled with 409 invalid_status', () => {
124
+ const result = checkStatusAndExpiry(makeAction({ status: 'cancelled' as never }))
125
+ expect(result).toMatchObject({ ok: false, status: 409, code: 'invalid_status' })
126
+ })
127
+
128
+ it('rejects expired with 409 expired', () => {
129
+ const result = checkStatusAndExpiry(makeAction(), {
130
+ now: new Date('2026-04-18T12:00:00.000Z'),
131
+ })
132
+ expect(result).toMatchObject({ ok: false, status: 409, code: 'expired' })
133
+ })
134
+ })
135
+
136
+ describe('checkAgentAndFeatures', () => {
137
+ it('passes when caller carries the agent features', () => {
138
+ const result = checkAgentAndFeatures(makeAgent({ requiredFeatures: ['catalog.view'] }), makeCtx())
139
+ expect(result.ok).toBe(true)
140
+ })
141
+
142
+ it('rejects unknown agent with 404', () => {
143
+ const result = checkAgentAndFeatures(null, makeCtx())
144
+ expect(result).toMatchObject({ ok: false, status: 404, code: 'agent_unknown' })
145
+ })
146
+
147
+ it('rejects missing feature with 403 agent_features_denied', () => {
148
+ const result = checkAgentAndFeatures(
149
+ makeAgent({ requiredFeatures: ['catalog.manage.full'] }),
150
+ makeCtx({ userFeatures: ['catalog.view'] }),
151
+ )
152
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'agent_features_denied' })
153
+ })
154
+ })
155
+
156
+ describe('checkToolWhitelist', () => {
157
+ it('passes for whitelisted mutation tool', () => {
158
+ const result = checkToolWhitelist(makeAgent(), makeTool(), makeAction())
159
+ expect(result.ok).toBe(true)
160
+ })
161
+
162
+ it('rejects missing tool with 403 tool_not_whitelisted', () => {
163
+ const result = checkToolWhitelist(makeAgent(), null, makeAction())
164
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'tool_not_whitelisted' })
165
+ })
166
+
167
+ it('rejects tool dropped from allowedTools with 403', () => {
168
+ const result = checkToolWhitelist(
169
+ makeAgent({ allowedTools: [] }),
170
+ makeTool(),
171
+ makeAction(),
172
+ )
173
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'tool_not_whitelisted' })
174
+ })
175
+
176
+ it('rejects non-mutation tool with 403 tool_not_whitelisted', () => {
177
+ const result = checkToolWhitelist(
178
+ makeAgent(),
179
+ makeTool({ isMutation: false }),
180
+ makeAction(),
181
+ )
182
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'tool_not_whitelisted' })
183
+ })
184
+
185
+ it('rejects read-only override with 403 read_only_agent', () => {
186
+ const result = checkToolWhitelist(makeAgent(), makeTool(), makeAction(), {
187
+ mutationPolicyOverride: 'read-only',
188
+ })
189
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'read_only_agent' })
190
+ })
191
+ })
192
+
193
+ describe('checkAttachmentScope', () => {
194
+ it('passes when attachmentIds is empty', async () => {
195
+ const result = await checkAttachmentScope(makeAction({ attachmentIds: [] }), makeCtx())
196
+ expect(result.ok).toBe(true)
197
+ expect(findWithDecryptionMock).not.toHaveBeenCalled()
198
+ })
199
+
200
+ it('passes when every attachment belongs to the caller tenant', async () => {
201
+ findWithDecryptionMock.mockResolvedValueOnce([
202
+ { id: 'a-1', tenantId: 'tenant-1', organizationId: 'org-1' },
203
+ { id: 'a-2', tenantId: 'tenant-1', organizationId: 'org-1' },
204
+ ])
205
+ const result = await checkAttachmentScope(
206
+ makeAction({ attachmentIds: ['a-1', 'a-2'] }),
207
+ makeCtx(),
208
+ )
209
+ expect(result.ok).toBe(true)
210
+ })
211
+
212
+ it('rejects cross-tenant attachment id with 403', async () => {
213
+ findWithDecryptionMock.mockResolvedValueOnce([
214
+ { id: 'a-1', tenantId: 'tenant-1', organizationId: 'org-1' },
215
+ { id: 'a-2', tenantId: 'tenant-2', organizationId: 'org-x' },
216
+ ])
217
+ const result = await checkAttachmentScope(
218
+ makeAction({ attachmentIds: ['a-1', 'a-2'] }),
219
+ makeCtx(),
220
+ )
221
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'attachment_cross_tenant' })
222
+ })
223
+
224
+ it('rejects when lookup returns fewer rows than requested ids', async () => {
225
+ findWithDecryptionMock.mockResolvedValueOnce([
226
+ { id: 'a-1', tenantId: 'tenant-1', organizationId: 'org-1' },
227
+ ])
228
+ const result = await checkAttachmentScope(
229
+ makeAction({ attachmentIds: ['a-1', 'a-2'] }),
230
+ makeCtx(),
231
+ )
232
+ expect(result).toMatchObject({ ok: false, status: 403, code: 'attachment_cross_tenant' })
233
+ })
234
+ })
235
+
236
+ describe('checkRecordVersion (single-record)', () => {
237
+ it('passes when stored version matches current', async () => {
238
+ const result = await checkRecordVersion(makeAction(), makeTool(), makeCtx())
239
+ expect(result.ok).toBe(true)
240
+ })
241
+
242
+ it('rejects mismatch with 412 stale_version', async () => {
243
+ const result = await checkRecordVersion(
244
+ makeAction({ recordVersion: 'v-1' } as never),
245
+ makeTool({
246
+ loadBeforeRecord: async () => ({
247
+ recordId: 'p-1',
248
+ entityType: 'catalog.product',
249
+ recordVersion: 'v-2',
250
+ before: { title: 'Old' },
251
+ }),
252
+ }),
253
+ makeCtx(),
254
+ )
255
+ expect(result).toMatchObject({ ok: false, status: 412, code: 'stale_version' })
256
+ })
257
+
258
+ it('rejects schema drift with 412 schema_drift', async () => {
259
+ const tool = makeTool({
260
+ inputSchema: z.object({ productId: z.string(), newTitle: z.string() }),
261
+ })
262
+ const result = await checkRecordVersion(
263
+ makeAction({ normalizedInput: { productId: 'p-1', patch: { title: 'x' } } } as never),
264
+ tool,
265
+ makeCtx(),
266
+ )
267
+ expect(result).toMatchObject({ ok: false, status: 412, code: 'schema_drift' })
268
+ })
269
+ })
270
+
271
+ describe('checkRecordVersion (batch)', () => {
272
+ function makeBatchAction() {
273
+ return makeAction({
274
+ records: [
275
+ {
276
+ recordId: 'r-1',
277
+ entityType: 'catalog.product',
278
+ label: 'P1',
279
+ fieldDiff: [],
280
+ recordVersion: 'v-1',
281
+ },
282
+ {
283
+ recordId: 'r-2',
284
+ entityType: 'catalog.product',
285
+ label: 'P2',
286
+ fieldDiff: [],
287
+ recordVersion: 'v-1',
288
+ },
289
+ {
290
+ recordId: 'r-3',
291
+ entityType: 'catalog.product',
292
+ label: 'P3',
293
+ fieldDiff: [],
294
+ recordVersion: 'v-1',
295
+ },
296
+ ],
297
+ normalizedInput: {
298
+ records: [
299
+ { recordId: 'r-1', patch: { title: 'a' } },
300
+ { recordId: 'r-2', patch: { title: 'b' } },
301
+ { recordId: 'r-3', patch: { title: 'c' } },
302
+ ],
303
+ },
304
+ } as never)
305
+ }
306
+
307
+ function makeBatchTool(currentVersions: Record<string, string>) {
308
+ return makeTool({
309
+ inputSchema: z.object({
310
+ records: z.array(
311
+ z.object({ recordId: z.string(), patch: z.object({ title: z.string() }).partial() }),
312
+ ),
313
+ }),
314
+ isBulk: true,
315
+ loadBeforeRecord: undefined,
316
+ loadBeforeRecords: async () =>
317
+ Object.entries(currentVersions).map(([recordId, recordVersion]) => ({
318
+ recordId,
319
+ entityType: 'catalog.product',
320
+ label: recordId,
321
+ recordVersion,
322
+ before: { title: 'X' },
323
+ })),
324
+ })
325
+ }
326
+
327
+ it('partial-stale: returns ok:true + failedRecords[] with only the stale ids', async () => {
328
+ const result = await checkRecordVersion(
329
+ makeBatchAction(),
330
+ makeBatchTool({ 'r-1': 'v-1', 'r-2': 'v-2', 'r-3': 'v-1' }),
331
+ makeCtx(),
332
+ )
333
+ expect(result.ok).toBe(true)
334
+ if (result.ok) {
335
+ expect(result.failedRecords).toEqual([
336
+ {
337
+ recordId: 'r-2',
338
+ error: { code: 'stale_version', message: 'Record version changed since preview.' },
339
+ },
340
+ ])
341
+ }
342
+ })
343
+
344
+ it('all stale: returns 412 stale_version', async () => {
345
+ const result = await checkRecordVersion(
346
+ makeBatchAction(),
347
+ makeBatchTool({ 'r-1': 'v-9', 'r-2': 'v-9', 'r-3': 'v-9' }),
348
+ makeCtx(),
349
+ )
350
+ expect(result).toMatchObject({ ok: false, status: 412, code: 'stale_version' })
351
+ })
352
+ })
353
+
354
+ describe('runPendingActionRechecks', () => {
355
+ it('runs every guard in order and returns ok when all pass', async () => {
356
+ findWithDecryptionMock.mockResolvedValueOnce([])
357
+ const result = await runPendingActionRechecks({
358
+ action: makeAction(),
359
+ agent: makeAgent(),
360
+ tool: makeTool(),
361
+ ctx: makeCtx(),
362
+ now: new Date('2026-04-18T10:30:00.000Z'),
363
+ })
364
+ expect(result.ok).toBe(true)
365
+ })
366
+
367
+ it('bubbles up first failure (expired before agent check)', async () => {
368
+ const result = await runPendingActionRechecks({
369
+ action: makeAction(),
370
+ agent: null,
371
+ tool: null,
372
+ ctx: makeCtx(),
373
+ now: new Date('2026-04-18T12:00:00.000Z'),
374
+ })
375
+ expect(result).toMatchObject({ ok: false, code: 'expired' })
376
+ })
377
+ })
378
+ })
@@ -0,0 +1,299 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ registerGeneratedAiToolEntries,
4
+ type AiToolConfigEntry,
5
+ } from '../tool-loader'
6
+ import { toolRegistry } from '../tool-registry'
7
+ import { convertMcpToolsToAiSdk } from '../mcp-tool-adapter'
8
+ import { defineAiTool } from '../ai-tool-definition'
9
+ import type {
10
+ AiToolDefinition,
11
+ McpToolDefinition,
12
+ } from '../types'
13
+ import type { InProcessMcpClient, ToolInfoWithSchema } from '../in-process-client'
14
+ import { createAiAgentsExtension } from '../../../../../../cli/src/lib/generators/extensions/ai-agents'
15
+
16
+ type PlainAiToolInput = { q: string }
17
+ type PlainAiToolOutput = { echo: string; name: string }
18
+
19
+ function makePlainAiTool(name: string): AiToolDefinition<PlainAiToolInput, PlainAiToolOutput> {
20
+ return {
21
+ name,
22
+ description: `Tool ${name}`,
23
+ inputSchema: z.object({ q: z.string() }),
24
+ requiredFeatures: [`${name}.view`],
25
+ handler: async (input) => ({ echo: input.q, name }),
26
+ }
27
+ }
28
+
29
+ function makeFakeAgentEntry(moduleId: string, agentCount: number) {
30
+ return {
31
+ moduleId,
32
+ agents: Array.from({ length: agentCount }, (_, index) => ({
33
+ id: `${moduleId}.agent_${index}`,
34
+ moduleId,
35
+ label: `${moduleId} agent ${index}`,
36
+ description: 'fixture',
37
+ systemPrompt: 'fixture',
38
+ allowedTools: [] as string[],
39
+ })),
40
+ }
41
+ }
42
+
43
+ function buildInProcessClient(result: unknown): {
44
+ client: InProcessMcpClient
45
+ callToolMock: jest.Mock
46
+ } {
47
+ const callToolMock = jest.fn(async () => ({ success: true, result }))
48
+ const client = { callTool: callToolMock } as unknown as InProcessMcpClient
49
+ return { client, callToolMock }
50
+ }
51
+
52
+ describe('Phase 0 — restored module-tool loading is additive — existing modules still register', () => {
53
+ beforeEach(() => {
54
+ toolRegistry.clear()
55
+ })
56
+
57
+ afterAll(() => {
58
+ toolRegistry.clear()
59
+ })
60
+
61
+ it('registers plain-object aiTools[] exports through the restored loader', () => {
62
+ const entries: AiToolConfigEntry[] = [
63
+ { moduleId: 'legacy_module', tools: [makePlainAiTool('legacy_search')] },
64
+ ]
65
+
66
+ const registered = registerGeneratedAiToolEntries(entries)
67
+
68
+ expect(registered).toBe(1)
69
+ expect(toolRegistry.getTool('legacy_search')).toBeDefined()
70
+ expect(toolRegistry.listToolsByModule('legacy_module')).toEqual(['legacy_search'])
71
+ })
72
+
73
+ it('resolves registered tools through the mcp-tool-adapter without shape loss', async () => {
74
+ const plainTool = makePlainAiTool('legacy_query')
75
+ registerGeneratedAiToolEntries([{ moduleId: 'legacy_module', tools: [plainTool] }])
76
+
77
+ const registered = toolRegistry.getTool('legacy_query')
78
+ expect(registered).toBeDefined()
79
+ if (!registered) return
80
+
81
+ const mcpTools: ToolInfoWithSchema[] = [
82
+ {
83
+ name: registered.name,
84
+ description: registered.description,
85
+ inputSchema: registered.inputSchema,
86
+ },
87
+ ]
88
+
89
+ const { client, callToolMock } = buildInProcessClient({ rows: 0 })
90
+ const aiSdkTools = convertMcpToolsToAiSdk(client, mcpTools)
91
+
92
+ expect(Object.keys(aiSdkTools)).toEqual(['legacy_query'])
93
+ const adapted = aiSdkTools.legacy_query as unknown as {
94
+ description: string
95
+ execute: (args: unknown) => Promise<string>
96
+ }
97
+ expect(adapted.description).toBe(registered.description)
98
+
99
+ await adapted.execute({ q: 'hello' })
100
+ expect(callToolMock).toHaveBeenCalledWith('legacy_query', { q: 'hello' })
101
+ })
102
+
103
+ it('is idempotent — re-running the loader does not duplicate registrations', () => {
104
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
105
+ const entries: AiToolConfigEntry[] = [
106
+ { moduleId: 'legacy_module', tools: [makePlainAiTool('legacy_query')] },
107
+ ]
108
+
109
+ registerGeneratedAiToolEntries(entries)
110
+ registerGeneratedAiToolEntries(entries)
111
+
112
+ expect(toolRegistry.listToolsByModule('legacy_module')).toEqual(['legacy_query'])
113
+ expect(toolRegistry.listToolNames()).toEqual(['legacy_query'])
114
+ warnSpy.mockRestore()
115
+ })
116
+
117
+ it('stays silent for modules without an ai-tools.ts export', () => {
118
+ const entries: AiToolConfigEntry[] = [
119
+ { moduleId: 'empty_module', tools: [] },
120
+ { moduleId: 'missing_module', tools: undefined as unknown as unknown[] },
121
+ ]
122
+
123
+ expect(() => registerGeneratedAiToolEntries(entries)).not.toThrow()
124
+ expect(toolRegistry.listToolNames()).toEqual([])
125
+ })
126
+ })
127
+
128
+ describe('Phase 0 — `defineAiTool()` return value is compatible with the plain-object shape', () => {
129
+ beforeEach(() => {
130
+ toolRegistry.clear()
131
+ })
132
+
133
+ afterAll(() => {
134
+ toolRegistry.clear()
135
+ })
136
+
137
+ it('builder output and plain-object definition are structurally equivalent on required fields', () => {
138
+ const schema = z.object({ id: z.string() })
139
+ const handler = async (input: { id: string }) => ({ ok: true, id: input.id })
140
+
141
+ const builderTool = defineAiTool({
142
+ name: 'catalog.touch',
143
+ description: 'Touch a catalog record',
144
+ inputSchema: schema,
145
+ handler,
146
+ })
147
+
148
+ const plainTool: AiToolDefinition<{ id: string }, { ok: boolean; id: string }> = {
149
+ name: 'catalog.touch',
150
+ description: 'Touch a catalog record',
151
+ inputSchema: schema,
152
+ handler,
153
+ }
154
+
155
+ expect(builderTool.name).toBe(plainTool.name)
156
+ expect(builderTool.description).toBe(plainTool.description)
157
+ expect(builderTool.inputSchema).toBe(plainTool.inputSchema)
158
+ expect(builderTool.handler).toBe(plainTool.handler)
159
+ expect(builderTool.requiredFeatures).toBeUndefined()
160
+ expect(plainTool.requiredFeatures).toBeUndefined()
161
+ expect(builderTool.displayName).toBeUndefined()
162
+ expect(builderTool.tags).toBeUndefined()
163
+ expect(builderTool.isMutation).toBeUndefined()
164
+ expect(builderTool.maxCallsPerTurn).toBeUndefined()
165
+ expect(builderTool.supportsAttachments).toBeUndefined()
166
+ })
167
+
168
+ it('builder output is assignable to both AiToolDefinition and McpToolDefinition', () => {
169
+ const built = defineAiTool({
170
+ name: 'meta.ping',
171
+ description: 'Ping',
172
+ inputSchema: z.object({}),
173
+ handler: async () => ({ pong: true }),
174
+ })
175
+ const asAi: AiToolDefinition = built
176
+ const asMcp: McpToolDefinition = built
177
+ expect(asAi.name).toBe('meta.ping')
178
+ expect(asMcp.name).toBe('meta.ping')
179
+ })
180
+
181
+ it('both shapes register through the same loader path and produce identical registry entries', () => {
182
+ const schema = z.object({ id: z.string() })
183
+ const handler = async () => ({ ok: true })
184
+
185
+ const builderTool = defineAiTool({
186
+ name: 'builder_tool',
187
+ description: 'Builder-built tool',
188
+ inputSchema: schema,
189
+ handler,
190
+ displayName: 'Builder tool',
191
+ tags: ['builder'],
192
+ isMutation: false,
193
+ maxCallsPerTurn: 3,
194
+ supportsAttachments: false,
195
+ })
196
+
197
+ const plainTool: AiToolDefinition = {
198
+ name: 'plain_tool',
199
+ description: 'Plain tool',
200
+ inputSchema: schema,
201
+ handler,
202
+ }
203
+
204
+ registerGeneratedAiToolEntries([
205
+ { moduleId: 'mixed_module', tools: [builderTool, plainTool] },
206
+ ])
207
+
208
+ expect(toolRegistry.listToolsByModule('mixed_module').sort()).toEqual([
209
+ 'builder_tool',
210
+ 'plain_tool',
211
+ ])
212
+ const registeredBuilder = toolRegistry.getTool('builder_tool')
213
+ const registeredPlain = toolRegistry.getTool('plain_tool')
214
+ expect(registeredBuilder?.name).toBe('builder_tool')
215
+ expect(registeredBuilder?.description).toBe('Builder-built tool')
216
+ expect(typeof registeredBuilder?.handler).toBe('function')
217
+ expect(registeredPlain?.name).toBe('plain_tool')
218
+ expect(registeredPlain?.description).toBe('Plain tool')
219
+ expect(typeof registeredPlain?.handler).toBe('function')
220
+ })
221
+ })
222
+
223
+ describe('Phase 0 — `ai-agents.generated.ts` discovery is additive — does not break `ai-tools.generated.ts` consumption', () => {
224
+ beforeEach(() => {
225
+ toolRegistry.clear()
226
+ })
227
+
228
+ afterAll(() => {
229
+ toolRegistry.clear()
230
+ })
231
+
232
+ it('loads tools and ignores agent entries when both are present', () => {
233
+ const toolEntries: AiToolConfigEntry[] = [
234
+ { moduleId: 'catalog', tools: [makePlainAiTool('catalog_search')] },
235
+ { moduleId: 'customers', tools: [makePlainAiTool('customers_search')] },
236
+ ]
237
+ const agentEntries = [
238
+ makeFakeAgentEntry('catalog', 1),
239
+ makeFakeAgentEntry('customers', 2),
240
+ ]
241
+
242
+ const toolsRegistered = registerGeneratedAiToolEntries(toolEntries)
243
+
244
+ expect(toolsRegistered).toBe(2)
245
+ expect(toolRegistry.listToolNames().sort()).toEqual([
246
+ 'catalog_search',
247
+ 'customers_search',
248
+ ])
249
+ for (const agentModule of agentEntries) {
250
+ for (const agent of agentModule.agents) {
251
+ expect(toolRegistry.getTool(agent.id)).toBeUndefined()
252
+ }
253
+ }
254
+ })
255
+
256
+ it('still loads tools when only legacy `aiToolConfigEntries` exists (pre-agents fixture)', () => {
257
+ const entries: AiToolConfigEntry[] = [
258
+ { moduleId: 'legacy_only', tools: [makePlainAiTool('legacy_only_tool')] },
259
+ ]
260
+
261
+ const registered = registerGeneratedAiToolEntries(entries)
262
+
263
+ expect(registered).toBe(1)
264
+ expect(toolRegistry.getTool('legacy_only_tool')).toBeDefined()
265
+ })
266
+
267
+ it('registers zero tools and does not throw when only agent entries exist', () => {
268
+ const registered = registerGeneratedAiToolEntries([])
269
+
270
+ expect(registered).toBe(0)
271
+ expect(toolRegistry.listToolNames()).toEqual([])
272
+ })
273
+ })
274
+
275
+ describe('Phase 0 — generator output is stable across runs', () => {
276
+ it('`createAiAgentsExtension()` produces byte-identical output across repeated runs', () => {
277
+ const firstExtension = createAiAgentsExtension()
278
+ const secondExtension = createAiAgentsExtension()
279
+
280
+ const firstOutput = firstExtension.generateOutput().get('ai-agents.generated.ts')
281
+ const secondOutput = secondExtension.generateOutput().get('ai-agents.generated.ts')
282
+
283
+ expect(firstOutput).toBeDefined()
284
+ expect(secondOutput).toBeDefined()
285
+ expect(firstOutput).toBe(secondOutput)
286
+ expect(firstOutput?.startsWith('// AUTO-GENERATED')).toBe(true)
287
+ expect(firstOutput).toContain('export const aiAgentConfigEntries')
288
+ expect(firstOutput).toContain('export const allAiAgents')
289
+ })
290
+
291
+ it('calling `generateOutput()` twice on the same factory instance produces identical text', () => {
292
+ const extension = createAiAgentsExtension()
293
+
294
+ const firstRun = extension.generateOutput().get('ai-agents.generated.ts')
295
+ const secondRun = extension.generateOutput().get('ai-agents.generated.ts')
296
+
297
+ expect(firstRun).toBe(secondRun)
298
+ })
299
+ })