@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,307 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
+ import type { EntityManager } from '@mikro-orm/postgresql'
8
+ import { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'
9
+ import { hasRequiredFeatures } from '../../../../../lib/auth'
10
+ import { AiAgentPromptOverrideRepository } from '../../../../../data/repositories/AiAgentPromptOverrideRepository'
11
+ import type { AiAgentPromptOverride } from '../../../../../data/entities'
12
+ import { findReservedKeys } from '../../../../../lib/prompt-override-merge'
13
+
14
+ const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/
15
+
16
+ const agentIdParamSchema = z.object({
17
+ agentId: z
18
+ .string()
19
+ .regex(agentIdPattern, 'agentId must match "<module>.<agent>" (lowercase, digits, underscores only)'),
20
+ })
21
+
22
+ const sectionsSchema = z.record(z.string(), z.string())
23
+
24
+ /**
25
+ * Accept both `sections` (Step 5.3 canonical shape) and the Step 4.5
26
+ * placeholder `overrides` key so UI callers that haven't redeployed still
27
+ * work. `sections` wins when both are present.
28
+ */
29
+ const promptOverrideRequestSchema = z
30
+ .object({
31
+ sections: sectionsSchema.optional(),
32
+ overrides: sectionsSchema.optional(),
33
+ notes: z.string().max(2000).optional(),
34
+ })
35
+ .refine((value) => value.sections !== undefined || value.overrides !== undefined, {
36
+ message: 'Body must include either `sections` or `overrides`.',
37
+ })
38
+
39
+ export type AiPromptOverrideRequest = z.infer<typeof promptOverrideRequestSchema>
40
+
41
+ const REQUIRED_FEATURE = 'ai_assistant.settings.manage'
42
+ const HISTORY_LIMIT = 10
43
+
44
+ export const openApi: OpenApiRouteDoc = {
45
+ tag: 'AI Assistant',
46
+ summary: 'Versioned additive prompt-overrides for an AI agent',
47
+ methods: {
48
+ GET: {
49
+ operationId: 'aiAssistantGetPromptOverride',
50
+ summary: 'Read the latest prompt-section override for an agent plus recent version history.',
51
+ description:
52
+ 'Returns `{ agentId, override, versions }` where `override` is the latest persisted ' +
53
+ 'row (or `null`) and `versions` is the newest-first history capped at 10 rows. ' +
54
+ 'Tenant-scoped; requires `ai_assistant.settings.manage`.',
55
+ responses: [
56
+ {
57
+ status: 200,
58
+ description: 'Latest override + recent version history.',
59
+ mediaType: 'application/json',
60
+ },
61
+ ],
62
+ errors: [
63
+ { status: 400, description: 'Invalid agent id.' },
64
+ { status: 401, description: 'Unauthenticated caller.' },
65
+ { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },
66
+ { status: 404, description: 'Unknown agent id.' },
67
+ ],
68
+ },
69
+ POST: {
70
+ operationId: 'aiAssistantSavePromptOverride',
71
+ summary: 'Save a new prompt-section override version for the agent.',
72
+ description:
73
+ 'Persists an additive `{ sections: Record<sectionId, text>, notes? }` override, allocating ' +
74
+ 'the next monotonic version for `(tenant, org, agent)`. Reserved policy keys ' +
75
+ '(`mutationPolicy`, `readOnly`, `allowedTools`, `acceptedMediaTypes`) are rejected with ' +
76
+ '400 / `reserved_key`. Requires `ai_assistant.settings.manage`.',
77
+ requestBody: {
78
+ contentType: 'application/json',
79
+ description: 'Body: `{ sections: Record<string, string>, notes?: string }`.',
80
+ schema: promptOverrideRequestSchema,
81
+ },
82
+ responses: [
83
+ {
84
+ status: 200,
85
+ description: 'Override persisted. Returns `{ ok: true, version, updatedAt }`.',
86
+ mediaType: 'application/json',
87
+ },
88
+ ],
89
+ errors: [
90
+ { status: 400, description: 'Invalid agent id, malformed body, or reserved policy key.' },
91
+ { status: 401, description: 'Unauthenticated caller.' },
92
+ { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },
93
+ { status: 404, description: 'Unknown agent id.' },
94
+ ],
95
+ },
96
+ },
97
+ }
98
+
99
+ export const metadata = {
100
+ requireAuth: true,
101
+ requireFeatures: [REQUIRED_FEATURE],
102
+ }
103
+
104
+ interface RouteContext {
105
+ params: Promise<{ agentId: string }>
106
+ }
107
+
108
+ function jsonError(
109
+ status: number,
110
+ message: string,
111
+ code: string,
112
+ extra?: Record<string, unknown>,
113
+ ): NextResponse {
114
+ return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })
115
+ }
116
+
117
+ interface ResolvedAuth {
118
+ tenantId: string | null
119
+ organizationId: string | null
120
+ userId: string
121
+ isSuperAdmin: boolean
122
+ features: string[]
123
+ container: Awaited<ReturnType<typeof createRequestContainer>>
124
+ rbacService: RbacService
125
+ }
126
+
127
+ async function resolveAuthOrRespond(
128
+ req: NextRequest,
129
+ ): Promise<ResolvedAuth | NextResponse> {
130
+ const auth = await getAuthFromRequest(req)
131
+ if (!auth) {
132
+ return jsonError(401, 'Unauthorized', 'unauthenticated')
133
+ }
134
+ const container = await createRequestContainer()
135
+ const rbacService = container.resolve<RbacService>('rbacService')
136
+ const acl = await rbacService.loadAcl(auth.sub, {
137
+ tenantId: auth.tenantId,
138
+ organizationId: auth.orgId,
139
+ })
140
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
141
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, 'forbidden')
142
+ }
143
+ return {
144
+ tenantId: auth.tenantId ?? null,
145
+ organizationId: auth.orgId ?? null,
146
+ userId: auth.sub,
147
+ isSuperAdmin: acl.isSuperAdmin,
148
+ features: acl.features,
149
+ container,
150
+ rbacService,
151
+ }
152
+ }
153
+
154
+ function serializeOverride(row: AiAgentPromptOverride) {
155
+ return {
156
+ id: row.id,
157
+ agentId: row.agentId,
158
+ version: row.version,
159
+ sections: row.sections ?? {},
160
+ notes: row.notes ?? null,
161
+ createdByUserId: row.createdByUserId ?? null,
162
+ createdAt: row.createdAt?.toISOString?.() ?? new Date().toISOString(),
163
+ updatedAt: row.updatedAt?.toISOString?.() ?? new Date().toISOString(),
164
+ }
165
+ }
166
+
167
+ export async function GET(req: NextRequest, context: RouteContext): Promise<Response> {
168
+ const authResult = await resolveAuthOrRespond(req)
169
+ if (authResult instanceof NextResponse) return authResult
170
+
171
+ const rawParams = await context.params
172
+ const paramResult = agentIdParamSchema.safeParse(rawParams)
173
+ if (!paramResult.success) {
174
+ return jsonError(400, 'Invalid agent id.', 'validation_error', {
175
+ issues: paramResult.error.issues,
176
+ })
177
+ }
178
+
179
+ try {
180
+ await loadAgentRegistry()
181
+ const agent = getAgent(paramResult.data.agentId)
182
+ if (!agent) {
183
+ return jsonError(404, `Unknown agent "${paramResult.data.agentId}".`, 'agent_unknown')
184
+ }
185
+
186
+ if (!authResult.tenantId) {
187
+ return NextResponse.json({
188
+ agentId: agent.id,
189
+ override: null,
190
+ versions: [],
191
+ })
192
+ }
193
+
194
+ const em = authResult.container.resolve<EntityManager>('em')
195
+ const repo = new AiAgentPromptOverrideRepository(em)
196
+ const [latest, versions] = await Promise.all([
197
+ repo.getLatest(agent.id, {
198
+ tenantId: authResult.tenantId,
199
+ organizationId: authResult.organizationId,
200
+ }),
201
+ repo.listVersions(
202
+ agent.id,
203
+ {
204
+ tenantId: authResult.tenantId,
205
+ organizationId: authResult.organizationId,
206
+ },
207
+ HISTORY_LIMIT,
208
+ ),
209
+ ])
210
+
211
+ return NextResponse.json({
212
+ agentId: agent.id,
213
+ override: latest ? serializeOverride(latest) : null,
214
+ versions: versions.map(serializeOverride),
215
+ })
216
+ } catch (error) {
217
+ console.error('[AI Prompt Override GET] Failure:', error)
218
+ return jsonError(
219
+ 500,
220
+ error instanceof Error ? error.message : 'Failed to load prompt override.',
221
+ 'internal_error',
222
+ )
223
+ }
224
+ }
225
+
226
+ export async function POST(req: NextRequest, context: RouteContext): Promise<Response> {
227
+ const authResult = await resolveAuthOrRespond(req)
228
+ if (authResult instanceof NextResponse) return authResult
229
+
230
+ const rawParams = await context.params
231
+ const paramResult = agentIdParamSchema.safeParse(rawParams)
232
+ if (!paramResult.success) {
233
+ return jsonError(400, 'Invalid agent id.', 'validation_error', {
234
+ issues: paramResult.error.issues,
235
+ })
236
+ }
237
+
238
+ let parsedBody: unknown
239
+ try {
240
+ parsedBody = await req.json()
241
+ } catch {
242
+ return jsonError(400, 'Request body must be valid JSON.', 'validation_error')
243
+ }
244
+
245
+ const bodyResult = promptOverrideRequestSchema.safeParse(parsedBody)
246
+ if (!bodyResult.success) {
247
+ return jsonError(400, 'Invalid request body.', 'validation_error', {
248
+ issues: bodyResult.error.issues,
249
+ })
250
+ }
251
+
252
+ const sections = bodyResult.data.sections ?? bodyResult.data.overrides ?? {}
253
+ const reservedHits = findReservedKeys(sections)
254
+ if (reservedHits.length > 0) {
255
+ return jsonError(
256
+ 400,
257
+ `Prompt override contains reserved policy keys: ${reservedHits.join(', ')}.`,
258
+ 'reserved_key',
259
+ { reservedKeys: reservedHits },
260
+ )
261
+ }
262
+
263
+ try {
264
+ await loadAgentRegistry()
265
+ const agent = getAgent(paramResult.data.agentId)
266
+ if (!agent) {
267
+ return jsonError(404, `Unknown agent "${paramResult.data.agentId}".`, 'agent_unknown')
268
+ }
269
+
270
+ if (!authResult.tenantId) {
271
+ return jsonError(
272
+ 400,
273
+ 'Caller has no tenant context; cannot persist tenant-scoped prompt override.',
274
+ 'tenant_required',
275
+ )
276
+ }
277
+
278
+ const em = authResult.container.resolve<EntityManager>('em')
279
+ const repo = new AiAgentPromptOverrideRepository(em)
280
+ const saved = await repo.save(
281
+ {
282
+ agentId: agent.id,
283
+ sections,
284
+ notes: bodyResult.data.notes ?? null,
285
+ },
286
+ {
287
+ tenantId: authResult.tenantId,
288
+ organizationId: authResult.organizationId,
289
+ userId: authResult.userId,
290
+ },
291
+ )
292
+
293
+ return NextResponse.json({
294
+ ok: true,
295
+ agentId: agent.id,
296
+ version: saved.version,
297
+ updatedAt: saved.updatedAt?.toISOString?.() ?? new Date().toISOString(),
298
+ })
299
+ } catch (error) {
300
+ console.error('[AI Prompt Override POST] Failure:', error)
301
+ return jsonError(
302
+ 500,
303
+ error instanceof Error ? error.message : 'Failed to save prompt override.',
304
+ 'internal_error',
305
+ )
306
+ }
307
+ }
@@ -0,0 +1,107 @@
1
+ import { NextResponse, type NextRequest } from 'next/server'
2
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
6
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
7
+ import { listAgents, loadAgentRegistry } from '../../../lib/agent-registry'
8
+ import { hasRequiredFeatures } from '../../../lib/auth'
9
+ import { toolRegistry } from '../../../lib/tool-registry'
10
+ import type { AiToolDefinition } from '../../../lib/types'
11
+
12
+ export const openApi: OpenApiRouteDoc = {
13
+ tag: 'AI Assistant',
14
+ summary: 'List AI agents the caller can invoke',
15
+ methods: {
16
+ GET: {
17
+ operationId: 'aiAssistantListAgents',
18
+ summary: 'List registered AI agents, filtered by the caller\'s features.',
19
+ description:
20
+ 'Returns `{ agents: [...] }` — the subset of agents from `ai-agents.generated.ts` that the ' +
21
+ 'authenticated caller can invoke based on each agent\'s `requiredFeatures`. Mirrors the ' +
22
+ '`meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an ' +
23
+ 'agent picker without going through the MCP tool transport.',
24
+ responses: [
25
+ {
26
+ status: 200,
27
+ description: 'Accessible agent summaries.',
28
+ mediaType: 'application/json',
29
+ },
30
+ ],
31
+ errors: [
32
+ { status: 401, description: 'Unauthenticated caller.' },
33
+ { status: 500, description: 'Internal failure while loading the agent registry.' },
34
+ ],
35
+ },
36
+ },
37
+ }
38
+
39
+ export const metadata = {
40
+ GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
41
+ }
42
+
43
+ export async function GET(req: NextRequest) {
44
+ const auth = await getAuthFromRequest(req)
45
+ if (!auth) {
46
+ return NextResponse.json({ error: 'Unauthorized', code: 'unauthenticated' }, { status: 401 })
47
+ }
48
+
49
+ try {
50
+ const container = await createRequestContainer()
51
+ const rbacService = container.resolve<RbacService>('rbacService')
52
+ const acl = await rbacService.loadAcl(auth.sub, {
53
+ tenantId: auth.tenantId,
54
+ organizationId: auth.orgId,
55
+ })
56
+
57
+ // No LLM provider configured (no API keys set). The launcher uses the
58
+ // `aiConfigured` flag to show a setup prompt; explicit `<AiChat>` mounts
59
+ // and playground pages still see the full registry so they can show their
60
+ // own configuration prompts instead of silently disappearing.
61
+ const aiConfigured = llmProviderRegistry.resolveFirstConfigured() != null
62
+
63
+ await loadAgentRegistry()
64
+ const all = listAgents()
65
+ const accessible = all.filter((agent) =>
66
+ hasRequiredFeatures(agent.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService),
67
+ )
68
+
69
+ const agents = accessible.map((agent) => {
70
+ const tools = agent.allowedTools.map((toolName) => {
71
+ const tool = toolRegistry.getTool(toolName) as AiToolDefinition | undefined
72
+ return {
73
+ name: toolName,
74
+ displayName: tool?.displayName ?? toolName,
75
+ isMutation: Boolean(tool?.isMutation),
76
+ registered: Boolean(tool),
77
+ }
78
+ })
79
+ return {
80
+ id: agent.id,
81
+ moduleId: agent.moduleId,
82
+ label: agent.label,
83
+ description: agent.description,
84
+ systemPrompt: agent.systemPrompt,
85
+ executionMode: agent.executionMode ?? 'chat',
86
+ mutationPolicy: agent.mutationPolicy ?? 'read-only',
87
+ readOnly: Boolean(agent.readOnly),
88
+ maxSteps: agent.maxSteps ?? null,
89
+ allowedTools: agent.allowedTools,
90
+ tools,
91
+ requiredFeatures: agent.requiredFeatures ?? [],
92
+ acceptedMediaTypes: agent.acceptedMediaTypes ?? [],
93
+ keywords: agent.keywords ?? [],
94
+ suggestions: agent.suggestions ?? [],
95
+ hasOutputSchema: Boolean(agent.output),
96
+ }
97
+ })
98
+
99
+ return NextResponse.json({ agents, total: agents.length, aiConfigured })
100
+ } catch (error) {
101
+ console.error('[AI Agents] Failed to list agents:', error)
102
+ return NextResponse.json(
103
+ { error: 'Failed to list agents', code: 'internal_error' },
104
+ { status: 500 },
105
+ )
106
+ }
107
+ }
@@ -0,0 +1,282 @@
1
+ import { z } from 'zod'
2
+ import type { AiAgentDefinition } from '../../../../lib/ai-agent-definition'
3
+ import type { AiToolDefinition } from '../../../../lib/types'
4
+ import {
5
+ resetAgentRegistryForTests,
6
+ seedAgentRegistryForTests,
7
+ } from '../../../../lib/agent-registry'
8
+ import { toolRegistry, registerMcpTool } from '../../../../lib/tool-registry'
9
+
10
+ const authMock = jest.fn()
11
+ const loadAclMock = jest.fn()
12
+ const createRequestContainerMock = jest.fn()
13
+ const runAiAgentTextMock = jest.fn()
14
+
15
+ jest.mock('@open-mercato/shared/lib/auth/server', () => ({
16
+ getAuthFromRequest: (...args: unknown[]) => authMock(...args),
17
+ }))
18
+
19
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
20
+ createRequestContainer: (...args: unknown[]) => createRequestContainerMock(...args),
21
+ }))
22
+
23
+ jest.mock('../../../../lib/agent-runtime', () => ({
24
+ runAiAgentText: (...args: unknown[]) => runAiAgentTextMock(...args),
25
+ }))
26
+
27
+ import { POST } from '../route'
28
+
29
+ function makeAgent(
30
+ overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>,
31
+ ): AiAgentDefinition {
32
+ return {
33
+ label: `${overrides.id} label`,
34
+ description: `${overrides.id} description`,
35
+ systemPrompt: 'You are a test agent.',
36
+ allowedTools: [],
37
+ ...overrides,
38
+ }
39
+ }
40
+
41
+ function makeTool(
42
+ overrides: Partial<AiToolDefinition> & Pick<AiToolDefinition, 'name'>,
43
+ ): AiToolDefinition {
44
+ return {
45
+ description: `${overrides.name} description`,
46
+ inputSchema: z.object({}),
47
+ handler: async () => ({ ok: true }),
48
+ ...overrides,
49
+ }
50
+ }
51
+
52
+ function buildRequest(options: {
53
+ agent?: string | null
54
+ body?: unknown
55
+ bodyRaw?: string
56
+ }): Request {
57
+ const url = new URL('http://localhost/api/ai/chat')
58
+ if (options.agent !== undefined && options.agent !== null) {
59
+ url.searchParams.set('agent', options.agent)
60
+ }
61
+ const init: RequestInit = { method: 'POST' }
62
+ if (options.bodyRaw !== undefined) {
63
+ init.body = options.bodyRaw
64
+ } else if (options.body !== undefined) {
65
+ init.body = JSON.stringify(options.body)
66
+ init.headers = { 'content-type': 'application/json' }
67
+ }
68
+ return new Request(url, init)
69
+ }
70
+
71
+ describe('POST /api/ai/chat', () => {
72
+ let consoleErrorSpy: jest.SpyInstance
73
+
74
+ beforeEach(() => {
75
+ jest.clearAllMocks()
76
+ resetAgentRegistryForTests()
77
+ toolRegistry.clear()
78
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
79
+ authMock.mockResolvedValue({
80
+ sub: 'user-1',
81
+ tenantId: 'tenant-1',
82
+ orgId: 'org-1',
83
+ })
84
+ loadAclMock.mockResolvedValue({ features: ['ai_assistant.view'], isSuperAdmin: false })
85
+ createRequestContainerMock.mockResolvedValue({
86
+ resolve: (name: string) => {
87
+ if (name === 'rbacService') return { loadAcl: loadAclMock }
88
+ return null
89
+ },
90
+ })
91
+ runAiAgentTextMock.mockResolvedValue(
92
+ new Response('data: {"type":"text","content":"ok"}\n\ndata: [DONE]\n\n', {
93
+ status: 200,
94
+ headers: { 'Content-Type': 'text/event-stream' },
95
+ }),
96
+ )
97
+ })
98
+
99
+ afterEach(() => {
100
+ consoleErrorSpy.mockRestore()
101
+ })
102
+
103
+ afterAll(() => {
104
+ resetAgentRegistryForTests()
105
+ toolRegistry.clear()
106
+ })
107
+
108
+ it('returns 401 when unauthenticated', async () => {
109
+ authMock.mockResolvedValueOnce(null)
110
+
111
+ const response = await POST(buildRequest({ agent: 'customers.assistant', body: { messages: [{ role: 'user', content: 'hi' }] } }) as any)
112
+
113
+ expect(response.status).toBe(401)
114
+ const json = await response.json()
115
+ expect(json.code).toBe('unauthenticated')
116
+ })
117
+
118
+ it('returns 400 when the agent query param is missing', async () => {
119
+ const response = await POST(buildRequest({ body: { messages: [{ role: 'user', content: 'hi' }] } }) as any)
120
+
121
+ expect(response.status).toBe(400)
122
+ const json = await response.json()
123
+ expect(json.code).toBe('validation_error')
124
+ })
125
+
126
+ it('returns 400 when the agent query param is malformed', async () => {
127
+ const response = await POST(buildRequest({ agent: 'BadAgent', body: { messages: [{ role: 'user', content: 'hi' }] } }) as any)
128
+
129
+ expect(response.status).toBe(400)
130
+ const json = await response.json()
131
+ expect(json.code).toBe('validation_error')
132
+ })
133
+
134
+ it('returns 400 when body fails zod validation (missing messages)', async () => {
135
+ seedAgentRegistryForTests([
136
+ makeAgent({ id: 'customers.assistant', moduleId: 'customers' }),
137
+ ])
138
+
139
+ const response = await POST(buildRequest({ agent: 'customers.assistant', body: {} }) as any)
140
+
141
+ expect(response.status).toBe(400)
142
+ const json = await response.json()
143
+ expect(json.code).toBe('validation_error')
144
+ })
145
+
146
+ it('returns 400 when messages exceed the cap', async () => {
147
+ seedAgentRegistryForTests([
148
+ makeAgent({ id: 'customers.assistant', moduleId: 'customers' }),
149
+ ])
150
+
151
+ const messages = Array.from({ length: 101 }, (_, index) => ({
152
+ role: 'user' as const,
153
+ content: `msg-${index}`,
154
+ }))
155
+
156
+ const response = await POST(buildRequest({ agent: 'customers.assistant', body: { messages } }) as any)
157
+
158
+ expect(response.status).toBe(400)
159
+ const json = await response.json()
160
+ expect(json.code).toBe('validation_error')
161
+ })
162
+
163
+ it('returns 404 for an unknown agent', async () => {
164
+ // registry intentionally empty
165
+ const response = await POST(
166
+ buildRequest({ agent: 'customers.missing', body: { messages: [{ role: 'user', content: 'hi' }] } }) as any,
167
+ )
168
+
169
+ expect(response.status).toBe(404)
170
+ const json = await response.json()
171
+ expect(json.code).toBe('agent_unknown')
172
+ })
173
+
174
+ it('returns 403 when the agent requires features the user lacks', async () => {
175
+ seedAgentRegistryForTests([
176
+ makeAgent({
177
+ id: 'customers.assistant',
178
+ moduleId: 'customers',
179
+ requiredFeatures: ['customers.assistant.use'],
180
+ }),
181
+ ])
182
+ loadAclMock.mockResolvedValueOnce({ features: ['ai_assistant.view'], isSuperAdmin: false })
183
+
184
+ const response = await POST(
185
+ buildRequest({
186
+ agent: 'customers.assistant',
187
+ body: { messages: [{ role: 'user', content: 'hi' }] },
188
+ }) as any,
189
+ )
190
+
191
+ expect(response.status).toBe(403)
192
+ const json = await response.json()
193
+ expect(json.code).toBe('agent_features_denied')
194
+ })
195
+
196
+ it('returns 409 when an object-mode agent is invoked via chat transport', async () => {
197
+ seedAgentRegistryForTests([
198
+ makeAgent({
199
+ id: 'catalog.extract',
200
+ moduleId: 'catalog',
201
+ executionMode: 'object',
202
+ output: { schema: z.object({ title: z.string() }) },
203
+ }),
204
+ ])
205
+
206
+ const response = await POST(
207
+ buildRequest({
208
+ agent: 'catalog.extract',
209
+ body: { messages: [{ role: 'user', content: 'hi' }] },
210
+ }) as any,
211
+ )
212
+
213
+ expect(response.status).toBe(409)
214
+ const json = await response.json()
215
+ expect(json.code).toBe('execution_mode_not_supported')
216
+ })
217
+
218
+ it('delegates to runAiAgentText with the resolved auth and body payload', async () => {
219
+ registerMcpTool(
220
+ makeTool({ name: 'customers.list_people', requiredFeatures: ['customers.people.view'] }),
221
+ { moduleId: 'customers' },
222
+ )
223
+ seedAgentRegistryForTests([
224
+ makeAgent({
225
+ id: 'customers.assistant',
226
+ moduleId: 'customers',
227
+ allowedTools: ['customers.list_people'],
228
+ }),
229
+ ])
230
+
231
+ const response = await POST(
232
+ buildRequest({
233
+ agent: 'customers.assistant',
234
+ body: {
235
+ messages: [{ role: 'user', content: 'Hello assistant' }],
236
+ debug: true,
237
+ pageContext: { pageId: 'customers.people' },
238
+ },
239
+ }) as any,
240
+ )
241
+
242
+ expect(response.status).toBe(200)
243
+ expect(response.headers.get('content-type')).toContain('text/event-stream')
244
+ expect(runAiAgentTextMock).toHaveBeenCalledTimes(1)
245
+ const callArg = runAiAgentTextMock.mock.calls[0][0] as {
246
+ agentId: string
247
+ messages: unknown
248
+ debug?: boolean
249
+ pageContext?: { pageId?: string }
250
+ authContext: { tenantId: string | null; organizationId: string | null; userId: string }
251
+ container: unknown
252
+ }
253
+ expect(callArg.agentId).toBe('customers.assistant')
254
+ expect(callArg.debug).toBe(true)
255
+ expect(callArg.pageContext).toEqual({ pageId: 'customers.people' })
256
+ expect(callArg.authContext.userId).toBe('user-1')
257
+ expect(callArg.authContext.tenantId).toBe('tenant-1')
258
+ expect(callArg.authContext.organizationId).toBe('org-1')
259
+ expect(callArg.container).toBeDefined()
260
+ })
261
+
262
+ it('maps AgentPolicyError thrown by the runtime to the canonical HTTP status', async () => {
263
+ const { AgentPolicyError } = await import('../../../../lib/agent-tools')
264
+ seedAgentRegistryForTests([
265
+ makeAgent({ id: 'customers.assistant', moduleId: 'customers' }),
266
+ ])
267
+ runAiAgentTextMock.mockRejectedValueOnce(
268
+ new AgentPolicyError('tool_not_whitelisted', 'Tool not whitelisted'),
269
+ )
270
+
271
+ const response = await POST(
272
+ buildRequest({
273
+ agent: 'customers.assistant',
274
+ body: { messages: [{ role: 'user', content: 'hi' }] },
275
+ }) as any,
276
+ )
277
+
278
+ expect(response.status).toBe(409)
279
+ const json = await response.json()
280
+ expect(json.code).toBe('tool_not_whitelisted')
281
+ })
282
+ })