@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,596 @@
1
+ /**
2
+ * In-process AI tool test runner.
3
+ *
4
+ * Iterates every tool registered in `ai-tools.generated.ts`, invokes the
5
+ * handler against a super-admin tenant context, and returns a structured
6
+ * report. Used by the `mercato ai_assistant test-tools` CLI subcommand and
7
+ * by `.ai/qa/tests/integration/TC-INT-AI-TOOLS.spec.ts`.
8
+ *
9
+ * Safety posture:
10
+ * - No HTTP exposure — runs only inside the Node process driving the CLI.
11
+ * - Mutation tools are exercised through `prepareMutation` and we assert a
12
+ * pending-action envelope is returned. The pending-action row is created
13
+ * in `ai_pending_actions` but never confirmed, so no real write happens.
14
+ * - Tools without an explicit fixture entry are skipped with reason
15
+ * `'no fixture'` rather than failing.
16
+ */
17
+ import path from 'node:path'
18
+ import fs from 'node:fs'
19
+ import { fileURLToPath, pathToFileURL } from 'node:url'
20
+ import type { AwilixContainer } from 'awilix'
21
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
22
+ import type { McpToolContext, AiToolDefinition } from './types'
23
+ import type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'
24
+ import { getFixture, type ToolFixture } from './tool-test-fixtures'
25
+ import { prepareMutation } from './prepare-mutation'
26
+ import { executePendingActionConfirm } from './pending-action-executor'
27
+
28
+ export type ToolTestStatus = 'pass' | 'fail' | 'skip'
29
+
30
+ export interface ToolTestRecord {
31
+ module: string
32
+ tool: string
33
+ isMutation: boolean
34
+ status: ToolTestStatus
35
+ durationMs: number
36
+ reason?: string
37
+ resultPreview?: unknown
38
+ }
39
+
40
+ export interface ToolTestReport {
41
+ tenantId: string | null
42
+ organizationId: string | null
43
+ total: number
44
+ passed: number
45
+ failed: number
46
+ skipped: number
47
+ records: ToolTestRecord[]
48
+ }
49
+
50
+ interface RawAiToolsModule {
51
+ aiToolConfigEntriesRaw?: { moduleId: string; tools: unknown[] }[]
52
+ aiToolConfigEntries?: { moduleId: string; tools: unknown[] }[]
53
+ }
54
+
55
+ function isToolDefinition(value: unknown): value is AiToolDefinition {
56
+ if (!value || typeof value !== 'object') return false
57
+ const candidate = value as Record<string, unknown>
58
+ return (
59
+ typeof candidate.name === 'string' &&
60
+ typeof candidate.description === 'string' &&
61
+ candidate.inputSchema !== undefined &&
62
+ typeof candidate.handler === 'function'
63
+ )
64
+ }
65
+
66
+ /**
67
+ * Locate `apps/mercato/.mercato/generated/ai-tools.generated.ts` without
68
+ * hardcoding the workspace layout. Searches upward from this file's
69
+ * compiled location; in the monorepo dist this is
70
+ * `packages/ai-assistant/dist/...` so we walk up until we hit a directory
71
+ * containing `apps/mercato/.mercato/generated`.
72
+ */
73
+ function findGeneratedAiToolsPath(): string | null {
74
+ const here = (() => {
75
+ try {
76
+ return fileURLToPath(import.meta.url)
77
+ } catch {
78
+ return null
79
+ }
80
+ })()
81
+ if (!here) return null
82
+ let cursor = path.dirname(here)
83
+ for (let i = 0; i < 12; i++) {
84
+ const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')
85
+ if (fs.existsSync(candidate)) return candidate
86
+ const next = path.dirname(cursor)
87
+ if (next === cursor) break
88
+ cursor = next
89
+ }
90
+ // Fallback: cwd-based lookup (when CLI is invoked from apps/mercato).
91
+ const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', 'ai-tools.generated.ts')
92
+ if (fs.existsSync(fromCwd)) return fromCwd
93
+ const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', 'ai-tools.generated.ts')
94
+ if (fs.existsSync(fromCwdDirect)) return fromCwdDirect
95
+ return null
96
+ }
97
+
98
+ /**
99
+ * Compile-and-import `ai-tools.generated.ts` on the fly. Mirrors the approach
100
+ * used by `loadBootstrapData` in `@open-mercato/shared/lib/bootstrap/dynamicLoader`:
101
+ * resolves the `@/` alias to the app root, marks every other package import as
102
+ * external, and emits a sibling `.mjs` we can `import()` from Node. Cached on
103
+ * mtime so repeat runs in the same process don't recompile.
104
+ */
105
+ async function compileAndImportGenerated(tsPath: string): Promise<RawAiToolsModule> {
106
+ const jsPath = tsPath.replace(/\.ts$/, '.mjs')
107
+ // appRoot is two directories up from `.mercato/generated/<file>.ts`.
108
+ const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
109
+ const tsExists = fs.existsSync(tsPath)
110
+ if (!tsExists) {
111
+ throw new Error(`Generated file not found: ${tsPath}`)
112
+ }
113
+ const jsExists = fs.existsSync(jsPath)
114
+ const needsCompile =
115
+ !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs
116
+ if (needsCompile) {
117
+ const esbuild = await import('esbuild')
118
+ // Transpile-only: don't bundle. Generated registry files only declare an
119
+ // array literal whose entries are static `import("…")` arrow functions —
120
+ // we want those `import()` strings to stay as runtime imports so Node
121
+ // resolves them lazily through the workspace's normal module resolution.
122
+ // Eagerly bundling them pulls Next.js / route handler internals into the
123
+ // .mjs and breaks at runtime (e.g. `next/server` package-exports map).
124
+ const tsSource = fs.readFileSync(tsPath, 'utf-8')
125
+ // Rewrite `@/...` aliases to absolute paths so Node can resolve them.
126
+ const aliasRewritten = tsSource.replace(
127
+ /from\s+["']@\/([^"']+)["']/g,
128
+ (_match, p1: string) => {
129
+ const target = path.join(appRoot, p1)
130
+ const candidate = fs.existsSync(target)
131
+ ? target
132
+ : fs.existsSync(target + '.ts')
133
+ ? target + '.ts'
134
+ : target
135
+ return `from ${JSON.stringify(pathToFileURL(candidate).href)}`
136
+ },
137
+ ).replace(
138
+ /import\s*\(\s*["']@\/([^"']+)["']\s*\)/g,
139
+ (_match, p1: string) => {
140
+ const target = path.join(appRoot, p1)
141
+ const candidate = fs.existsSync(target)
142
+ ? target
143
+ : fs.existsSync(target + '.ts')
144
+ ? target + '.ts'
145
+ : target
146
+ return `import(${JSON.stringify(pathToFileURL(candidate).href)})`
147
+ },
148
+ )
149
+ const result = await esbuild.transform(aliasRewritten, {
150
+ loader: 'ts',
151
+ format: 'esm',
152
+ target: 'node18',
153
+ sourcemap: false,
154
+ sourcefile: tsPath,
155
+ })
156
+ fs.writeFileSync(jsPath, result.code)
157
+ }
158
+ return (await import(pathToFileURL(jsPath).href)) as RawAiToolsModule
159
+ }
160
+
161
+ /**
162
+ * Compile-and-import `api-routes.generated.ts` and register its manifest with
163
+ * the shared registry. Many tool handlers delegate to `aiApiOperationRunner`
164
+ * which fails closed when no manifest is registered. Idempotent: registering
165
+ * the same array twice is safe.
166
+ */
167
+ async function ensureApiRouteManifestsRegistered(generatedDir: string): Promise<void> {
168
+ const tsPath = path.join(generatedDir, 'api-routes.generated.ts')
169
+ if (!fs.existsSync(tsPath)) return
170
+ try {
171
+ const mod = (await compileAndImportGenerated(tsPath)) as Record<string, unknown>
172
+ const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes
173
+ if (!Array.isArray(apiRoutes)) {
174
+ console.warn('[tool-test-runner] api-routes.generated.mjs returned no apiRoutes array')
175
+ return
176
+ }
177
+ const registry = await import('@open-mercato/shared/modules/registry')
178
+ registry.registerApiRouteManifests(
179
+ apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],
180
+ )
181
+ if (process.env.OM_TOOL_TEST_DEBUG === '1') {
182
+ console.log(
183
+ `[tool-test-runner] Registered ${apiRoutes.length} api-route manifests; getApiRouteManifests().length=${registry.getApiRouteManifests().length}`,
184
+ )
185
+ }
186
+ } catch (error) {
187
+ console.warn(
188
+ '[tool-test-runner] Could not register api-routes manifest:',
189
+ error instanceof Error ? error.message : error,
190
+ )
191
+ }
192
+ }
193
+
194
+ async function loadGeneratedTools(): Promise<{ moduleId: string; tools: AiToolDefinition[] }[]> {
195
+ const tsPath = findGeneratedAiToolsPath()
196
+ if (!tsPath) {
197
+ throw new Error(
198
+ 'Could not locate apps/<app>/.mercato/generated/ai-tools.generated.ts. Run `yarn generate` first.',
199
+ )
200
+ }
201
+ await ensureApiRouteManifestsRegistered(path.dirname(tsPath))
202
+ const mod = await compileAndImportGenerated(tsPath)
203
+ const entries = mod.aiToolConfigEntriesRaw ?? mod.aiToolConfigEntries ?? []
204
+ const result: { moduleId: string; tools: AiToolDefinition[] }[] = []
205
+ for (const entry of entries) {
206
+ if (!entry || typeof entry.moduleId !== 'string') continue
207
+ const tools = Array.isArray(entry.tools)
208
+ ? entry.tools.filter(isToolDefinition)
209
+ : []
210
+ result.push({ moduleId: entry.moduleId, tools })
211
+ }
212
+ return result
213
+ }
214
+
215
+ async function pickDefaultTenant(
216
+ container: AwilixContainer,
217
+ ): Promise<{ tenantId: string; organizationId: string | null; userId: string | null } | null> {
218
+ try {
219
+ const em = container.resolve<{
220
+ getConnection: () => { execute: (sql: string) => Promise<unknown[]> }
221
+ }>('em') as unknown as {
222
+ getConnection: () => { execute: (sql: string) => Promise<Record<string, unknown>[]> }
223
+ }
224
+ const conn = em.getConnection()
225
+ const tenantRows = await conn.execute(
226
+ `SELECT id FROM tenants WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,
227
+ )
228
+ const tenantId =
229
+ Array.isArray(tenantRows) && tenantRows[0]
230
+ ? String((tenantRows[0] as Record<string, unknown>).id)
231
+ : null
232
+ if (!tenantId) return null
233
+ const escaped = tenantId.replace(/'/g, "''")
234
+ const orgRows = await conn.execute(
235
+ `SELECT id FROM organizations WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,
236
+ )
237
+ const organizationId =
238
+ Array.isArray(orgRows) && orgRows[0]
239
+ ? String((orgRows[0] as Record<string, unknown>).id)
240
+ : null
241
+ const userRows = await conn.execute(
242
+ `SELECT id FROM users WHERE tenant_id = '${escaped}' AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1`,
243
+ )
244
+ const userId =
245
+ Array.isArray(userRows) && userRows[0]
246
+ ? String((userRows[0] as Record<string, unknown>).id)
247
+ : null
248
+ return { tenantId, organizationId, userId }
249
+ } catch (error) {
250
+ if (process.env.OM_TOOL_TEST_DEBUG === '1') {
251
+ console.warn('[tool-test-runner] pickDefaultTenant failed:', error)
252
+ }
253
+ return null
254
+ }
255
+ }
256
+
257
+ function buildSuperAdminContext(
258
+ container: AwilixContainer,
259
+ tenantId: string | null,
260
+ organizationId: string | null,
261
+ userId: string | null,
262
+ ): McpToolContext {
263
+ return {
264
+ tenantId,
265
+ organizationId,
266
+ userId,
267
+ container,
268
+ userFeatures: ['*'],
269
+ isSuperAdmin: true,
270
+ }
271
+ }
272
+
273
+ function clipPreview(value: unknown): unknown {
274
+ try {
275
+ const json = JSON.stringify(value)
276
+ if (json.length <= 400) return value
277
+ return `${json.slice(0, 400)}…(truncated, ${json.length} bytes)`
278
+ } catch {
279
+ return '[unserializable]'
280
+ }
281
+ }
282
+
283
+ function dummyAgentForTool(tool: AiToolDefinition): AiAgentDefinition {
284
+ // The runner never registers this agent; it's only used as input to
285
+ // `prepareMutation` for shape compatibility. The id namespace is namespaced
286
+ // under `__test__` so it cannot collide with real agents.
287
+ const policy: AiAgentMutationPolicy = 'destructive-confirm-required'
288
+ return {
289
+ id: `__test__.${tool.name}`,
290
+ moduleId: tool.name.split('.')[0] ?? '__test__',
291
+ label: 'Tool Test Runner',
292
+ description: 'Synthetic agent used by the tool test runner',
293
+ systemPrompt: '',
294
+ allowedTools: [tool.name],
295
+ readOnly: false,
296
+ mutationPolicy: policy,
297
+ } as AiAgentDefinition
298
+ }
299
+
300
+ async function executeReadTool(
301
+ tool: AiToolDefinition,
302
+ args: Record<string, unknown>,
303
+ container: AwilixContainer,
304
+ ctx: McpToolContext,
305
+ ): Promise<unknown> {
306
+ // Mirror the dispatcher: resolve a fresh container per call so EM identity
307
+ // map state is clean between tools.
308
+ const fresh = await createRequestContainer()
309
+ const freshCtx: McpToolContext = { ...ctx, container: fresh, tool }
310
+ void container // keep arg for future use
311
+ return tool.handler(args as never, freshCtx)
312
+ }
313
+
314
+ async function executeMutationTool(
315
+ tool: AiToolDefinition,
316
+ args: Record<string, unknown>,
317
+ container: AwilixContainer,
318
+ ctx: McpToolContext,
319
+ ): Promise<unknown> {
320
+ // Route through prepareMutation so we assert the approval contract still
321
+ // holds — the handler must NEVER write directly. We pass a destructive
322
+ // policy so the runtime treats the call as a confirmation candidate and
323
+ // creates a pending-action row.
324
+ const fresh = await createRequestContainer()
325
+ const agent = dummyAgentForTool(tool)
326
+ const userId = ctx.userId ?? '00000000-0000-0000-0000-000000000000'
327
+ const { uiPart, pendingAction } = await prepareMutation(
328
+ {
329
+ agent,
330
+ tool,
331
+ toolCallArgs: args,
332
+ conversationId: null,
333
+ mutationPolicyOverride: null,
334
+ },
335
+ {
336
+ tenantId: ctx.tenantId,
337
+ organizationId: ctx.organizationId,
338
+ userId,
339
+ features: ctx.userFeatures,
340
+ isSuperAdmin: ctx.isSuperAdmin,
341
+ container: fresh,
342
+ },
343
+ )
344
+ // Exercise the actual handler via the same path the production confirm
345
+ // route uses. Without this, mutation tools whose handler crashes (e.g.
346
+ // missing `ctx.tool` for the API operation runner) would be reported as
347
+ // passing because `prepareMutation` never invokes the handler.
348
+ // `prepareMutation` already enforced `tenantId` is non-null above; cast
349
+ // here for the stricter `PendingActionExecuteContext` shape.
350
+ const confirmTenantId = ctx.tenantId as string
351
+ const confirmContainer = await createRequestContainer()
352
+ const confirmation = await executePendingActionConfirm({
353
+ action: pendingAction,
354
+ agent,
355
+ tool,
356
+ ctx: {
357
+ tenantId: confirmTenantId,
358
+ organizationId: ctx.organizationId,
359
+ userId,
360
+ container: confirmContainer,
361
+ userFeatures: ctx.userFeatures,
362
+ isSuperAdmin: ctx.isSuperAdmin,
363
+ },
364
+ emitEvent: async () => {},
365
+ })
366
+ if (!confirmation.ok) {
367
+ const error = (confirmation.executionResult as { error?: { message?: string } } | undefined)?.error
368
+ const cause = confirmation.cause
369
+ const causeMessage =
370
+ cause instanceof Error
371
+ ? cause.message
372
+ : typeof cause === 'string'
373
+ ? cause
374
+ : null
375
+ const message = error?.message ?? causeMessage ?? 'handler invocation failed'
376
+ throw new Error(message)
377
+ }
378
+ return {
379
+ status: 'pending-confirmation',
380
+ pendingActionId: pendingAction.id,
381
+ expiresAt: pendingAction.expiresAt.toISOString(),
382
+ uiPartType: (uiPart as { type?: string } | undefined)?.type ?? null,
383
+ handlerExecuted: true,
384
+ }
385
+ }
386
+
387
+ async function resolveFixtureInput(
388
+ fixture: ToolFixture,
389
+ resolved: Map<string, AiToolDefinition>,
390
+ container: AwilixContainer,
391
+ ctx: McpToolContext,
392
+ ): Promise<{ ok: true; input: Record<string, unknown> } | { ok: false; reason: string }> {
393
+ if (fixture.input) return { ok: true, input: { ...fixture.input } }
394
+ if (!fixture.idFrom) {
395
+ return { ok: false, reason: 'fixture has neither input nor idFrom' }
396
+ }
397
+ const sourceTool = resolved.get(fixture.idFrom)
398
+ if (!sourceTool) {
399
+ return { ok: false, reason: `idFrom tool not found: ${fixture.idFrom}` }
400
+ }
401
+ const sourceFixture = getFixture(fixture.idFrom)
402
+ if (!sourceFixture?.input) {
403
+ return {
404
+ ok: false,
405
+ reason: `idFrom source ${fixture.idFrom} has no static input fixture`,
406
+ }
407
+ }
408
+ let listResult: unknown
409
+ try {
410
+ listResult = await executeReadTool(
411
+ sourceTool,
412
+ { ...sourceFixture.input },
413
+ container,
414
+ ctx,
415
+ )
416
+ } catch (error) {
417
+ return {
418
+ ok: false,
419
+ reason: `idFrom source ${fixture.idFrom} threw: ${
420
+ error instanceof Error ? error.message : String(error)
421
+ }`,
422
+ }
423
+ }
424
+ const records = extractRecords(listResult)
425
+ if (!records.length) {
426
+ return {
427
+ ok: false,
428
+ reason: `idFrom source ${fixture.idFrom} returned no records`,
429
+ }
430
+ }
431
+ const idField = 'id'
432
+ const id = (records[0] as Record<string, unknown>)[idField]
433
+ if (typeof id !== 'string' || !id) {
434
+ return {
435
+ ok: false,
436
+ reason: `idFrom source ${fixture.idFrom} first record has no string id`,
437
+ }
438
+ }
439
+ const bindAs = fixture.bindAs ?? 'id'
440
+ return {
441
+ ok: true,
442
+ input: { [bindAs]: id, ...(fixture.extra ?? {}) },
443
+ }
444
+ }
445
+
446
+ function extractRecords(value: unknown): unknown[] {
447
+ if (!value || typeof value !== 'object') return []
448
+ const candidate = value as Record<string, unknown>
449
+ for (const key of ['records', 'items', 'people', 'companies', 'deals', 'results', 'data']) {
450
+ const arr = candidate[key]
451
+ if (Array.isArray(arr)) return arr
452
+ }
453
+ return []
454
+ }
455
+
456
+ export interface RunToolTestsOptions {
457
+ tenantId?: string | null
458
+ organizationId?: string | null
459
+ userId?: string | null
460
+ moduleFilter?: string | null
461
+ includeMutations?: boolean
462
+ }
463
+
464
+ export async function runToolTests(
465
+ options: RunToolTestsOptions = {},
466
+ ): Promise<ToolTestReport> {
467
+ const includeMutations = options.includeMutations ?? true
468
+ const container = await createRequestContainer()
469
+ let tenantId: string | null = options.tenantId ?? null
470
+ let organizationId: string | null = options.organizationId ?? null
471
+ let userId: string | null = options.userId ?? null
472
+ if (!tenantId) {
473
+ const picked = await pickDefaultTenant(container)
474
+ if (picked) {
475
+ tenantId = picked.tenantId
476
+ organizationId = picked.organizationId
477
+ userId = userId ?? picked.userId
478
+ }
479
+ }
480
+ const ctx = buildSuperAdminContext(container, tenantId, organizationId, userId)
481
+ const grouped = await loadGeneratedTools()
482
+
483
+ const flat: { moduleId: string; tool: AiToolDefinition }[] = []
484
+ for (const group of grouped) {
485
+ if (options.moduleFilter && group.moduleId !== options.moduleFilter) continue
486
+ for (const tool of group.tools) flat.push({ moduleId: group.moduleId, tool })
487
+ }
488
+ const resolved = new Map<string, AiToolDefinition>()
489
+ for (const { tool } of flat) resolved.set(tool.name, tool)
490
+
491
+ const records: ToolTestRecord[] = []
492
+ for (const { moduleId, tool } of flat) {
493
+ const start = Date.now()
494
+ const isMutation = tool.isMutation === true
495
+ const fixture = getFixture(tool.name)
496
+ if (!fixture) {
497
+ records.push({
498
+ module: moduleId,
499
+ tool: tool.name,
500
+ isMutation,
501
+ status: 'skip',
502
+ durationMs: Date.now() - start,
503
+ reason: 'no fixture',
504
+ })
505
+ continue
506
+ }
507
+ if (fixture.skip) {
508
+ records.push({
509
+ module: moduleId,
510
+ tool: tool.name,
511
+ isMutation,
512
+ status: 'skip',
513
+ durationMs: Date.now() - start,
514
+ reason: fixture.note ?? 'skip-by-fixture',
515
+ })
516
+ continue
517
+ }
518
+ if (isMutation && !includeMutations) {
519
+ records.push({
520
+ module: moduleId,
521
+ tool: tool.name,
522
+ isMutation,
523
+ status: 'skip',
524
+ durationMs: Date.now() - start,
525
+ reason: 'mutations excluded',
526
+ })
527
+ continue
528
+ }
529
+ const inputResult = await resolveFixtureInput(fixture, resolved, container, ctx)
530
+ if (!inputResult.ok) {
531
+ records.push({
532
+ module: moduleId,
533
+ tool: tool.name,
534
+ isMutation,
535
+ status: 'skip',
536
+ durationMs: Date.now() - start,
537
+ reason: inputResult.reason,
538
+ })
539
+ continue
540
+ }
541
+ try {
542
+ const result = isMutation
543
+ ? await executeMutationTool(tool, inputResult.input, container, ctx)
544
+ : await executeReadTool(tool, inputResult.input, container, ctx)
545
+ // Result must be JSON-serializable.
546
+ JSON.stringify(result)
547
+ // Mutation tools must return a pending-confirmation envelope.
548
+ if (
549
+ isMutation &&
550
+ (typeof result !== 'object' ||
551
+ result === null ||
552
+ (result as Record<string, unknown>).status !== 'pending-confirmation')
553
+ ) {
554
+ records.push({
555
+ module: moduleId,
556
+ tool: tool.name,
557
+ isMutation,
558
+ status: 'fail',
559
+ durationMs: Date.now() - start,
560
+ reason: 'mutation tool did not route through prepareMutation (no pending-confirmation envelope)',
561
+ resultPreview: clipPreview(result),
562
+ })
563
+ continue
564
+ }
565
+ records.push({
566
+ module: moduleId,
567
+ tool: tool.name,
568
+ isMutation,
569
+ status: 'pass',
570
+ durationMs: Date.now() - start,
571
+ resultPreview: clipPreview(result),
572
+ })
573
+ } catch (error) {
574
+ records.push({
575
+ module: moduleId,
576
+ tool: tool.name,
577
+ isMutation,
578
+ status: 'fail',
579
+ durationMs: Date.now() - start,
580
+ reason: error instanceof Error ? error.message : String(error),
581
+ })
582
+ }
583
+ }
584
+ const passed = records.filter((r) => r.status === 'pass').length
585
+ const failed = records.filter((r) => r.status === 'fail').length
586
+ const skipped = records.filter((r) => r.status === 'skip').length
587
+ return {
588
+ tenantId,
589
+ organizationId,
590
+ total: records.length,
591
+ passed,
592
+ failed,
593
+ skipped,
594
+ records,
595
+ }
596
+ }