@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,410 @@
1
+ /**
2
+ * Pending-action re-check contract (spec §9.4 / Step 5.8).
3
+ *
4
+ * The operator presses "Confirm" on a `mutation-preview-card`. The server
5
+ * MUST re-verify every invariant before executing the wrapped tool — the
6
+ * pending row was created at propose-time and agents / policies / record
7
+ * versions can all drift between then and confirm. This module is the
8
+ * single source of truth for that contract.
9
+ *
10
+ * The individual `check*` helpers are exported independently so the Step
11
+ * 5.8 unit suite can exercise each guard in isolation. The orchestrator
12
+ * {@link runPendingActionRechecks} stops at the first failure and returns
13
+ * a structured denial the route turns into a JSON error envelope.
14
+ *
15
+ * This module is pure aside from DB reads; the caller owns the transaction
16
+ * boundary. The Step 5.9 cancel route reuses {@link checkStatusAndExpiry}.
17
+ */
18
+ import type { EntityManager } from '@mikro-orm/postgresql'
19
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
20
+ import { Attachment } from '@open-mercato/core/modules/attachments/data/entities'
21
+ import type { AiAgentDefinition } from './ai-agent-definition'
22
+ import type { AiToolDefinition, McpToolContext } from './types'
23
+ import type { AiPendingAction } from '../data/entities'
24
+ import type {
25
+ AiPendingActionFailedRecord,
26
+ AiPendingActionRecordDiff,
27
+ AiPendingActionStatus,
28
+ } from './pending-action-types'
29
+ import { resolveEffectiveMutationPolicy } from './agent-policy'
30
+ import { hasRequiredFeatures } from './auth'
31
+ import type { AiAgentMutationPolicy } from './ai-agent-definition'
32
+
33
+ export type PendingActionRecheckCode =
34
+ | 'invalid_status'
35
+ | 'expired'
36
+ | 'agent_unknown'
37
+ | 'agent_features_denied'
38
+ | 'tool_not_whitelisted'
39
+ | 'read_only_agent'
40
+ | 'attachment_cross_tenant'
41
+ | 'stale_version'
42
+ | 'schema_drift'
43
+
44
+ export interface PendingActionRecheckOkResult {
45
+ ok: true
46
+ /**
47
+ * Per-record stale list for batch mutations when only some records were
48
+ * stale. Present only when the batch path produced a partial-stale set;
49
+ * the caller should persist these via `repo.setStatus(..., { failedRecords })`
50
+ * and proceed with the remaining records.
51
+ */
52
+ failedRecords?: AiPendingActionFailedRecord[]
53
+ }
54
+
55
+ export interface PendingActionRecheckFailResult {
56
+ ok: false
57
+ status: number
58
+ code: PendingActionRecheckCode
59
+ message: string
60
+ extra?: Record<string, unknown>
61
+ }
62
+
63
+ export type PendingActionRecheckResult =
64
+ | PendingActionRecheckOkResult
65
+ | PendingActionRecheckFailResult
66
+
67
+ export interface PendingActionAuthContext {
68
+ tenantId: string
69
+ organizationId: string | null
70
+ userId: string
71
+ userFeatures: string[]
72
+ isSuperAdmin: boolean
73
+ /**
74
+ * Optional DI container used by `checkRecordVersion` to hand the tool's
75
+ * `loadBeforeRecord` resolver an `McpToolContext`.
76
+ */
77
+ container?: import('awilix').AwilixContainer
78
+ em?: EntityManager
79
+ }
80
+
81
+ export interface PendingActionRecheckInput {
82
+ action: AiPendingAction
83
+ agent: AiAgentDefinition | null | undefined
84
+ tool: AiToolDefinition | null | undefined
85
+ ctx: PendingActionAuthContext
86
+ /** Explicit clock for deterministic tests. */
87
+ now?: Date
88
+ /**
89
+ * Optional tenant-scoped mutation-policy downgrade resolved by the caller
90
+ * (Step 5.4). Missing / null → agent's code-declared policy stands alone.
91
+ */
92
+ mutationPolicyOverride?: AiAgentMutationPolicy | null
93
+ }
94
+
95
+ /**
96
+ * Guards 3 + 2: pending action is still in `pending` and has not expired.
97
+ * Returns a structured 409 on mismatch. Reused by the cancel route (which
98
+ * only runs 1-3 of the re-check list).
99
+ */
100
+ export function checkStatusAndExpiry(
101
+ action: AiPendingAction,
102
+ options: { now?: Date } = {},
103
+ ): PendingActionRecheckResult {
104
+ const now = options.now ?? new Date()
105
+ if (action.status !== 'pending') {
106
+ return {
107
+ ok: false,
108
+ status: 409,
109
+ code: 'invalid_status',
110
+ message: `Pending action is in status "${action.status}"; expected "pending".`,
111
+ }
112
+ }
113
+ const expiresAt =
114
+ action.expiresAt instanceof Date ? action.expiresAt : new Date(action.expiresAt)
115
+ if (expiresAt.getTime() <= now.getTime()) {
116
+ return {
117
+ ok: false,
118
+ status: 409,
119
+ code: 'expired',
120
+ message: 'Pending action has expired. The model must re-propose the mutation.',
121
+ }
122
+ }
123
+ return { ok: true }
124
+ }
125
+
126
+ /**
127
+ * Guard 4: the agent is still registered AND the caller still carries the
128
+ * agent's `requiredFeatures`. Missing agent → 404. Missing features → 403.
129
+ */
130
+ export function checkAgentAndFeatures(
131
+ agent: AiAgentDefinition | null | undefined,
132
+ ctx: PendingActionAuthContext,
133
+ ): PendingActionRecheckResult {
134
+ if (!agent) {
135
+ return {
136
+ ok: false,
137
+ status: 404,
138
+ code: 'agent_unknown',
139
+ message: 'Agent is no longer registered.',
140
+ }
141
+ }
142
+ const required = agent.requiredFeatures ?? []
143
+ if (!hasRequiredFeatures(required, ctx.userFeatures, ctx.isSuperAdmin)) {
144
+ return {
145
+ ok: false,
146
+ status: 403,
147
+ code: 'agent_features_denied',
148
+ message: `Caller lacks one of the agent's required features: ${required.join(', ')}`,
149
+ }
150
+ }
151
+ return { ok: true }
152
+ }
153
+
154
+ /**
155
+ * Guards 5 + 6: effective mutation policy still allows mutation, AND the
156
+ * tool is still whitelisted + still declared `isMutation: true`. Read-only
157
+ * policy or whitelist drop → 403.
158
+ */
159
+ export function checkToolWhitelist(
160
+ agent: AiAgentDefinition,
161
+ tool: AiToolDefinition | null | undefined,
162
+ action: AiPendingAction,
163
+ options: { mutationPolicyOverride?: AiAgentMutationPolicy | null } = {},
164
+ ): PendingActionRecheckResult {
165
+ if (!tool) {
166
+ return {
167
+ ok: false,
168
+ status: 403,
169
+ code: 'tool_not_whitelisted',
170
+ message: `Tool "${action.toolName}" is not registered.`,
171
+ }
172
+ }
173
+ if (!agent.allowedTools.includes(tool.name) || tool.isMutation !== true) {
174
+ return {
175
+ ok: false,
176
+ status: 403,
177
+ code: 'tool_not_whitelisted',
178
+ message: `Tool "${tool.name}" is no longer whitelisted as a mutation tool for agent "${agent.id}".`,
179
+ }
180
+ }
181
+ const effective = resolveEffectiveMutationPolicy(
182
+ agent.mutationPolicy,
183
+ options.mutationPolicyOverride ?? null,
184
+ agent.id,
185
+ )
186
+ if (effective === 'read-only') {
187
+ return {
188
+ ok: false,
189
+ status: 403,
190
+ code: 'read_only_agent',
191
+ message: `Agent "${agent.id}" effective mutationPolicy=read-only; mutation tool "${tool.name}" cannot be executed.`,
192
+ }
193
+ }
194
+ return { ok: true }
195
+ }
196
+
197
+ /**
198
+ * Guard 7: every attachment id referenced by the pending row belongs to the
199
+ * caller's tenant/org. Any cross-tenant id short-circuits with 403 — we do
200
+ * not leak which specific id was rejected.
201
+ */
202
+ export async function checkAttachmentScope(
203
+ action: AiPendingAction,
204
+ ctx: PendingActionAuthContext,
205
+ ): Promise<PendingActionRecheckResult> {
206
+ const ids = Array.isArray(action.attachmentIds) ? action.attachmentIds : []
207
+ if (ids.length === 0) return { ok: true }
208
+ const em = ctx.em
209
+ if (!em) {
210
+ return {
211
+ ok: false,
212
+ status: 500,
213
+ code: 'attachment_cross_tenant',
214
+ message: 'Attachment scope check requires an EntityManager.',
215
+ }
216
+ }
217
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
+ const rows = (await findWithDecryption<any>(
219
+ em,
220
+ Attachment as any,
221
+ { id: { $in: ids } } as any,
222
+ {},
223
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
224
+ )) as Array<{ id: string; tenantId?: string | null; organizationId?: string | null }>
225
+ if (rows.length !== ids.length) {
226
+ return {
227
+ ok: false,
228
+ status: 403,
229
+ code: 'attachment_cross_tenant',
230
+ message: 'One or more attachments are not accessible to the caller.',
231
+ }
232
+ }
233
+ for (const row of rows) {
234
+ if ((row.tenantId ?? null) !== ctx.tenantId) {
235
+ return {
236
+ ok: false,
237
+ status: 403,
238
+ code: 'attachment_cross_tenant',
239
+ message: 'One or more attachments belong to a different tenant.',
240
+ }
241
+ }
242
+ if (ctx.organizationId !== null && row.organizationId && row.organizationId !== ctx.organizationId) {
243
+ return {
244
+ ok: false,
245
+ status: 403,
246
+ code: 'attachment_cross_tenant',
247
+ message: 'One or more attachments belong to a different organization.',
248
+ }
249
+ }
250
+ }
251
+ return { ok: true }
252
+ }
253
+
254
+ /**
255
+ * Guard 8: record version matches the row's captured `recordVersion`. For
256
+ * single-record actions a mismatch is a hard 412. For batch actions we
257
+ * compute per-record and return a `failedRecords[]` entry for each stale
258
+ * record; the caller proceeds with the remaining records. If the batch
259
+ * has NO remaining records (every one is stale) the function returns 412.
260
+ *
261
+ * Also acts as the schema-drift guard: re-parses `action.normalizedInput`
262
+ * through the tool's current zod schema. A shape change between propose
263
+ * and confirm surfaces as 412 `schema_drift` so the model re-proposes.
264
+ */
265
+ export async function checkRecordVersion(
266
+ action: AiPendingAction,
267
+ tool: AiToolDefinition,
268
+ ctx: PendingActionAuthContext,
269
+ ): Promise<PendingActionRecheckResult> {
270
+ const parseResult = tool.inputSchema.safeParse(action.normalizedInput ?? {})
271
+ if (!parseResult.success) {
272
+ return {
273
+ ok: false,
274
+ status: 412,
275
+ code: 'schema_drift',
276
+ message: 'Pending input no longer satisfies the tool schema.',
277
+ extra: { issues: parseResult.error.issues },
278
+ }
279
+ }
280
+
281
+ const handlerContext = toHandlerContext(ctx)
282
+
283
+ const records = Array.isArray(action.records) ? action.records : null
284
+ if (records && records.length > 0) {
285
+ if (!tool.loadBeforeRecords) {
286
+ return { ok: true }
287
+ }
288
+ const currentRows = await tool.loadBeforeRecords(parseResult.data as never, handlerContext)
289
+ const currentVersionById = new Map<string, string | null>()
290
+ for (const row of currentRows ?? []) {
291
+ currentVersionById.set(row.recordId, row.recordVersion ?? null)
292
+ }
293
+ const stale: AiPendingActionFailedRecord[] = []
294
+ for (const record of records as AiPendingActionRecordDiff[]) {
295
+ const current = currentVersionById.get(record.recordId)
296
+ const captured = record.recordVersion ?? null
297
+ if (current === undefined) {
298
+ stale.push({
299
+ recordId: record.recordId,
300
+ error: { code: 'stale_version', message: 'Record no longer exists.' },
301
+ })
302
+ continue
303
+ }
304
+ if (captured !== null && current !== captured) {
305
+ stale.push({
306
+ recordId: record.recordId,
307
+ error: { code: 'stale_version', message: 'Record version changed since preview.' },
308
+ })
309
+ }
310
+ }
311
+ if (stale.length === records.length) {
312
+ return {
313
+ ok: false,
314
+ status: 412,
315
+ code: 'stale_version',
316
+ message: 'All pending records are stale; the model must re-propose the batch.',
317
+ extra: { staleRecords: stale.map((entry) => entry.recordId) },
318
+ }
319
+ }
320
+ if (stale.length > 0) {
321
+ return { ok: true, failedRecords: stale }
322
+ }
323
+ return { ok: true }
324
+ }
325
+
326
+ if (!tool.loadBeforeRecord) {
327
+ return { ok: true }
328
+ }
329
+ const before = await tool.loadBeforeRecord(parseResult.data as never, handlerContext)
330
+ if (!before) return { ok: true }
331
+ const captured = action.recordVersion ?? null
332
+ const current = before.recordVersion ?? null
333
+ if (captured !== null && current !== captured) {
334
+ return {
335
+ ok: false,
336
+ status: 412,
337
+ code: 'stale_version',
338
+ message: 'Record version changed since preview; re-propose the mutation.',
339
+ extra: { recordId: before.recordId },
340
+ }
341
+ }
342
+ return { ok: true }
343
+ }
344
+
345
+ function toHandlerContext(ctx: PendingActionAuthContext): McpToolContext {
346
+ return {
347
+ tenantId: ctx.tenantId,
348
+ organizationId: ctx.organizationId,
349
+ userId: ctx.userId,
350
+ container: ctx.container as never,
351
+ userFeatures: ctx.userFeatures,
352
+ isSuperAdmin: ctx.isSuperAdmin,
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Orchestrator: runs every guard in spec §9.4 order and returns the first
358
+ * failure. Callers receive either `{ ok: true, failedRecords? }` — where a
359
+ * non-empty `failedRecords` indicates a partial-stale batch that should
360
+ * proceed with the non-stale subset — or `{ ok: false, status, code, ... }`
361
+ * ready to serialize as an HTTP error envelope.
362
+ */
363
+ export async function runPendingActionRechecks(
364
+ input: PendingActionRecheckInput,
365
+ ): Promise<PendingActionRecheckResult> {
366
+ const { action, agent, tool, ctx, now, mutationPolicyOverride } = input
367
+
368
+ const statusCheck = checkStatusAndExpiry(action, { now })
369
+ if (!statusCheck.ok) return statusCheck
370
+
371
+ const agentCheck = checkAgentAndFeatures(agent, ctx)
372
+ if (!agentCheck.ok) return agentCheck
373
+
374
+ const whitelistCheck = checkToolWhitelist(agent!, tool, action, {
375
+ mutationPolicyOverride: mutationPolicyOverride ?? null,
376
+ })
377
+ if (!whitelistCheck.ok) return whitelistCheck
378
+
379
+ const attachmentCheck = await checkAttachmentScope(action, ctx)
380
+ if (!attachmentCheck.ok) return attachmentCheck
381
+
382
+ const versionCheck = await checkRecordVersion(action, tool!, ctx)
383
+ return versionCheck
384
+ }
385
+
386
+ /**
387
+ * Test-only helper shadowed here to let the route unit tests assert the
388
+ * exhaustive set of codes without importing each guard individually.
389
+ */
390
+ export const PENDING_ACTION_RECHECK_CODES: ReadonlyArray<PendingActionRecheckCode> = [
391
+ 'invalid_status',
392
+ 'expired',
393
+ 'agent_unknown',
394
+ 'agent_features_denied',
395
+ 'tool_not_whitelisted',
396
+ 'read_only_agent',
397
+ 'attachment_cross_tenant',
398
+ 'stale_version',
399
+ 'schema_drift',
400
+ ]
401
+
402
+ export function isPendingActionRecheckCode(
403
+ value: unknown,
404
+ ): value is PendingActionRecheckCode {
405
+ return typeof value === 'string' && (PENDING_ACTION_RECHECK_CODES as readonly string[]).includes(value)
406
+ }
407
+
408
+ export type { AiPendingActionFailedRecord } from './pending-action-types'
409
+
410
+ export type PendingActionRecheckStatus = AiPendingActionStatus
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Shared enums + error type for the Phase 3 WS-C mutation approval gate
3
+ * (spec §8 `AiPendingAction` + §9 server contract).
4
+ *
5
+ * These values are referenced by the entity, the repository, the
6
+ * `/api/ai/actions/*` routes (Steps 5.7 / 5.8 / 5.9), and the cleanup
7
+ * worker (Step 5.12). Colocated here so every consumer shares the same
8
+ * source of truth.
9
+ */
10
+
11
+ export const AI_PENDING_ACTION_STATUSES = [
12
+ 'pending',
13
+ 'confirmed',
14
+ 'cancelled',
15
+ 'expired',
16
+ 'executing',
17
+ 'failed',
18
+ ] as const
19
+
20
+ export type AiPendingActionStatus = (typeof AI_PENDING_ACTION_STATUSES)[number]
21
+
22
+ export const AI_PENDING_ACTION_QUEUE_MODES = ['inline', 'stack'] as const
23
+
24
+ export type AiPendingActionQueueMode = (typeof AI_PENDING_ACTION_QUEUE_MODES)[number]
25
+
26
+ /**
27
+ * Allowed state-machine edges for `AiPendingAction.status`:
28
+ *
29
+ * ```
30
+ * pending ──┬─▶ confirmed ──▶ executing ──▶ failed
31
+ * │ └──▶ (terminal success keeps status = 'confirmed'
32
+ * │ and stores executionResult.recordId)
33
+ * ├─▶ cancelled
34
+ * └─▶ expired
35
+ * ```
36
+ *
37
+ * Every other transition is rejected with `AiPendingActionStateError`.
38
+ */
39
+ export const AI_PENDING_ACTION_ALLOWED_TRANSITIONS: Record<
40
+ AiPendingActionStatus,
41
+ ReadonlyArray<AiPendingActionStatus>
42
+ > = {
43
+ pending: ['confirmed', 'cancelled', 'expired'],
44
+ confirmed: ['executing'],
45
+ executing: ['confirmed', 'failed'],
46
+ cancelled: [],
47
+ expired: [],
48
+ failed: [],
49
+ }
50
+
51
+ export const AI_PENDING_ACTION_TERMINAL_STATUSES: ReadonlyArray<AiPendingActionStatus> = [
52
+ 'confirmed',
53
+ 'cancelled',
54
+ 'expired',
55
+ 'failed',
56
+ ]
57
+
58
+ /**
59
+ * Per-record batch diff entry, mirrored in `AiPendingAction.records`.
60
+ *
61
+ * When present, the batch diff is authoritative and `fieldDiff` at the
62
+ * top level is ignored by every consumer (spec §8 rule 2).
63
+ */
64
+ export type AiPendingActionRecordDiff = {
65
+ recordId: string
66
+ entityType: string
67
+ label: string
68
+ fieldDiff: Array<{ field: string; before: unknown; after: unknown }>
69
+ recordVersion: string | null
70
+ attachmentIds?: string[]
71
+ }
72
+
73
+ /**
74
+ * Per-record failure shape populated by the confirm handler (Step 5.8)
75
+ * when partial success occurs inside a batch.
76
+ */
77
+ export type AiPendingActionFailedRecord = {
78
+ recordId: string
79
+ error: { code: string; message: string }
80
+ }
81
+
82
+ export type AiPendingActionFieldDiff = {
83
+ field: string
84
+ before: unknown
85
+ after: unknown
86
+ }
87
+
88
+ /**
89
+ * Structured error context the confirm-executor stamps on
90
+ * `executionResult.error` when a tool handler throws. Additive — every
91
+ * field is optional so older serialized snapshots remain valid.
92
+ *
93
+ * - `details`: free-form structured payload extracted from the thrown
94
+ * error (e.g. ZodError `issues`, `cause`, custom error properties).
95
+ * Forwarded verbatim to the operator's "Fix with AI" prompt so the
96
+ * model can correct the call instead of staring at "Invalid input".
97
+ * - `input`: a JSON-serializable echo of the arguments the handler was
98
+ * invoked with. Lets the model compare what it sent vs. what the
99
+ * schema expected. PII-redaction is the caller's responsibility (the
100
+ * confirm-executor passes through `normalizedInput` which has already
101
+ * been Zod-parsed and stripped of unknown keys).
102
+ * - `name`: the constructor name of the thrown error (e.g. `ZodError`,
103
+ * `TypeError`) — useful when the message is generic.
104
+ * - `stack`: short stack snippet for handler-side diagnostics. Kept off
105
+ * by default (only populated when `OM_AI_INCLUDE_HANDLER_STACK=1`).
106
+ */
107
+ export type AiPendingActionExecutionErrorDetails = {
108
+ issues?: Array<{ path?: (string | number)[]; message?: string; code?: string }>
109
+ fieldErrors?: Record<string, string[]>
110
+ cause?: unknown
111
+ [key: string]: unknown
112
+ }
113
+
114
+ export type AiPendingActionExecutionResult = {
115
+ recordId?: string
116
+ commandName?: string
117
+ error?: {
118
+ code: string
119
+ message: string
120
+ name?: string
121
+ details?: AiPendingActionExecutionErrorDetails
122
+ input?: unknown
123
+ stack?: string
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Thrown by the repository when a caller attempts an illegal status
129
+ * transition (e.g. `confirmed → pending`). Callers at the route layer
130
+ * turn this into a `409 Conflict` response.
131
+ */
132
+ export class AiPendingActionStateError extends Error {
133
+ public readonly code = 'ai_pending_action_invalid_transition'
134
+
135
+ constructor(
136
+ public readonly from: AiPendingActionStatus,
137
+ public readonly to: AiPendingActionStatus,
138
+ ) {
139
+ super(`Illegal AiPendingAction status transition: ${from} → ${to}`)
140
+ this.name = 'AiPendingActionStateError'
141
+ }
142
+ }
143
+
144
+ export function isAiPendingActionStatus(
145
+ value: unknown,
146
+ ): value is AiPendingActionStatus {
147
+ return (
148
+ typeof value === 'string' &&
149
+ (AI_PENDING_ACTION_STATUSES as readonly string[]).includes(value)
150
+ )
151
+ }
152
+
153
+ export function isAiPendingActionQueueMode(
154
+ value: unknown,
155
+ ): value is AiPendingActionQueueMode {
156
+ return (
157
+ typeof value === 'string' &&
158
+ (AI_PENDING_ACTION_QUEUE_MODES as readonly string[]).includes(value)
159
+ )
160
+ }
161
+
162
+ export function isTerminalAiPendingActionStatus(
163
+ status: AiPendingActionStatus,
164
+ ): boolean {
165
+ return AI_PENDING_ACTION_TERMINAL_STATUSES.includes(status)
166
+ }
167
+
168
+ export function isAllowedAiPendingActionTransition(
169
+ from: AiPendingActionStatus,
170
+ to: AiPendingActionStatus,
171
+ ): boolean {
172
+ return (AI_PENDING_ACTION_ALLOWED_TRANSITIONS[from] ?? []).includes(to)
173
+ }
174
+
175
+ /**
176
+ * Default TTL for a pending action (spec §8 rule `expiresAt defaults to 10 min;
177
+ * overridable per agent`). The runtime default is 15 min here because the
178
+ * Step 5.5 brief pins it there; the repo reads `AI_PENDING_ACTION_TTL_SECONDS`
179
+ * from the environment to allow override without a code change.
180
+ */
181
+ export const AI_PENDING_ACTION_DEFAULT_TTL_SECONDS = 900
182
+ export const AI_PENDING_ACTION_TTL_ENV_VAR = 'AI_PENDING_ACTION_TTL_SECONDS'
183
+
184
+ export function resolveAiPendingActionTtlSeconds(
185
+ env: NodeJS.ProcessEnv = process.env,
186
+ ): number {
187
+ const raw = env[AI_PENDING_ACTION_TTL_ENV_VAR]
188
+ if (raw == null) return AI_PENDING_ACTION_DEFAULT_TTL_SECONDS
189
+ const parsed = Number.parseInt(String(raw), 10)
190
+ if (!Number.isFinite(parsed) || parsed <= 0) {
191
+ return AI_PENDING_ACTION_DEFAULT_TTL_SECONDS
192
+ }
193
+ return parsed
194
+ }