@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,751 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import type { LanguageModel, UIMessage } from 'ai'
4
+ import {
5
+ convertToModelMessages,
6
+ generateObject,
7
+ stepCountIs,
8
+ streamObject,
9
+ streamText,
10
+ } from 'ai'
11
+ import type { ZodTypeAny } from 'zod'
12
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
13
+ import type {
14
+ AiAgentDefinition,
15
+ AiAgentPageContextInput,
16
+ AiAgentStructuredOutput,
17
+ } from './ai-agent-definition'
18
+ import type {
19
+ AiChatRequestContext,
20
+ AiResolvedAttachmentPart,
21
+ } from './attachment-bridge-types'
22
+ import { resolveAiAgentTools, AgentPolicyError } from './agent-tools'
23
+ import { resolveEffectiveMutationPolicy } from './agent-policy'
24
+ import { toolRegistry } from './tool-registry'
25
+ import {
26
+ attachmentPartsToUiFileParts,
27
+ resolveAttachmentPartsForAgent,
28
+ summarizeAttachmentPartsForPrompt,
29
+ } from './attachment-parts'
30
+ import { AiAgentPromptOverrideRepository } from '../data/repositories/AiAgentPromptOverrideRepository'
31
+ import { AiAgentMutationPolicyOverrideRepository } from '../data/repositories/AiAgentMutationPolicyOverrideRepository'
32
+ import { composeSystemPromptWithOverride } from './prompt-override-merge'
33
+ import { isKnownMutationPolicy } from './agent-policy'
34
+ import type { AiAgentMutationPolicy } from './ai-agent-definition'
35
+
36
+ // Ensure built-in LLM providers are registered. Side-effect import; identical to
37
+ // what `./ai-sdk.ts` consumers already rely on.
38
+ import './llm-bootstrap'
39
+
40
+ export interface AgentRequestPageContext {
41
+ pageId?: string | null
42
+ entityType?: string | null
43
+ recordId?: string | null
44
+ [key: string]: unknown
45
+ }
46
+
47
+ export interface RunAiAgentTextInput {
48
+ agentId: string
49
+ messages: UIMessage[]
50
+ attachmentIds?: string[]
51
+ pageContext?: AgentRequestPageContext
52
+ debug?: boolean
53
+ /**
54
+ * Phase 1 exposes the caller-supplied auth context directly on the helper
55
+ * input. Phase 4 may wrap this behind a thinner public API once a global
56
+ * request-context resolver exists. Helpers running inside the HTTP
57
+ * dispatcher receive the same `AiChatRequestContext` used by `checkAgentPolicy`.
58
+ */
59
+ authContext: AiChatRequestContext
60
+ /**
61
+ * Optional per-call model id override that wins over `agent.defaultModel`.
62
+ * The production model-factory extraction lives in Step 5.1; this Step
63
+ * accepts a literal model id string so the Phase 1 runtime already honors
64
+ * `agent.defaultModel` without inventing a new indirection layer.
65
+ */
66
+ modelOverride?: string
67
+ /**
68
+ * Optional DI container used by `resolvePageContext` callbacks. When omitted
69
+ * and the agent declares a `resolvePageContext`, hydration is skipped with a
70
+ * warning (callbacks that need database/DI cannot run safely without one).
71
+ */
72
+ container?: AwilixContainer
73
+ /**
74
+ * Optional stable chat-turn conversation id forwarded from `<AiChat>`.
75
+ * Bridged into the Step 5.6 `prepareMutation` idempotency hash so repeated
76
+ * turns within the same chat collapse onto the same pending action. When
77
+ * omitted, the idempotency hash falls back to `null` which still preserves
78
+ * per-tenant/org uniqueness within the TTL window.
79
+ */
80
+ conversationId?: string | null
81
+ }
82
+
83
+ interface ResolvedAgentModel {
84
+ model: LanguageModel
85
+ modelId: string
86
+ providerId: string
87
+ }
88
+
89
+ function resolveAgentModel(
90
+ agent: AiAgentDefinition,
91
+ modelOverride: string | undefined,
92
+ ): ResolvedAgentModel {
93
+ const provider = llmProviderRegistry.resolveFirstConfigured()
94
+ if (!provider) {
95
+ throw new Error(
96
+ 'No LLM provider is configured. Set OPENCODE_PROVIDER plus a matching API key such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',
97
+ )
98
+ }
99
+ const apiKey = provider.resolveApiKey()
100
+ if (!apiKey) {
101
+ throw new Error(
102
+ `LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`,
103
+ )
104
+ }
105
+ const modelId =
106
+ (modelOverride && modelOverride.trim().length > 0 ? modelOverride : undefined) ??
107
+ agent.defaultModel ??
108
+ provider.defaultModel
109
+ const model = provider.createModel({ modelId, apiKey }) as LanguageModel
110
+ return { model, modelId, providerId: provider.id }
111
+ }
112
+
113
+ /**
114
+ * Composes the effective system prompt for a run. When the agent declares a
115
+ * `resolvePageContext` callback AND the incoming request carries both
116
+ * `entityType` and `recordId`, the callback is invoked and its return value
117
+ * is appended to `agent.systemPrompt`. Throwing callbacks are caught and
118
+ * logged without failing the request — the spec allows hydration to be
119
+ * best-effort until Step 5.2 wires a stricter contract.
120
+ */
121
+ export async function composeSystemPrompt(
122
+ agent: AiAgentDefinition,
123
+ pageContext: AgentRequestPageContext | undefined,
124
+ container: AwilixContainer | undefined,
125
+ tenantId: string | null,
126
+ organizationId: string | null,
127
+ ): Promise<string> {
128
+ const baseFromOverride = await resolveBaseSystemPromptWithOverride(
129
+ agent,
130
+ container,
131
+ tenantId,
132
+ organizationId,
133
+ )
134
+ const resolve = agent.resolvePageContext
135
+ if (!resolve) return baseFromOverride
136
+ const entityType = pageContext?.entityType
137
+ const recordId = pageContext?.recordId
138
+ if (typeof entityType !== 'string' || entityType.length === 0) return baseFromOverride
139
+ if (typeof recordId !== 'string' || recordId.length === 0) return baseFromOverride
140
+ if (!container) {
141
+ console.warn(
142
+ `[AI Agents] Agent "${agent.id}" declares resolvePageContext but no container was passed to runAiAgentText; skipping hydration.`,
143
+ )
144
+ return baseFromOverride
145
+ }
146
+ const hydrationInput: AiAgentPageContextInput = {
147
+ entityType,
148
+ recordId,
149
+ container,
150
+ tenantId,
151
+ organizationId,
152
+ }
153
+ try {
154
+ const hydrated = await resolve(hydrationInput)
155
+ if (typeof hydrated === 'string' && hydrated.trim().length > 0) {
156
+ return `${baseFromOverride}\n\n${hydrated}`
157
+ }
158
+ } catch (error) {
159
+ console.error(
160
+ `[AI Agents] resolvePageContext for agent "${agent.id}" failed; continuing without hydration:`,
161
+ error,
162
+ )
163
+ }
164
+ return baseFromOverride
165
+ }
166
+
167
+ /**
168
+ * Fetches the latest tenant-scoped prompt override for `agent` (if any) and
169
+ * layers it onto the built-in `systemPrompt` via the additive merge helper.
170
+ *
171
+ * BC + fail-open: every failure mode — missing container, missing `em`
172
+ * registration, repository throw, missing migration — is logged at `warn`
173
+ * and falls back to `agent.systemPrompt`. A chat turn MUST never fail on
174
+ * override lookup (per Step 5.3 spec: "If the repo call throws, log and
175
+ * fall back to the built-in prompt — never fail the chat request").
176
+ */
177
+ async function resolveBaseSystemPromptWithOverride(
178
+ agent: AiAgentDefinition,
179
+ container: AwilixContainer | undefined,
180
+ tenantId: string | null,
181
+ organizationId: string | null,
182
+ ): Promise<string> {
183
+ const base = agent.systemPrompt
184
+ if (!tenantId || !container) return base
185
+ let em: EntityManager | null = null
186
+ try {
187
+ em = container.resolve<EntityManager>('em')
188
+ } catch {
189
+ em = null
190
+ }
191
+ if (!em) return base
192
+ try {
193
+ const repo = new AiAgentPromptOverrideRepository(em)
194
+ const latest = await repo.getLatest(agent.id, {
195
+ tenantId,
196
+ organizationId: organizationId ?? null,
197
+ })
198
+ if (!latest || !latest.sections || Object.keys(latest.sections).length === 0) {
199
+ return base
200
+ }
201
+ return composeSystemPromptWithOverride(base, { sections: latest.sections })
202
+ } catch (error) {
203
+ console.warn(
204
+ `[AI Agents] Prompt-override lookup failed for agent "${agent.id}"; falling back to built-in prompt.`,
205
+ error,
206
+ )
207
+ return base
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Looks up the tenant-scoped `mutationPolicy` override for `agentId` (Step
213
+ * 5.4). Fails SAFE: any repo error, missing container, missing `em`
214
+ * registration, or corrupt enum value returns `null`, which causes the
215
+ * runtime to fall back to the agent's code-declared policy. A chat turn
216
+ * MUST never fail on override lookup.
217
+ */
218
+ async function resolveMutationPolicyOverride(
219
+ agentId: string,
220
+ container: AwilixContainer | undefined,
221
+ tenantId: string | null,
222
+ organizationId: string | null,
223
+ ): Promise<AiAgentMutationPolicy | null> {
224
+ if (!tenantId || !container) return null
225
+ let em: EntityManager | null = null
226
+ try {
227
+ em = container.resolve<EntityManager>('em')
228
+ } catch {
229
+ em = null
230
+ }
231
+ if (!em) return null
232
+ try {
233
+ const repo = new AiAgentMutationPolicyOverrideRepository(em)
234
+ const row = await repo.get(agentId, { tenantId, organizationId: organizationId ?? null })
235
+ if (!row) return null
236
+ const raw = row.mutationPolicy
237
+ if (!isKnownMutationPolicy(raw)) {
238
+ console.warn(
239
+ `[AI Agents] Ignoring corrupt mutationPolicy override row for agent "${agentId}": "${raw}". Falling back to code-declared policy.`,
240
+ )
241
+ return null
242
+ }
243
+ return raw
244
+ } catch (error) {
245
+ console.warn(
246
+ `[AI Agents] mutationPolicy override lookup failed for agent "${agentId}"; falling back to code-declared policy.`,
247
+ error,
248
+ )
249
+ return null
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Normalizes simple `{ role, content }` chat messages into the AI SDK
255
+ * `UIMessage` shape that `convertToModelMessages` requires. When the
256
+ * incoming message already carries a `parts` array it is left untouched;
257
+ * otherwise a single `TextUIPart` is synthesized from `content`.
258
+ */
259
+ function ensureUiMessageShape(messages: UIMessage[]): UIMessage[] {
260
+ return messages.map((message, index) => {
261
+ const raw = message as unknown as { id?: string; role?: string; content?: string; parts?: unknown[] }
262
+ if (Array.isArray(raw.parts) && raw.parts.length > 0) {
263
+ // Already has parts — only ensure `id` is present
264
+ return { ...message, id: raw.id ?? `msg-${index}` } as UIMessage
265
+ }
266
+ const textContent = typeof raw.content === 'string' ? raw.content : ''
267
+ return {
268
+ id: raw.id ?? `msg-${index}`,
269
+ role: raw.role ?? 'user',
270
+ parts: [{ type: 'text', text: textContent }],
271
+ } as unknown as UIMessage
272
+ })
273
+ }
274
+
275
+ /**
276
+ * Appends AI SDK v6 `FileUIPart` entries to the last user message in the
277
+ * request so resolved attachment bytes / signed URLs reach the model. Pure
278
+ * helper so chat-mode and object-mode share identical behavior — any
279
+ * divergence here breaks the Step 3.6 parity contract.
280
+ */
281
+ function attachAttachmentsToMessages(
282
+ messages: UIMessage[],
283
+ parts: readonly AiResolvedAttachmentPart[],
284
+ ): UIMessage[] {
285
+ if (parts.length === 0) return messages
286
+ const fileParts = attachmentPartsToUiFileParts(parts)
287
+ if (fileParts.length === 0) return messages
288
+ const next = messages.slice()
289
+ let lastUserIndex = -1
290
+ for (let index = next.length - 1; index >= 0; index -= 1) {
291
+ const candidate = next[index] as unknown as { role?: string }
292
+ if (candidate?.role === 'user') {
293
+ lastUserIndex = index
294
+ break
295
+ }
296
+ }
297
+ if (lastUserIndex === -1) {
298
+ next.push({
299
+ id: 'ai-runtime-attachments',
300
+ role: 'user',
301
+ parts: fileParts as unknown as UIMessage['parts'],
302
+ } as unknown as UIMessage)
303
+ return next
304
+ }
305
+ const source = next[lastUserIndex] as unknown as { parts?: unknown[] }
306
+ const existingParts = Array.isArray(source.parts) ? source.parts : []
307
+ next[lastUserIndex] = {
308
+ ...(next[lastUserIndex] as object),
309
+ parts: [...existingParts, ...fileParts],
310
+ } as UIMessage
311
+ return next
312
+ }
313
+
314
+ function appendAttachmentSummary(
315
+ systemPrompt: string,
316
+ parts: readonly AiResolvedAttachmentPart[],
317
+ ): string {
318
+ const summary = summarizeAttachmentPartsForPrompt(parts)
319
+ if (!summary) return systemPrompt
320
+ return `${systemPrompt}\n\n${summary}`
321
+ }
322
+
323
+ /**
324
+ * Builds a runtime "MUTATION POLICY (RUNTIME)" block describing the
325
+ * EFFECTIVE policy for this turn — what the model should expect when it
326
+ * calls each whitelisted mutation tool. Generated dynamically because:
327
+ *
328
+ * - the agent's static prompt cannot know which per-tenant override is
329
+ * in force (`destructive-confirm-required` flips most writes to
330
+ * run-direct) and would otherwise mislead the operator with stale
331
+ * "this requires approval" copy;
332
+ * - the per-tool `isDestructive` flag determines whether each
333
+ * whitelisted write goes through the approval card or runs inline.
334
+ *
335
+ * Without this block, the model parrots its hardcoded "always route
336
+ * through the approval card" prompt language and tells the user "your
337
+ * change is awaiting approval" when in fact the dispatcher already
338
+ * applied the change directly. The injected block flips the model to
339
+ * report results accurately ("applied", "pending your approval", or
340
+ * "blocked because read-only") tool-by-tool.
341
+ */
342
+ function buildRuntimeMutationPolicySection(
343
+ agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },
344
+ mutationPolicyOverride: string | null,
345
+ ): string | null {
346
+ const effective = resolveEffectiveMutationPolicy(
347
+ (agent.mutationPolicy ?? null) as never,
348
+ (mutationPolicyOverride ?? null) as never,
349
+ agent.id,
350
+ )
351
+ const lines: string[] = []
352
+ lines.push('MUTATION POLICY (RUNTIME)')
353
+ lines.push(`Declared agent policy: ${agent.mutationPolicy ?? 'read-only'}.`)
354
+ if (mutationPolicyOverride && mutationPolicyOverride !== agent.mutationPolicy) {
355
+ lines.push(`Tenant override active: ${mutationPolicyOverride}.`)
356
+ }
357
+ lines.push(`Effective policy: ${effective}.`)
358
+
359
+ // Bucket the agent's allowlisted tools into "gated" / "direct" / "conditional"
360
+ // / "blocked" so the model can phrase outcomes correctly per tool.
361
+ // `conditional` covers tools whose `isDestructive` is a predicate function:
362
+ // their gate-vs-direct decision depends on the per-call input (e.g.
363
+ // `customers.manage_deal_comment` gates only its delete branch under
364
+ // `destructive-confirm-required`).
365
+ const direct: string[] = []
366
+ const gated: string[] = []
367
+ const conditional: string[] = []
368
+ const blocked: string[] = []
369
+ for (const toolName of agent.allowedTools) {
370
+ const tool = toolRegistry.getTool(toolName) as
371
+ | { isMutation?: boolean; isDestructive?: boolean | ((input: unknown) => boolean) }
372
+ | undefined
373
+ if (!tool || tool.isMutation !== true) continue
374
+ if (effective === 'read-only') {
375
+ blocked.push(toolName)
376
+ continue
377
+ }
378
+ if (effective === 'confirm-required') {
379
+ gated.push(toolName)
380
+ continue
381
+ }
382
+ // destructive-confirm-required
383
+ if (typeof tool.isDestructive === 'function') {
384
+ conditional.push(toolName)
385
+ } else if (tool.isDestructive === true) {
386
+ gated.push(toolName)
387
+ } else {
388
+ direct.push(toolName)
389
+ }
390
+ }
391
+
392
+ if (
393
+ direct.length === 0 &&
394
+ gated.length === 0 &&
395
+ conditional.length === 0 &&
396
+ blocked.length === 0
397
+ ) {
398
+ // Read-only agent with no mutation tools — no runtime policy block needed.
399
+ return null
400
+ }
401
+ if (direct.length > 0) {
402
+ lines.push('')
403
+ lines.push(
404
+ `Tools that WILL RUN DIRECTLY (no approval card, no pending action) under the effective policy: ${direct.join(', ')}.`,
405
+ )
406
+ lines.push(
407
+ 'When you call any of these and the call returns successfully, the change has ALREADY BEEN APPLIED. Report it in the past tense ("Updated …", "Added …", "Created …"). Do NOT tell the operator the action is "pending your approval" or "awaiting confirmation" — that would be a false statement under the current policy.',
408
+ )
409
+ }
410
+ if (gated.length > 0) {
411
+ lines.push('')
412
+ lines.push(
413
+ `Tools that REQUIRE APPROVAL under the effective policy: ${gated.join(', ')}.`,
414
+ )
415
+ lines.push(
416
+ 'When you call any of these, the dispatcher returns an "awaiting confirmation" envelope and renders an inline approval card. Tell the operator the change is pending their confirmation; do NOT claim it has been applied.',
417
+ )
418
+ }
419
+ if (conditional.length > 0) {
420
+ lines.push('')
421
+ lines.push(
422
+ `Tools whose approval requirement DEPENDS ON THE INPUT under the effective policy: ${conditional.join(', ')}.`,
423
+ )
424
+ lines.push(
425
+ 'These multi-operation tools gate ONLY the destructive branches (typically `operation: "delete"` or similar). Read the tool result envelope: if it carries `status: "pending-confirmation"` then the change is pending — tell the operator it needs their approval. If it carries direct success data, the change has ALREADY BEEN APPLIED — report it in the past tense. Never assume one branch behaves like another.',
426
+ )
427
+ }
428
+ if (blocked.length > 0) {
429
+ lines.push('')
430
+ lines.push(
431
+ `Tools that are BLOCKED under the effective policy (read-only): ${blocked.join(', ')}.`,
432
+ )
433
+ lines.push(
434
+ 'Calls to these tools are refused before the handler runs. Do not attempt them; instead direct the operator to the matching backoffice page or to switch the tenant policy if they have permission.',
435
+ )
436
+ }
437
+ lines.push('')
438
+ lines.push(
439
+ 'This RUNTIME policy block always wins over any conflicting "approval card" language earlier in the prompt — the static prompt is written for the most restrictive case but real behavior depends on the per-call policy described here.',
440
+ )
441
+ return lines.join('\n')
442
+ }
443
+
444
+ function appendRuntimeMutationPolicy(
445
+ systemPrompt: string,
446
+ agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },
447
+ mutationPolicyOverride: string | null,
448
+ ): string {
449
+ const block = buildRuntimeMutationPolicySection(agent, mutationPolicyOverride)
450
+ if (!block) return systemPrompt
451
+ return `${systemPrompt}\n\n${block}`
452
+ }
453
+
454
+ /**
455
+ * Server-side helper that runs an Open Mercato agent in chat mode via the
456
+ * Vercel AI SDK and returns a streaming `Response` ready to be emitted from a
457
+ * route handler. Shares the same policy gate and tool resolution path as the
458
+ * HTTP dispatcher — a caller using this helper can never bypass the agent's
459
+ * `requiredFeatures`, `allowedTools`, `executionMode`, or `mutationPolicy`.
460
+ *
461
+ * Attachment-to-model conversion (Step 3.7): resolved
462
+ * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6
463
+ * `FileUIPart` entries on the last user message (images/PDFs) and as a
464
+ * structured `[ATTACHMENTS]` block appended to the system prompt (text
465
+ * extracts + metadata-only summaries). The existing `attachmentIds`
466
+ * pass-through into `resolveAiAgentTools` is preserved — Step 3.6 parity
467
+ * invariant #7 still holds.
468
+ */
469
+ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Response> {
470
+ const mutationPolicyOverride = await resolveMutationPolicyOverride(
471
+ input.agentId,
472
+ input.container,
473
+ input.authContext.tenantId,
474
+ input.authContext.organizationId,
475
+ )
476
+ const { agent, tools } = await resolveAiAgentTools({
477
+ agentId: input.agentId,
478
+ authContext: input.authContext,
479
+ pageContext: input.pageContext,
480
+ attachmentIds: input.attachmentIds,
481
+ mutationPolicyOverride,
482
+ container: input.container,
483
+ conversationId: input.conversationId ?? null,
484
+ })
485
+
486
+ const resolvedAttachments = await resolveAttachmentPartsForAgent({
487
+ agent,
488
+ attachmentIds: input.attachmentIds,
489
+ authContext: input.authContext,
490
+ container: input.container,
491
+ })
492
+
493
+ const baseSystemPrompt = await composeSystemPrompt(
494
+ agent,
495
+ input.pageContext,
496
+ input.container,
497
+ input.authContext.tenantId,
498
+ input.authContext.organizationId,
499
+ )
500
+ const systemPrompt = appendRuntimeMutationPolicy(
501
+ appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),
502
+ agent,
503
+ mutationPolicyOverride,
504
+ )
505
+
506
+ const { model } = resolveAgentModel(agent, input.modelOverride)
507
+ const normalizedMessages = ensureUiMessageShape(input.messages)
508
+ const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments)
509
+ const modelMessages = await convertToModelMessages(hydratedMessages)
510
+ // Default to 10 agentic steps when the agent does not declare maxSteps.
511
+ // Without stopWhen the AI SDK runs a single model call and never executes
512
+ // tool calls, which makes every tool-using query return an empty stream.
513
+ const effectiveMaxSteps = typeof agent.maxSteps === 'number' && agent.maxSteps > 0
514
+ ? agent.maxSteps
515
+ : 10
516
+ const stopWhen = stepCountIs(effectiveMaxSteps)
517
+
518
+ const streamArgs: Parameters<typeof streamText>[0] = {
519
+ model,
520
+ system: systemPrompt,
521
+ messages: modelMessages,
522
+ tools,
523
+ stopWhen,
524
+ }
525
+
526
+ const result = streamText(streamArgs)
527
+ return result.toUIMessageStreamResponse({
528
+ sendReasoning: true,
529
+ headers: {
530
+ 'Cache-Control': 'no-cache, no-transform',
531
+ Connection: 'keep-alive',
532
+ },
533
+ })
534
+ }
535
+
536
+ /**
537
+ * Runtime override for the structured-output schema used by {@link runAiAgentObject}.
538
+ * When the agent itself declares no `output` block, the caller MUST supply this;
539
+ * otherwise the helper rejects with {@link AgentPolicyError} code
540
+ * `execution_mode_not_supported`.
541
+ */
542
+ export interface RunAiAgentObjectOutputOverride<TSchema = ZodTypeAny> {
543
+ schemaName: string
544
+ schema: TSchema
545
+ /**
546
+ * `'generate'` (default) calls AI SDK `generateObject` and resolves to the
547
+ * parsed object. `'stream'` calls `streamObject` and returns the SDK's
548
+ * streaming handle so callers can consume partial objects / text deltas.
549
+ */
550
+ mode?: 'generate' | 'stream'
551
+ }
552
+
553
+ export interface RunAiAgentObjectInput<TSchema = ZodTypeAny> {
554
+ agentId: string
555
+ /**
556
+ * Accepts either a bare user prompt (wrapped as `[{ role: 'user', content }]`)
557
+ * or a prebuilt `UIMessage[]` array — matches the source spec's
558
+ * `RunAiAgentObjectInput` contract (§1149–1160).
559
+ */
560
+ input: string | UIMessage[]
561
+ attachmentIds?: string[]
562
+ pageContext?: AgentRequestPageContext
563
+ /**
564
+ * Same Phase-1 shim as {@link RunAiAgentTextInput.authContext}. Required until
565
+ * a global request-context resolver lands (Phase 4).
566
+ */
567
+ authContext: AiChatRequestContext
568
+ modelOverride?: string
569
+ output?: RunAiAgentObjectOutputOverride<TSchema>
570
+ debug?: boolean
571
+ container?: AwilixContainer
572
+ }
573
+
574
+ export type RunAiAgentObjectGenerateResult<TSchema> = {
575
+ mode: 'generate'
576
+ object: TSchema
577
+ finishReason?: string
578
+ usage?: { inputTokens?: number; outputTokens?: number }
579
+ }
580
+
581
+ export type RunAiAgentObjectStreamResult<TSchema> = {
582
+ mode: 'stream'
583
+ /** Full parsed object once the stream completes. */
584
+ object: Promise<TSchema>
585
+ /** Async iterator of partial (progressively hydrated) objects. */
586
+ partialObjectStream: AsyncIterable<Partial<TSchema>>
587
+ /** Async iterator of the raw text deltas the model emitted. */
588
+ textStream: AsyncIterable<string>
589
+ finishReason?: Promise<string | undefined>
590
+ usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>
591
+ }
592
+
593
+ export type RunAiAgentObjectResult<TSchema> =
594
+ | RunAiAgentObjectGenerateResult<TSchema>
595
+ | RunAiAgentObjectStreamResult<TSchema>
596
+
597
+ function normalizeObjectMessages(input: string | UIMessage[]): UIMessage[] {
598
+ if (typeof input === 'string') {
599
+ return [
600
+ {
601
+ id: 'user-input',
602
+ role: 'user',
603
+ parts: [{ type: 'text', text: input }],
604
+ } as unknown as UIMessage,
605
+ ]
606
+ }
607
+ return input
608
+ }
609
+
610
+ function resolveStructuredOutput<TSchema>(
611
+ agent: AiAgentDefinition,
612
+ override: RunAiAgentObjectOutputOverride<TSchema> | undefined,
613
+ ): { schemaName: string; schema: unknown; mode: 'generate' | 'stream' } {
614
+ if (override) {
615
+ return {
616
+ schemaName: override.schemaName,
617
+ schema: override.schema as unknown,
618
+ mode: override.mode ?? 'generate',
619
+ }
620
+ }
621
+ const declared = agent.output as AiAgentStructuredOutput | undefined
622
+ if (!declared) {
623
+ throw new AgentPolicyError(
624
+ 'execution_mode_not_supported',
625
+ `Agent "${agent.id}" does not declare a structured-output schema; pass runAiAgentObject({ output }) or declare agent.output.`,
626
+ )
627
+ }
628
+ return {
629
+ schemaName: declared.schemaName,
630
+ schema: declared.schema as unknown,
631
+ mode: declared.mode ?? 'generate',
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Server-side helper that runs an Open Mercato agent in structured-output mode
637
+ * via the Vercel AI SDK. Shares the same policy gate, tool resolution path,
638
+ * system-prompt composition, and model resolution as {@link runAiAgentText} —
639
+ * object-mode and chat-mode CANNOT diverge.
640
+ *
641
+ * Attachment-to-model conversion (Step 3.7): resolved
642
+ * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6
643
+ * `FileUIPart` entries on the last user message (images/PDFs) and as a
644
+ * structured `[ATTACHMENTS]` block appended to the system prompt (text
645
+ * extracts + metadata-only summaries). Matches {@link runAiAgentText} byte-
646
+ * for-byte so the Step 3.6 parity contract is preserved.
647
+ */
648
+ export async function runAiAgentObject<TSchema = unknown>(
649
+ input: RunAiAgentObjectInput<TSchema>,
650
+ ): Promise<RunAiAgentObjectResult<TSchema>> {
651
+ const mutationPolicyOverride = await resolveMutationPolicyOverride(
652
+ input.agentId,
653
+ input.container,
654
+ input.authContext.tenantId,
655
+ input.authContext.organizationId,
656
+ )
657
+ const { agent, tools } = await resolveAiAgentTools({
658
+ agentId: input.agentId,
659
+ authContext: input.authContext,
660
+ pageContext: input.pageContext,
661
+ attachmentIds: input.attachmentIds,
662
+ requestedExecutionMode: 'object',
663
+ mutationPolicyOverride,
664
+ container: input.container,
665
+ })
666
+
667
+ const resolvedOutput = resolveStructuredOutput(agent, input.output)
668
+
669
+ const resolvedAttachments = await resolveAttachmentPartsForAgent({
670
+ agent,
671
+ attachmentIds: input.attachmentIds,
672
+ authContext: input.authContext,
673
+ container: input.container,
674
+ })
675
+
676
+ const baseSystemPrompt = await composeSystemPrompt(
677
+ agent,
678
+ input.pageContext,
679
+ input.container,
680
+ input.authContext.tenantId,
681
+ input.authContext.organizationId,
682
+ )
683
+ const systemPrompt = appendRuntimeMutationPolicy(
684
+ appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),
685
+ agent,
686
+ mutationPolicyOverride,
687
+ )
688
+
689
+ const { model } = resolveAgentModel(agent, input.modelOverride)
690
+ const normalizedMessages = ensureUiMessageShape(normalizeObjectMessages(input.input))
691
+ const hydratedMessages = attachAttachmentsToMessages(
692
+ normalizedMessages,
693
+ resolvedAttachments,
694
+ )
695
+ const modelMessages = await convertToModelMessages(hydratedMessages)
696
+ const stopWhen = typeof agent.maxSteps === 'number' && agent.maxSteps > 0
697
+ ? stepCountIs(agent.maxSteps)
698
+ : undefined
699
+
700
+ if (resolvedOutput.mode === 'stream') {
701
+ const streamArgs: Parameters<typeof streamObject>[0] = {
702
+ model,
703
+ system: systemPrompt,
704
+ messages: modelMessages,
705
+ schema: resolvedOutput.schema as never,
706
+ schemaName: resolvedOutput.schemaName,
707
+ }
708
+ const result = streamObject(streamArgs) as unknown as {
709
+ object: Promise<TSchema>
710
+ partialObjectStream: AsyncIterable<Partial<TSchema>>
711
+ textStream: AsyncIterable<string>
712
+ finishReason?: Promise<string | undefined>
713
+ usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>
714
+ }
715
+ return {
716
+ mode: 'stream',
717
+ object: result.object,
718
+ partialObjectStream: result.partialObjectStream,
719
+ textStream: result.textStream,
720
+ finishReason: result.finishReason,
721
+ usage: result.usage,
722
+ }
723
+ }
724
+
725
+ const generateArgs: Parameters<typeof generateObject>[0] = {
726
+ model,
727
+ system: systemPrompt,
728
+ messages: modelMessages,
729
+ schema: resolvedOutput.schema as never,
730
+ schemaName: resolvedOutput.schemaName,
731
+ }
732
+ if (stopWhen) {
733
+ // generateObject shares `CallSettings` with generateText; stopWhen is ignored
734
+ // by the typed surface but harmless for providers that respect it. Tools
735
+ // flow through the system prompt only in object mode today — the whitelist
736
+ // has already been resolved via `resolveAiAgentTools` above, even if we
737
+ // don't hand it to generateObject.
738
+ ;(generateArgs as Record<string, unknown>).stopWhen = stopWhen
739
+ }
740
+ void tools
741
+
742
+ const result = await generateObject(generateArgs)
743
+ return {
744
+ mode: 'generate',
745
+ object: (result as { object: unknown }).object as TSchema,
746
+ finishReason: (result as { finishReason?: string }).finishReason,
747
+ usage: (result as { usage?: { inputTokens?: number; outputTokens?: number } }).usage,
748
+ }
749
+ }
750
+
751
+ export { AgentPolicyError }