@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,433 @@
1
+ import { promises as fs } from 'fs'
2
+ import type { AwilixContainer } from 'awilix'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import type {
6
+ AiAgentAcceptedMediaType,
7
+ AiAgentDefinition,
8
+ } from './ai-agent-definition'
9
+ import type {
10
+ AiChatRequestContext,
11
+ AiResolvedAttachmentPart,
12
+ } from './attachment-bridge-types'
13
+
14
+ // Provider-native inline byte limit. Most AI providers accept inline image/PDF
15
+ // payloads comfortably under 4 MB; anything larger SHOULD travel as a short-lived
16
+ // signed URL (see AttachmentSigner below). Above this ceiling and with no signer
17
+ // configured, the helper downgrades to `metadata-only` so the model at least sees
18
+ // that the attachment exists.
19
+ const DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024
20
+
21
+ // Extracted text cap. The `content` column on the `attachments` table is the
22
+ // OCR/text-extraction output; we forward it verbatim up to this character count
23
+ // so the system prompt + messages combined do not blow past model context
24
+ // limits. Truncation is signaled to the model via a trailing `[... truncated]`
25
+ // marker.
26
+ const DEFAULT_MAX_TEXT_CHARS = 64 * 1024
27
+
28
+ /**
29
+ * Optional attachment-signer. When the DI container resolves a value under
30
+ * `attachmentSigner`, the resolver uses it to mint a short-lived URL for
31
+ * images/PDFs that exceed the inline-bytes threshold. Phase 1 does not ship a
32
+ * concrete signer; the hook exists so the `signed-url` branch of
33
+ * {@link AiResolvedAttachmentPart} is reachable as soon as a provider wires one
34
+ * up without requiring another runtime change.
35
+ */
36
+ export interface AttachmentSigner {
37
+ sign(input: {
38
+ attachmentId: string
39
+ fileName: string
40
+ mediaType: string
41
+ tenantId: string | null
42
+ organizationId: string | null
43
+ }): Promise<string | null>
44
+ }
45
+
46
+ export interface ResolveAttachmentPartsInput {
47
+ attachmentIds: readonly string[]
48
+ authContext: AiChatRequestContext
49
+ acceptedMediaTypes?: readonly AiAgentAcceptedMediaType[]
50
+ container?: AwilixContainer
51
+ /**
52
+ * Optional override for the inline bytes threshold. Callers SHOULD leave
53
+ * this untouched; the default tracks a safe cross-provider ceiling.
54
+ */
55
+ maxInlineBytes?: number
56
+ /**
57
+ * Optional override for the extracted-text character cap.
58
+ */
59
+ maxTextChars?: number
60
+ }
61
+
62
+ function classifyMediaType(mimeType: string | null | undefined): AiAgentAcceptedMediaType {
63
+ const normalized = (mimeType ?? '').toLowerCase().trim()
64
+ if (normalized.startsWith('image/')) return 'image'
65
+ if (normalized === 'application/pdf') return 'pdf'
66
+ return 'file'
67
+ }
68
+
69
+ function isTextLikeMime(mimeType: string | null | undefined): boolean {
70
+ const normalized = (mimeType ?? '').toLowerCase().trim()
71
+ if (!normalized) return false
72
+ if (normalized.startsWith('text/')) return true
73
+ if (normalized === 'application/json') return true
74
+ if (normalized === 'application/xml') return true
75
+ if (normalized === 'application/x-yaml' || normalized === 'text/yaml') return true
76
+ if (normalized === 'application/csv') return true
77
+ return false
78
+ }
79
+
80
+ function truncateText(value: string, maxChars: number): string {
81
+ if (value.length <= maxChars) return value
82
+ return `${value.slice(0, Math.max(0, maxChars - 16))}\n[... truncated]`
83
+ }
84
+
85
+ function resolveEm(container: AwilixContainer | undefined): EntityManager | null {
86
+ if (!container) return null
87
+ try {
88
+ const candidate = container.resolve('em') as EntityManager | undefined
89
+ return candidate ?? null
90
+ } catch {
91
+ return null
92
+ }
93
+ }
94
+
95
+ function resolveSigner(container: AwilixContainer | undefined): AttachmentSigner | null {
96
+ if (!container) return null
97
+ try {
98
+ const candidate = container.resolve('attachmentSigner') as AttachmentSigner | undefined
99
+ if (candidate && typeof candidate.sign === 'function') {
100
+ return candidate
101
+ }
102
+ } catch {
103
+ return null
104
+ }
105
+ return null
106
+ }
107
+
108
+ type AttachmentRow = {
109
+ id: string
110
+ entityId: string
111
+ fileName: string
112
+ mimeType: string
113
+ fileSize: number
114
+ storagePath: string
115
+ storageDriver: string
116
+ partitionCode: string
117
+ tenantId: string | null
118
+ organizationId: string | null
119
+ content: string | null
120
+ }
121
+
122
+ async function loadAttachmentRow(
123
+ em: EntityManager,
124
+ attachmentId: string,
125
+ authContext: AiChatRequestContext,
126
+ ): Promise<AttachmentRow | null> {
127
+ // Attachment entity is imported lazily to keep ai-assistant isomorphic — the
128
+ // core package owns the MikroORM metadata and is the only place tests would
129
+ // need to bootstrap for real DB access.
130
+ const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')
131
+ const record = await findOneWithDecryption(
132
+ em,
133
+ Attachment as never,
134
+ { id: attachmentId } as never,
135
+ undefined,
136
+ {
137
+ tenantId: authContext.tenantId,
138
+ organizationId: authContext.organizationId,
139
+ },
140
+ )
141
+ if (!record) return null
142
+ const row = record as unknown as AttachmentRow
143
+ return {
144
+ id: row.id,
145
+ entityId: row.entityId,
146
+ fileName: row.fileName,
147
+ mimeType: row.mimeType,
148
+ fileSize: row.fileSize,
149
+ storagePath: row.storagePath,
150
+ storageDriver: row.storageDriver,
151
+ partitionCode: row.partitionCode,
152
+ tenantId: row.tenantId ?? null,
153
+ organizationId: row.organizationId ?? null,
154
+ content: row.content ?? null,
155
+ }
156
+ }
157
+
158
+ function rowBelongsToCaller(row: AttachmentRow, authContext: AiChatRequestContext): boolean {
159
+ if (authContext.isSuperAdmin) return true
160
+ // Tenant scope: if the record is tenant-scoped, it MUST match the caller tenant.
161
+ if (row.tenantId && row.tenantId !== authContext.tenantId) return false
162
+ // Organization scope: if the record is org-scoped, it MUST match the caller org.
163
+ if (row.organizationId && row.organizationId !== authContext.organizationId) return false
164
+ return true
165
+ }
166
+
167
+ async function readAttachmentBytes(row: AttachmentRow): Promise<Uint8Array | null> {
168
+ const { resolveAttachmentAbsolutePath } = await import(
169
+ '@open-mercato/core/modules/attachments/lib/storage'
170
+ )
171
+ const absolutePath = resolveAttachmentAbsolutePath(
172
+ row.partitionCode,
173
+ row.storagePath,
174
+ row.storageDriver,
175
+ )
176
+ try {
177
+ const buffer = await fs.readFile(absolutePath)
178
+ return new Uint8Array(buffer)
179
+ } catch (error) {
180
+ console.warn(
181
+ `[AI Agents] Failed to read attachment ${row.id} from storage; falling back to metadata-only:`,
182
+ error,
183
+ )
184
+ return null
185
+ }
186
+ }
187
+
188
+ async function classifyAndBuildPart(
189
+ row: AttachmentRow,
190
+ mediaClass: AiAgentAcceptedMediaType,
191
+ maxInlineBytes: number,
192
+ maxTextChars: number,
193
+ signer: AttachmentSigner | null,
194
+ authContext: AiChatRequestContext,
195
+ ): Promise<AiResolvedAttachmentPart> {
196
+ const base: Pick<AiResolvedAttachmentPart, 'attachmentId' | 'fileName' | 'mediaType'> = {
197
+ attachmentId: row.id,
198
+ fileName: row.fileName,
199
+ mediaType: row.mimeType || 'application/octet-stream',
200
+ }
201
+
202
+ // Text-like generic files — use the pre-extracted content column if present.
203
+ if (mediaClass === 'file' && isTextLikeMime(row.mimeType) && typeof row.content === 'string' && row.content.length > 0) {
204
+ return {
205
+ ...base,
206
+ source: 'text',
207
+ textContent: truncateText(row.content, maxTextChars),
208
+ }
209
+ }
210
+
211
+ // Images + PDFs — prefer inline bytes when small enough; otherwise signed URL
212
+ // if the container registered an attachmentSigner; otherwise metadata-only.
213
+ if (mediaClass === 'image' || mediaClass === 'pdf') {
214
+ if (row.fileSize > 0 && row.fileSize <= maxInlineBytes) {
215
+ const bytes = await readAttachmentBytes(row)
216
+ if (bytes) {
217
+ return {
218
+ ...base,
219
+ source: 'bytes',
220
+ data: bytes,
221
+ }
222
+ }
223
+ }
224
+ if (signer) {
225
+ try {
226
+ const url = await signer.sign({
227
+ attachmentId: row.id,
228
+ fileName: row.fileName,
229
+ mediaType: row.mimeType,
230
+ tenantId: authContext.tenantId,
231
+ organizationId: authContext.organizationId,
232
+ })
233
+ if (typeof url === 'string' && url.length > 0) {
234
+ return {
235
+ ...base,
236
+ source: 'signed-url',
237
+ url,
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.warn(
242
+ `[AI Agents] attachmentSigner failed for ${row.id}; falling back to metadata-only:`,
243
+ error,
244
+ )
245
+ }
246
+ }
247
+ return { ...base, source: 'metadata-only' }
248
+ }
249
+
250
+ // Generic file without extracted text — metadata-only so the model at least
251
+ // knows the attachment is present.
252
+ return { ...base, source: 'metadata-only' }
253
+ }
254
+
255
+ /**
256
+ * Resolves each `attachmentId` into a model-ready {@link AiResolvedAttachmentPart}.
257
+ *
258
+ * Contract:
259
+ *
260
+ * - Tenant/org scope is enforced: records that don't belong to the caller are
261
+ * dropped with a `console.warn`. Super-admin callers bypass the scope check.
262
+ * - When the agent declares `acceptedMediaTypes`, parts whose classified media
263
+ * type is not in the whitelist are dropped with a `console.warn`.
264
+ * `acceptedMediaTypes: undefined` means "no filter".
265
+ * - When the DI container is missing or the attachments service is
266
+ * unavailable, the helper returns `[]` with a single `console.warn` and
267
+ * does NOT throw — the caller's `attachmentIds` pass-through to
268
+ * {@link resolveAiAgentTools} remains the Step 3.6 parity behavior.
269
+ * - The returned parts are ordered to match `attachmentIds`. Any id that
270
+ * cannot be resolved (not found, out-of-scope, unreadable) is silently
271
+ * dropped from the result — the caller observes a shorter list.
272
+ */
273
+ export async function resolveAttachmentParts(
274
+ input: ResolveAttachmentPartsInput,
275
+ ): Promise<AiResolvedAttachmentPart[]> {
276
+ const ids = Array.from(input.attachmentIds ?? [])
277
+ if (ids.length === 0) return []
278
+
279
+ const em = resolveEm(input.container)
280
+ if (!em) {
281
+ console.warn(
282
+ '[AI Agents] resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution.',
283
+ )
284
+ return []
285
+ }
286
+
287
+ const maxInlineBytes = input.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES
288
+ const maxTextChars = input.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS
289
+ const signer = resolveSigner(input.container)
290
+ const acceptedSet = input.acceptedMediaTypes
291
+ ? new Set<AiAgentAcceptedMediaType>(input.acceptedMediaTypes)
292
+ : null
293
+
294
+ const parts: AiResolvedAttachmentPart[] = []
295
+ for (const id of ids) {
296
+ if (typeof id !== 'string' || id.length === 0) continue
297
+ let row: AttachmentRow | null
298
+ try {
299
+ row = await loadAttachmentRow(em, id, input.authContext)
300
+ } catch (error) {
301
+ console.warn(
302
+ `[AI Agents] Failed to load attachment ${id}; skipping:`,
303
+ error,
304
+ )
305
+ continue
306
+ }
307
+ if (!row) {
308
+ console.warn(`[AI Agents] Attachment ${id} not found; skipping.`)
309
+ continue
310
+ }
311
+ if (!rowBelongsToCaller(row, input.authContext)) {
312
+ console.warn(
313
+ `[AI Agents] Attachment ${id} is out of scope for caller (tenant=${input.authContext.tenantId}, org=${input.authContext.organizationId}); skipping.`,
314
+ )
315
+ continue
316
+ }
317
+ const mediaClass = classifyMediaType(row.mimeType)
318
+ if (acceptedSet && !acceptedSet.has(mediaClass)) {
319
+ console.warn(
320
+ `[AI Agents] Attachment ${id} (${row.mimeType}) is not in agent acceptedMediaTypes=${[...acceptedSet].join(',')}; skipping.`,
321
+ )
322
+ continue
323
+ }
324
+ try {
325
+ const part = await classifyAndBuildPart(
326
+ row,
327
+ mediaClass,
328
+ maxInlineBytes,
329
+ maxTextChars,
330
+ signer,
331
+ input.authContext,
332
+ )
333
+ parts.push(part)
334
+ } catch (error) {
335
+ console.warn(
336
+ `[AI Agents] Failed to build attachment part for ${id}; skipping:`,
337
+ error,
338
+ )
339
+ }
340
+ }
341
+
342
+ return parts
343
+ }
344
+
345
+ /**
346
+ * Helper used by {@link ./agent-runtime} to fan out attachment resolution for
347
+ * an agent. Kept separate so the runtime helpers share identical semantics
348
+ * (Step 3.6 parity invariant #7 widened: resolved parts flow into both the
349
+ * chat and object paths through the same code).
350
+ */
351
+ export async function resolveAttachmentPartsForAgent(input: {
352
+ agent: AiAgentDefinition
353
+ attachmentIds: readonly string[] | undefined
354
+ authContext: AiChatRequestContext
355
+ container?: AwilixContainer
356
+ }): Promise<AiResolvedAttachmentPart[]> {
357
+ if (!input.attachmentIds || input.attachmentIds.length === 0) return []
358
+ return resolveAttachmentParts({
359
+ attachmentIds: input.attachmentIds,
360
+ authContext: input.authContext,
361
+ acceptedMediaTypes: input.agent.acceptedMediaTypes,
362
+ container: input.container,
363
+ })
364
+ }
365
+
366
+ /**
367
+ * Converts resolved attachment parts into AI SDK v6 `FileUIPart` shapes so
368
+ * they can be appended to the last user `UIMessage.parts`. `metadata-only`
369
+ * parts are dropped — there is no provider-safe file-part shape for them;
370
+ * their presence is surfaced through the system prompt instead by
371
+ * {@link summarizeAttachmentPartsForPrompt}.
372
+ */
373
+ export function attachmentPartsToUiFileParts(
374
+ parts: readonly AiResolvedAttachmentPart[],
375
+ ): Array<{ type: 'file'; mediaType: string; filename: string; url: string }> {
376
+ const output: Array<{ type: 'file'; mediaType: string; filename: string; url: string }> = []
377
+ for (const part of parts) {
378
+ if (part.source === 'bytes' && part.data) {
379
+ const base64 = toBase64(part.data)
380
+ if (base64) {
381
+ output.push({
382
+ type: 'file',
383
+ mediaType: part.mediaType,
384
+ filename: part.fileName,
385
+ url: `data:${part.mediaType};base64,${base64}`,
386
+ })
387
+ }
388
+ continue
389
+ }
390
+ if (part.source === 'signed-url' && typeof part.url === 'string' && part.url.length > 0) {
391
+ output.push({
392
+ type: 'file',
393
+ mediaType: part.mediaType,
394
+ filename: part.fileName,
395
+ url: part.url,
396
+ })
397
+ }
398
+ }
399
+ return output
400
+ }
401
+
402
+ /**
403
+ * Renders a compact, human-readable attachment summary to append to the
404
+ * system prompt. Covers `text`, `metadata-only`, and as a fallback the
405
+ * `bytes`/`signed-url` kinds so the model can always reason about which
406
+ * attachments are in scope. Keeping this as a string keeps provider-agnostic
407
+ * behavior — object-mode and chat-mode both consume the same surface.
408
+ */
409
+ export function summarizeAttachmentPartsForPrompt(
410
+ parts: readonly AiResolvedAttachmentPart[],
411
+ ): string | null {
412
+ if (parts.length === 0) return null
413
+ const lines: string[] = ['[ATTACHMENTS]']
414
+ for (const part of parts) {
415
+ const header = `- ${part.fileName} (${part.mediaType}, source=${part.source})`
416
+ if (part.source === 'text' && typeof part.textContent === 'string' && part.textContent.length > 0) {
417
+ lines.push(header)
418
+ lines.push(part.textContent)
419
+ } else {
420
+ lines.push(header)
421
+ }
422
+ }
423
+ return lines.join('\n')
424
+ }
425
+
426
+ function toBase64(data: Uint8Array | string): string | null {
427
+ if (typeof data === 'string') return data
428
+ try {
429
+ return Buffer.from(data).toString('base64')
430
+ } catch {
431
+ return null
432
+ }
433
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Shared AI model factory (Phase 3 WS-A — Step 5.1).
3
+ *
4
+ * Consolidates the previously-per-module model-creation plumbing (inbox_ops's
5
+ * `llmProvider.ts`, the agent-runtime's inline `resolveAgentModel`) behind a
6
+ * single DI-friendly port. Every AI-runtime caller (chat, object, inbox-ops
7
+ * extraction, future agents) resolves the `LanguageModelV1` it hands to the
8
+ * Vercel AI SDK through `createModelFactory(container).resolveModel(...)` so
9
+ * all of them share one resolution order:
10
+ *
11
+ * 1. `callerOverride` (non-empty string) — highest precedence, e.g. the
12
+ * `modelOverride` field on `runAiAgentText`/`runAiAgentObject`.
13
+ * 2. Env variable `<MODULE>_AI_MODEL` (uppercased `moduleId`) when
14
+ * `moduleId` is provided. Example: `INBOX_OPS_AI_MODEL=claude-haiku-4-5`,
15
+ * `CATALOG_AI_MODEL=gpt-4o-mini`.
16
+ * 3. `agentDefaultModel` — typically `AiAgentDefinition.defaultModel`.
17
+ * 4. The configured provider's own default model id
18
+ * (`provider.defaultModel`).
19
+ *
20
+ * Resolution walks the `llmProviderRegistry`'s `resolveFirstConfigured()`
21
+ * output so it honors the same env-driven provider discovery that existing
22
+ * callers already rely on. The factory throws {@link AiModelFactoryError}
23
+ * when no provider is configured — every current call site already expects
24
+ * the throw (see the bare `throw new Error('No LLM provider is configured...')`
25
+ * in `agent-runtime.ts` prior to this Step).
26
+ *
27
+ * @see packages/shared/src/lib/ai/llm-provider-registry.ts
28
+ * @see packages/ai-assistant/src/modules/ai_assistant/lib/agent-runtime.ts
29
+ * @see packages/core/src/modules/inbox_ops/lib/llmProvider.ts
30
+ */
31
+
32
+ import type { AwilixContainer } from 'awilix'
33
+ import type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'
34
+ import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
35
+
36
+ /**
37
+ * Minimal AI SDK LanguageModel shape — the factory exposes the protocol-
38
+ * agnostic `unknown`-typed return from {@link LlmProvider.createModel} under a
39
+ * dedicated alias so callers can document intent without importing the AI SDK
40
+ * here. Call sites that hand the result to `generateText` / `streamText` /
41
+ * `generateObject` / `streamObject` continue to cast to the SDK's
42
+ * `LanguageModelV1` / `LanguageModel` union exactly as they already do.
43
+ */
44
+ export type AiModelInstance = unknown
45
+
46
+ /**
47
+ * Input accepted by {@link AiModelFactory.resolveModel}. All fields are
48
+ * optional — passing an empty input resolves the provider default.
49
+ */
50
+ export interface AiModelFactoryInput {
51
+ /**
52
+ * Owning module id (matches `Module.id`). When set, the factory checks
53
+ * `<MODULE>_AI_MODEL` (uppercased) as the env-override source. Example:
54
+ * `moduleId: 'inbox_ops'` → env var `INBOX_OPS_AI_MODEL`.
55
+ */
56
+ moduleId?: string
57
+ /**
58
+ * Agent-level default, typically `AiAgentDefinition.defaultModel`. Used
59
+ * when neither `callerOverride` nor the module env override is present.
60
+ */
61
+ agentDefaultModel?: string
62
+ /**
63
+ * Per-call override (e.g. `runAiAgentText({ modelOverride })`). Wins over
64
+ * every other source when it is a non-empty trimmed string. Empty strings
65
+ * are treated as "no override" so the next source in the chain wins —
66
+ * callers MUST NOT need a separate "clear override" API.
67
+ */
68
+ callerOverride?: string
69
+ }
70
+
71
+ /**
72
+ * Materialized output returned by {@link AiModelFactory.resolveModel}.
73
+ */
74
+ export interface AiModelResolution {
75
+ /**
76
+ * Concrete AI SDK model instance ready to pass to
77
+ * `generateText`/`streamText`/`generateObject`/`streamObject`. Typed as
78
+ * {@link AiModelInstance} to avoid coupling this port to a specific SDK
79
+ * major version.
80
+ */
81
+ model: AiModelInstance
82
+ /** Resolved upstream model id (e.g. `claude-haiku-4-5-20251001`). */
83
+ modelId: string
84
+ /** Stable provider id from {@link LlmProvider.id}. */
85
+ providerId: string
86
+ /**
87
+ * Which source won resolution. Useful for logs and tests; never exposed
88
+ * as a public contract beyond these four enum values.
89
+ */
90
+ source: 'caller_override' | 'module_env' | 'agent_default' | 'provider_default'
91
+ }
92
+
93
+ /**
94
+ * Port exposed by {@link createModelFactory}. Stateless — the factory
95
+ * re-reads the registry + env on every `resolveModel` call so hot-reload
96
+ * and test overrides work without needing factory re-creation.
97
+ */
98
+ export interface AiModelFactory {
99
+ resolveModel(input: AiModelFactoryInput): AiModelResolution
100
+ }
101
+
102
+ /**
103
+ * Typed error thrown by the factory when it cannot materialize a model.
104
+ *
105
+ * `code` is a stable string union so downstream callers can branch without
106
+ * parsing error messages. `AiModelFactoryError`s bubble through
107
+ * `runAiAgentText`/`runAiAgentObject` unchanged — the agent runtime does
108
+ * NOT catch them, matching the pre-Step-5.1 behavior of the inline
109
+ * resolver.
110
+ */
111
+ export type AiModelFactoryErrorCode =
112
+ | 'no_provider_configured'
113
+ | 'api_key_missing'
114
+
115
+ export class AiModelFactoryError extends Error {
116
+ readonly code: AiModelFactoryErrorCode
117
+
118
+ constructor(code: AiModelFactoryErrorCode, message: string) {
119
+ super(message)
120
+ this.name = 'AiModelFactoryError'
121
+ this.code = code
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Internal dependencies of the factory. Exposed for tests only; production
127
+ * callers rely on the defaults wired by {@link createModelFactory}.
128
+ */
129
+ export interface CreateModelFactoryDependencies {
130
+ /**
131
+ * Registry used to resolve the first configured provider. Defaults to the
132
+ * singleton `llmProviderRegistry`.
133
+ */
134
+ registry?: { resolveFirstConfigured: (options?: { env?: EnvLookup }) => LlmProvider | null }
135
+ /** Env lookup for `<MODULE>_AI_MODEL` + provider credentials. */
136
+ env?: EnvLookup
137
+ }
138
+
139
+ function normalizeOverride(value: string | undefined): string | null {
140
+ if (typeof value !== 'string') return null
141
+ const trimmed = value.trim()
142
+ return trimmed.length > 0 ? trimmed : null
143
+ }
144
+
145
+ function moduleEnvVarName(moduleId: string): string {
146
+ return `${moduleId.toUpperCase()}_AI_MODEL`
147
+ }
148
+
149
+ /**
150
+ * Creates an {@link AiModelFactory} bound to the DI container. The container
151
+ * reference is accepted for API symmetry with other runtime helpers (and so
152
+ * future work can read provider overrides registered on the container); the
153
+ * current implementation only needs the registry + env. No breaking change
154
+ * when later implementations DO consult the container.
155
+ */
156
+ export function createModelFactory(
157
+ _container: AwilixContainer,
158
+ deps: CreateModelFactoryDependencies = {},
159
+ ): AiModelFactory {
160
+ const registry = deps.registry ?? llmProviderRegistry
161
+ const env = deps.env ?? process.env
162
+
163
+ return {
164
+ resolveModel(input: AiModelFactoryInput): AiModelResolution {
165
+ const provider = registry.resolveFirstConfigured({ env })
166
+ if (!provider) {
167
+ throw new AiModelFactoryError(
168
+ 'no_provider_configured',
169
+ '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.',
170
+ )
171
+ }
172
+ const apiKey = provider.resolveApiKey(env)
173
+ if (!apiKey) {
174
+ throw new AiModelFactoryError(
175
+ 'api_key_missing',
176
+ `LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`,
177
+ )
178
+ }
179
+
180
+ const callerOverride = normalizeOverride(input.callerOverride)
181
+ const moduleEnvOverride =
182
+ input.moduleId && input.moduleId.length > 0
183
+ ? normalizeOverride(env[moduleEnvVarName(input.moduleId)])
184
+ : null
185
+ const agentDefault = normalizeOverride(input.agentDefaultModel)
186
+
187
+ let modelId: string
188
+ let source: AiModelResolution['source']
189
+ if (callerOverride) {
190
+ modelId = callerOverride
191
+ source = 'caller_override'
192
+ } else if (moduleEnvOverride) {
193
+ modelId = moduleEnvOverride
194
+ source = 'module_env'
195
+ } else if (agentDefault) {
196
+ modelId = agentDefault
197
+ source = 'agent_default'
198
+ } else {
199
+ modelId = provider.defaultModel
200
+ source = 'provider_default'
201
+ }
202
+
203
+ const model = provider.createModel({ modelId, apiKey })
204
+ return {
205
+ model,
206
+ modelId,
207
+ providerId: provider.id,
208
+ source,
209
+ }
210
+ },
211
+ }
212
+ }