@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,197 @@
1
+ import { AiAgentPromptOverrideRepository } from '../AiAgentPromptOverrideRepository'
2
+ import { AiAgentPromptOverride } from '../../entities'
3
+
4
+ type Row = {
5
+ id: string
6
+ tenantId: string
7
+ organizationId: string | null
8
+ agentId: string
9
+ version: number
10
+ sections: Record<string, string>
11
+ notes: string | null
12
+ createdByUserId: string | null
13
+ createdAt: Date
14
+ updatedAt: Date
15
+ }
16
+
17
+ let idCounter = 0
18
+
19
+ function mockEm() {
20
+ const store: Row[] = []
21
+
22
+ const find = async (_entity: unknown, where: any, options?: any): Promise<Row[]> => {
23
+ let rows = store.filter((row) => {
24
+ if (where?.agentId && row.agentId !== where.agentId) return false
25
+ if (where?.tenantId && row.tenantId !== where.tenantId) return false
26
+ // organizationId supports null filter equivalence.
27
+ if (where && 'organizationId' in where) {
28
+ const expected = where.organizationId ?? null
29
+ if ((row.organizationId ?? null) !== expected) return false
30
+ }
31
+ return true
32
+ })
33
+ const orderBy = options?.orderBy
34
+ if (orderBy?.version === 'desc') {
35
+ rows = [...rows].sort((a, b) => b.version - a.version)
36
+ } else if (orderBy?.version === 'asc') {
37
+ rows = [...rows].sort((a, b) => a.version - b.version)
38
+ }
39
+ if (typeof options?.limit === 'number') rows = rows.slice(0, options.limit)
40
+ return rows
41
+ }
42
+
43
+ const em: any = {
44
+ find,
45
+ findOne: async (_entity: unknown, where: any, options?: any) => {
46
+ const rows = await find(_entity, where, options)
47
+ return rows[0] ?? null
48
+ },
49
+ create: (_entity: unknown, data: any) => {
50
+ idCounter += 1
51
+ const row: Row = {
52
+ id: `row-${idCounter}`,
53
+ tenantId: data.tenantId,
54
+ organizationId: data.organizationId ?? null,
55
+ agentId: data.agentId,
56
+ version: data.version,
57
+ sections: data.sections,
58
+ notes: data.notes ?? null,
59
+ createdByUserId: data.createdByUserId ?? null,
60
+ createdAt: new Date(),
61
+ updatedAt: new Date(),
62
+ }
63
+ return row
64
+ },
65
+ persist: (row: Row) => {
66
+ em.__pendingPersist = row
67
+ return em
68
+ },
69
+ flush: async () => {
70
+ if (em.__pendingPersist) {
71
+ store.push(em.__pendingPersist as Row)
72
+ em.__pendingPersist = null
73
+ }
74
+ },
75
+ transactional: async (fn: (tx: any) => Promise<unknown>) => {
76
+ return fn(em)
77
+ },
78
+ __pendingPersist: null as Row | null,
79
+ __store: store,
80
+ }
81
+
82
+ return em
83
+ }
84
+
85
+ describe('AiAgentPromptOverrideRepository', () => {
86
+ it('allocates monotonic versions per (tenant, org, agent)', async () => {
87
+ const em = mockEm()
88
+ const repo = new AiAgentPromptOverrideRepository(em)
89
+ const ctx = { tenantId: 't1', organizationId: null }
90
+
91
+ const first = await repo.save(
92
+ { agentId: 'catalog.assistant', sections: { role: 'A' } },
93
+ ctx,
94
+ )
95
+ expect(first.version).toBe(1)
96
+
97
+ const second = await repo.save(
98
+ { agentId: 'catalog.assistant', sections: { role: 'B' } },
99
+ ctx,
100
+ )
101
+ expect(second.version).toBe(2)
102
+
103
+ // Different agent under same tenant starts at 1 again.
104
+ const otherAgent = await repo.save(
105
+ { agentId: 'customers.assistant', sections: { role: 'C' } },
106
+ ctx,
107
+ )
108
+ expect(otherAgent.version).toBe(1)
109
+ })
110
+
111
+ it('scopes per tenant — getLatest for a different tenant returns null', async () => {
112
+ const em = mockEm()
113
+ const repo = new AiAgentPromptOverrideRepository(em)
114
+
115
+ await repo.save(
116
+ { agentId: 'catalog.assistant', sections: { role: 'A' } },
117
+ { tenantId: 't1', organizationId: null },
118
+ )
119
+ const latestA = await repo.getLatest('catalog.assistant', {
120
+ tenantId: 't1',
121
+ organizationId: null,
122
+ })
123
+ const latestB = await repo.getLatest('catalog.assistant', {
124
+ tenantId: 't2',
125
+ organizationId: null,
126
+ })
127
+ expect(latestA?.version).toBe(1)
128
+ expect(latestB).toBeNull()
129
+ })
130
+
131
+ it('listVersions returns rows newest first and caps by limit', async () => {
132
+ const em = mockEm()
133
+ const repo = new AiAgentPromptOverrideRepository(em)
134
+ const ctx = { tenantId: 't1', organizationId: null }
135
+
136
+ for (let i = 0; i < 5; i += 1) {
137
+ await repo.save(
138
+ { agentId: 'catalog.assistant', sections: { role: `v${i}` } },
139
+ ctx,
140
+ )
141
+ }
142
+
143
+ const versions = await repo.listVersions('catalog.assistant', ctx, 3)
144
+ expect(versions.map((v) => v.version)).toEqual([5, 4, 3])
145
+ })
146
+
147
+ it('returns empty array when tenant/agent has no rows', async () => {
148
+ const em = mockEm()
149
+ const repo = new AiAgentPromptOverrideRepository(em)
150
+ const rows = await repo.listVersions('catalog.assistant', {
151
+ tenantId: 't1',
152
+ organizationId: null,
153
+ })
154
+ expect(rows).toEqual([])
155
+ })
156
+
157
+ it('drops empty/whitespace-only override values before persisting', async () => {
158
+ const em = mockEm()
159
+ const repo = new AiAgentPromptOverrideRepository(em)
160
+ const saved = await repo.save(
161
+ {
162
+ agentId: 'catalog.assistant',
163
+ sections: { role: 'kept', scope: ' ', data: '' },
164
+ },
165
+ { tenantId: 't1', organizationId: null },
166
+ )
167
+ expect(Object.keys(saved.sections)).toEqual(['role'])
168
+ })
169
+
170
+ it('throws when tenantId is missing on save', async () => {
171
+ const em = mockEm()
172
+ const repo = new AiAgentPromptOverrideRepository(em)
173
+ await expect(
174
+ repo.save(
175
+ { agentId: 'catalog.assistant', sections: { role: 'x' } },
176
+ { tenantId: '', organizationId: null } as any,
177
+ ),
178
+ ).rejects.toThrow(/tenantId/)
179
+ })
180
+
181
+ it('returns an AiAgentPromptOverride-shaped payload', async () => {
182
+ const em = mockEm()
183
+ const repo = new AiAgentPromptOverrideRepository(em)
184
+ const saved = await repo.save(
185
+ { agentId: 'catalog.assistant', sections: { role: 'x' }, notes: 'note' },
186
+ { tenantId: 't1', organizationId: 'o1', userId: 'u1' },
187
+ )
188
+ expect(saved.agentId).toBe('catalog.assistant')
189
+ expect(saved.version).toBe(1)
190
+ expect(saved.organizationId).toBe('o1')
191
+ expect(saved.createdByUserId).toBe('u1')
192
+ expect(saved.notes).toBe('note')
193
+ // Entity class reference intact (mock returns a plain object but the
194
+ // real repo path calls tx.create(AiAgentPromptOverride, data)).
195
+ void AiAgentPromptOverride
196
+ })
197
+ })
@@ -0,0 +1,357 @@
1
+ import { AiPendingActionRepository } from '../AiPendingActionRepository'
2
+ import { AiPendingAction } from '../../entities'
3
+ import {
4
+ AiPendingActionStateError,
5
+ type AiPendingActionStatus,
6
+ } from '../../../lib/pending-action-types'
7
+
8
+ type Row = {
9
+ id: string
10
+ tenantId: string
11
+ organizationId: string | null
12
+ agentId: string
13
+ toolName: string
14
+ conversationId: string | null
15
+ targetEntityType: string | null
16
+ targetRecordId: string | null
17
+ normalizedInput: Record<string, unknown>
18
+ fieldDiff: Array<{ field: string; before: unknown; after: unknown }>
19
+ records: Array<Record<string, unknown>> | null
20
+ failedRecords: Array<Record<string, unknown>> | null
21
+ sideEffectsSummary: string | null
22
+ recordVersion: string | null
23
+ attachmentIds: string[]
24
+ idempotencyKey: string
25
+ createdByUserId: string
26
+ status: AiPendingActionStatus
27
+ queueMode: 'inline' | 'stack'
28
+ executionResult: Record<string, unknown> | null
29
+ createdAt: Date
30
+ expiresAt: Date
31
+ resolvedAt: Date | null
32
+ resolvedByUserId: string | null
33
+ }
34
+
35
+ let idCounter = 0
36
+
37
+ function rowMatchesWhere(row: Row, where: any): boolean {
38
+ if (!where) return true
39
+ if (where.id && row.id !== where.id) return false
40
+ if (where.tenantId && row.tenantId !== where.tenantId) return false
41
+ if ('organizationId' in where) {
42
+ const expected = where.organizationId ?? null
43
+ if ((row.organizationId ?? null) !== expected) return false
44
+ }
45
+ if (where.agentId && row.agentId !== where.agentId) return false
46
+ if (where.idempotencyKey && row.idempotencyKey !== where.idempotencyKey) {
47
+ return false
48
+ }
49
+ if (where.status && row.status !== where.status) return false
50
+ if (where.expiresAt && typeof where.expiresAt === 'object') {
51
+ if ('$lt' in where.expiresAt) {
52
+ if (!(row.expiresAt.getTime() < (where.expiresAt.$lt as Date).getTime())) {
53
+ return false
54
+ }
55
+ }
56
+ }
57
+ return true
58
+ }
59
+
60
+ function applyOrder(rows: Row[], orderBy: any): Row[] {
61
+ if (!orderBy) return rows
62
+ if (orderBy.createdAt === 'desc') {
63
+ return [...rows].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
64
+ }
65
+ if (orderBy.createdAt === 'asc') {
66
+ return [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
67
+ }
68
+ if (orderBy.expiresAt === 'asc') {
69
+ return [...rows].sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime())
70
+ }
71
+ if (orderBy.expiresAt === 'desc') {
72
+ return [...rows].sort((a, b) => b.expiresAt.getTime() - a.expiresAt.getTime())
73
+ }
74
+ return rows
75
+ }
76
+
77
+ function mockEm() {
78
+ const store: Row[] = []
79
+
80
+ const find = async (_entity: unknown, where: any, options?: any): Promise<Row[]> => {
81
+ let rows = store.filter((row) => rowMatchesWhere(row, where))
82
+ rows = applyOrder(rows, options?.orderBy)
83
+ if (typeof options?.limit === 'number') rows = rows.slice(0, options.limit)
84
+ return rows
85
+ }
86
+
87
+ const em: any = {
88
+ find,
89
+ findOne: async (_entity: unknown, where: any, options?: any) => {
90
+ const rows = await find(_entity, where, options)
91
+ return rows[0] ?? null
92
+ },
93
+ create: (_entity: unknown, data: any) => {
94
+ idCounter += 1
95
+ const row: Row = {
96
+ id: `row-${idCounter}`,
97
+ tenantId: data.tenantId,
98
+ organizationId: data.organizationId ?? null,
99
+ agentId: data.agentId,
100
+ toolName: data.toolName,
101
+ conversationId: data.conversationId ?? null,
102
+ targetEntityType: data.targetEntityType ?? null,
103
+ targetRecordId: data.targetRecordId ?? null,
104
+ normalizedInput: data.normalizedInput ?? {},
105
+ fieldDiff: Array.isArray(data.fieldDiff) ? data.fieldDiff : [],
106
+ records: data.records ?? null,
107
+ failedRecords: data.failedRecords ?? null,
108
+ sideEffectsSummary: data.sideEffectsSummary ?? null,
109
+ recordVersion: data.recordVersion ?? null,
110
+ attachmentIds: Array.isArray(data.attachmentIds) ? data.attachmentIds : [],
111
+ idempotencyKey: data.idempotencyKey,
112
+ createdByUserId: data.createdByUserId,
113
+ status: data.status ?? 'pending',
114
+ queueMode: data.queueMode ?? 'inline',
115
+ executionResult: data.executionResult ?? null,
116
+ createdAt: data.createdAt instanceof Date ? data.createdAt : new Date(),
117
+ expiresAt: data.expiresAt instanceof Date ? data.expiresAt : new Date(),
118
+ resolvedAt: data.resolvedAt ?? null,
119
+ resolvedByUserId: data.resolvedByUserId ?? null,
120
+ }
121
+ return row
122
+ },
123
+ persist: (row: Row) => {
124
+ em.__pendingPersist = row
125
+ return em
126
+ },
127
+ remove: (row: Row) => {
128
+ em.__pendingRemove = row
129
+ return em
130
+ },
131
+ flush: async () => {
132
+ if (em.__pendingRemove) {
133
+ const row = em.__pendingRemove as Row
134
+ const idx = store.findIndex((candidate) => candidate.id === row.id)
135
+ if (idx >= 0) store.splice(idx, 1)
136
+ em.__pendingRemove = null
137
+ }
138
+ if (em.__pendingPersist) {
139
+ const row = em.__pendingPersist as Row
140
+ const idx = store.findIndex((candidate) => candidate.id === row.id)
141
+ if (idx >= 0) store[idx] = row
142
+ else store.push(row)
143
+ em.__pendingPersist = null
144
+ }
145
+ },
146
+ transactional: async (fn: (tx: any) => Promise<unknown>) => {
147
+ return fn(em)
148
+ },
149
+ __pendingPersist: null as Row | null,
150
+ __store: store,
151
+ }
152
+
153
+ return em
154
+ }
155
+
156
+ const tenantAlpha = 't-alpha'
157
+ const tenantBeta = 't-beta'
158
+
159
+ function baseInput(overrides: Partial<any> = {}) {
160
+ return {
161
+ agentId: 'catalog.merchandising_assistant',
162
+ toolName: 'catalog.products.update',
163
+ idempotencyKey: overrides.idempotencyKey ?? 'idem-1',
164
+ createdByUserId: 'u-1',
165
+ normalizedInput: { productId: 'p-1', patch: { name: 'new' } },
166
+ fieldDiff: [{ field: 'name', before: 'old', after: 'new' }],
167
+ targetEntityType: 'catalog.product',
168
+ targetRecordId: 'p-1',
169
+ recordVersion: 'v-1',
170
+ ...overrides,
171
+ }
172
+ }
173
+
174
+ describe('AiPendingActionRepository', () => {
175
+ it('creates a row in status=pending with TTL-derived expiresAt and empty attachmentIds default', async () => {
176
+ const em = mockEm()
177
+ const repo = new AiPendingActionRepository(em)
178
+ const now = new Date('2026-04-18T12:00:00.000Z')
179
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
180
+
181
+ const row = await repo.create(
182
+ baseInput({ now, ttlSeconds: 900 }),
183
+ ctx,
184
+ )
185
+
186
+ expect(row.status).toBe('pending')
187
+ expect(row.tenantId).toBe(tenantAlpha)
188
+ expect(row.attachmentIds).toEqual([])
189
+ expect(row.expiresAt.getTime()).toBe(now.getTime() + 900 * 1000)
190
+ expect(row.queueMode).toBe('inline')
191
+ expect(row.executionResult).toBeNull()
192
+ expect(row.resolvedAt).toBeNull()
193
+ expect(row.resolvedByUserId).toBeNull()
194
+ })
195
+
196
+ it('is idempotent: second create with same (tenant, org, idempotencyKey) returns the same row while pending', async () => {
197
+ const em = mockEm()
198
+ const repo = new AiPendingActionRepository(em)
199
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
200
+
201
+ const first = await repo.create(baseInput({ idempotencyKey: 'idem-42' }), ctx)
202
+ const second = await repo.create(
203
+ baseInput({
204
+ idempotencyKey: 'idem-42',
205
+ normalizedInput: { productId: 'p-1', patch: { name: 'different-call' } },
206
+ }),
207
+ ctx,
208
+ )
209
+
210
+ expect(second.id).toBe(first.id)
211
+ // the repo MUST NOT mutate the existing row from the second call
212
+ expect(second.normalizedInput).toEqual(first.normalizedInput)
213
+ expect(em.__store).toHaveLength(1)
214
+ })
215
+
216
+ it('after a terminal status, same idempotencyKey mints a NEW row (new id)', async () => {
217
+ const em = mockEm()
218
+ const repo = new AiPendingActionRepository(em)
219
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
220
+
221
+ const first = await repo.create(baseInput({ idempotencyKey: 'idem-9' }), ctx)
222
+ await repo.setStatus(first.id, 'cancelled', ctx, { resolvedByUserId: 'u-1' })
223
+
224
+ const second = await repo.create(baseInput({ idempotencyKey: 'idem-9' }), ctx)
225
+ expect(second.id).not.toBe(first.id)
226
+ expect(second.status).toBe('pending')
227
+ // The repo removes stale terminal rows (cancelled/failed/expired) before
228
+ // minting a fresh pending row so the unique-key constraint stays satisfied
229
+ // and the "Fix with AI" retry flow works. Store ends up with just the
230
+ // new pending row.
231
+ expect(em.__store).toHaveLength(1)
232
+ expect(em.__store[0].id).toBe(second.id)
233
+ })
234
+
235
+ it('setStatus rejects illegal transitions (e.g. confirmed → pending) with AiPendingActionStateError', async () => {
236
+ const em = mockEm()
237
+ const repo = new AiPendingActionRepository(em)
238
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
239
+
240
+ const row = await repo.create(baseInput({ idempotencyKey: 'idem-illegal' }), ctx)
241
+ await repo.setStatus(row.id, 'confirmed', ctx, { resolvedByUserId: 'u-1' })
242
+
243
+ await expect(
244
+ repo.setStatus(row.id, 'pending', ctx),
245
+ ).rejects.toBeInstanceOf(AiPendingActionStateError)
246
+ await expect(
247
+ repo.setStatus(row.id, 'cancelled', ctx),
248
+ ).rejects.toBeInstanceOf(AiPendingActionStateError)
249
+ })
250
+
251
+ it('setStatus to expired sets resolvedAt and resolvedByUserId: null', async () => {
252
+ const em = mockEm()
253
+ const repo = new AiPendingActionRepository(em)
254
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
255
+
256
+ const row = await repo.create(baseInput({ idempotencyKey: 'idem-expire' }), ctx)
257
+ const expiredAt = new Date('2026-04-18T13:00:00.000Z')
258
+ const expired = await repo.setStatus(row.id, 'expired', ctx, { now: expiredAt })
259
+
260
+ expect(expired.status).toBe('expired')
261
+ expect(expired.resolvedAt).toEqual(expiredAt)
262
+ expect(expired.resolvedByUserId).toBeNull()
263
+ })
264
+
265
+ it('listExpired returns rows with status=pending and expiresAt < now, capped by limit, tenant-isolated', async () => {
266
+ const em = mockEm()
267
+ const repo = new AiPendingActionRepository(em)
268
+ const ctxAlpha = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
269
+ const ctxBeta = { tenantId: tenantBeta, organizationId: null, userId: 'u-2' }
270
+
271
+ const baseNow = new Date('2026-04-18T12:00:00.000Z')
272
+ for (let i = 0; i < 4; i += 1) {
273
+ await repo.create(
274
+ baseInput({
275
+ idempotencyKey: `alpha-${i}`,
276
+ now: new Date(baseNow.getTime() + i * 1000),
277
+ ttlSeconds: 60,
278
+ }),
279
+ ctxAlpha,
280
+ )
281
+ }
282
+ // a beta-tenant row that is also expired (MUST NOT appear in alpha's listExpired)
283
+ await repo.create(
284
+ baseInput({
285
+ idempotencyKey: 'beta-0',
286
+ now: baseNow,
287
+ ttlSeconds: 60,
288
+ }),
289
+ ctxBeta,
290
+ )
291
+
292
+ // an alpha row that is still in the future
293
+ await repo.create(
294
+ baseInput({
295
+ idempotencyKey: 'alpha-future',
296
+ now: new Date(baseNow.getTime() + 3600 * 1000),
297
+ ttlSeconds: 3600,
298
+ }),
299
+ ctxAlpha,
300
+ )
301
+
302
+ const cleanupNow = new Date(baseNow.getTime() + 120 * 1000)
303
+ const alphaExpired = await repo.listExpired(ctxAlpha, cleanupNow, 2)
304
+ expect(alphaExpired).toHaveLength(2)
305
+ for (const row of alphaExpired) {
306
+ expect(row.tenantId).toBe(tenantAlpha)
307
+ expect(row.status).toBe('pending')
308
+ expect(row.expiresAt.getTime()).toBeLessThan(cleanupNow.getTime())
309
+ }
310
+
311
+ const betaExpired = await repo.listExpired(ctxBeta, cleanupNow, 10)
312
+ expect(betaExpired.map((r) => r.idempotencyKey)).toEqual(['beta-0'])
313
+ })
314
+
315
+ it('getById is tenant-scoped: another tenant returns null', async () => {
316
+ const em = mockEm()
317
+ const repo = new AiPendingActionRepository(em)
318
+ const ctxAlpha = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
319
+
320
+ const row = await repo.create(baseInput({ idempotencyKey: 'idem-iso' }), ctxAlpha)
321
+ const sameTenant = await repo.getById(row.id, ctxAlpha)
322
+ expect(sameTenant?.id).toBe(row.id)
323
+
324
+ const otherTenant = await repo.getById(row.id, {
325
+ tenantId: tenantBeta,
326
+ organizationId: null,
327
+ })
328
+ expect(otherTenant).toBeNull()
329
+
330
+ // sanity: the entity class is importable from both paths
331
+ void AiPendingAction
332
+ })
333
+
334
+ it('listPendingForAgent returns only pending rows for the requested agent and tenant', async () => {
335
+ const em = mockEm()
336
+ const repo = new AiPendingActionRepository(em)
337
+ const ctx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-1' }
338
+
339
+ const first = await repo.create(
340
+ baseInput({ idempotencyKey: 'p-1', agentId: 'catalog.assistant' }),
341
+ ctx,
342
+ )
343
+ await repo.create(
344
+ baseInput({ idempotencyKey: 'p-2', agentId: 'catalog.assistant' }),
345
+ ctx,
346
+ )
347
+ await repo.create(
348
+ baseInput({ idempotencyKey: 'p-3', agentId: 'customers.assistant' }),
349
+ ctx,
350
+ )
351
+ await repo.setStatus(first.id, 'cancelled', ctx, { resolvedByUserId: 'u-1' })
352
+
353
+ const pending = await repo.listPendingForAgent('catalog.assistant', ctx)
354
+ expect(pending).toHaveLength(1)
355
+ expect(pending[0].idempotencyKey).toBe('p-2')
356
+ })
357
+ })
@@ -0,0 +1,112 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ /**
4
+ * AI Assistant Module Events
5
+ *
6
+ * Typed declarations for the pending-action lifecycle events emitted by
7
+ * the Phase 3 WS-C mutation approval flow. The event IDs are FROZEN per
8
+ * `BACKWARD_COMPATIBILITY.md` §5 (contract surface #5) and MUST NOT be
9
+ * renamed; additive payload changes are allowed.
10
+ *
11
+ * - `ai.action.confirmed` — emitted by `executePendingActionConfirm`
12
+ * (Step 5.8) after the `pending → confirmed → executing → {confirmed|
13
+ * failed}` transition. The handler's outcome lives in
14
+ * `executionResult`; partial-stale rows carry the surviving stale
15
+ * records via `failedRecords`.
16
+ * - `ai.action.cancelled` — emitted by `executePendingActionCancel`
17
+ * (Step 5.9) after the atomic `pending → cancelled` transition.
18
+ * - `ai.action.expired` — emitted by the Step 5.9 expired short-circuit
19
+ * AND by the Step 5.12 cleanup worker when the TTL elapses. The
20
+ * worker is the actor in that path, so `resolvedByUserId` is NOT part
21
+ * of the payload.
22
+ */
23
+ const events = [
24
+ {
25
+ id: 'ai.action.confirmed',
26
+ label: 'AI Pending Action Confirmed',
27
+ entity: 'ai_pending_action',
28
+ category: 'system' as const,
29
+ },
30
+ {
31
+ id: 'ai.action.cancelled',
32
+ label: 'AI Pending Action Cancelled',
33
+ entity: 'ai_pending_action',
34
+ category: 'system' as const,
35
+ },
36
+ {
37
+ id: 'ai.action.expired',
38
+ label: 'AI Pending Action Expired',
39
+ entity: 'ai_pending_action',
40
+ category: 'system' as const,
41
+ },
42
+ ] as const
43
+
44
+ export const eventsConfig = createModuleEvents({
45
+ moduleId: 'ai_assistant',
46
+ events,
47
+ })
48
+
49
+ /** Type-safe event emitter for the ai_assistant module. */
50
+ export const emitAiAssistantEvent = eventsConfig.emit
51
+
52
+ /** Event IDs declared by the ai_assistant module. */
53
+ export type AiAssistantEventId = (typeof events)[number]['id']
54
+
55
+ /**
56
+ * Typed payload contracts for each ai_assistant event. Payloads are
57
+ * additive-only — extend existing fields rather than renaming/removing.
58
+ */
59
+ export interface AiActionFailedRecordPayload {
60
+ recordId: string
61
+ error: { code: string; message: string }
62
+ }
63
+
64
+ export interface AiActionExecutionResultPayload {
65
+ recordId?: string
66
+ commandName?: string
67
+ error?: { code: string; message: string }
68
+ }
69
+
70
+ export interface AiActionConfirmedPayload {
71
+ pendingActionId: string
72
+ agentId: string
73
+ toolName: string
74
+ status: string
75
+ tenantId: string | null
76
+ organizationId: string | null
77
+ userId: string
78
+ resolvedByUserId: string
79
+ resolvedAt: string
80
+ executionResult: AiActionExecutionResultPayload | null
81
+ failedRecords?: AiActionFailedRecordPayload[] | null
82
+ }
83
+
84
+ export interface AiActionCancelledPayload {
85
+ pendingActionId: string
86
+ agentId: string
87
+ toolName: string
88
+ status: string
89
+ tenantId: string | null
90
+ organizationId: string | null
91
+ userId: string
92
+ resolvedByUserId: string
93
+ resolvedAt: string
94
+ executionResult: AiActionExecutionResultPayload | null
95
+ reason?: string
96
+ }
97
+
98
+ export interface AiActionExpiredPayload {
99
+ pendingActionId: string
100
+ agentId: string
101
+ toolName: string
102
+ status: string
103
+ tenantId: string | null
104
+ organizationId: string | null
105
+ userId: string | null
106
+ resolvedByUserId: null
107
+ resolvedAt: string
108
+ expiresAt?: string
109
+ expiredAt?: string
110
+ }
111
+
112
+ export default eventsConfig