@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +361 -0
- package/README.md +5 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js +73 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js +484 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js +251 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js +91 -0
- package/dist/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js +202 -0
- package/dist/modules/ai_assistant/ai-tools/attachments-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js +121 -0
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js +94 -0
- package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +7 -0
- package/dist/modules/ai_assistant/ai-tools.js +14 -0
- package/dist/modules/ai_assistant/ai-tools.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +175 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +174 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +101 -0
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +311 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +246 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +94 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js +173 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js +167 -0
- package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +1111 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js +30 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js +4 -6
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js +1 -21
- package/dist/modules/ai_assistant/backend/config/ai-assistant/page.meta.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +462 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js +28 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +78 -12
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentPromptOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiPendingAction.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +228 -0
- package/dist/modules/ai_assistant/data/entities.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js +95 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js +223 -0
- package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +33 -0
- package/dist/modules/ai_assistant/events.js.map +7 -0
- package/dist/modules/ai_assistant/i18n/de.json +252 -0
- package/dist/modules/ai_assistant/i18n/en.json +252 -0
- package/dist/modules/ai_assistant/i18n/es.json +252 -0
- package/dist/modules/ai_assistant/i18n/pl.json +252 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js +168 -0
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js +195 -0
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js +451 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js +223 -0
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +7 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js +25 -0
- package/dist/modules/ai_assistant/lib/agent-transport.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js +11 -0
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-agents-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +239 -0
- package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js +189 -0
- package/dist/modules/ai_assistant/lib/ai-overrides.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js +7 -0
- package/dist/modules/ai_assistant/lib/ai-tool-definition.js.map +7 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js +1 -0
- package/dist/modules/ai_assistant/lib/ai-tools-generated.d.js.map +7 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js +48 -0
- package/dist/modules/ai_assistant/lib/api-backed-tool.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js +1 -0
- package/dist/modules/ai_assistant/lib/attachment-bridge-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js +276 -0
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +68 -0
- package/dist/modules/ai_assistant/lib/model-factory.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +86 -0
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js +35 -0
- package/dist/modules/ai_assistant/lib/pending-action-client.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +243 -0
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js +246 -0
- package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +7 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js +70 -0
- package/dist/modules/ai_assistant/lib/pending-action-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +315 -0
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js +7 -0
- package/dist/modules/ai_assistant/lib/prompt-composition-types.js.map +7 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js +175 -0
- package/dist/modules/ai_assistant/lib/prompt-override-merge.js.map +7 -0
- package/dist/modules/ai_assistant/lib/schema-utils.js +5 -1
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js +13 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js +86 -11
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +120 -0
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +418 -0
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419100521.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419132948.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js +17 -0
- package/dist/modules/ai_assistant/migrations/Migration20260419134235.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +36 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +161 -0
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +7 -0
- package/generated/entities/ai_agent_mutation_policy_override/index.ts +9 -0
- package/generated/entities/ai_agent_prompt_override/index.ts +10 -0
- package/generated/entities/ai_pending_action/index.ts +24 -0
- package/generated/entities.ids.generated.ts +13 -0
- package/generated/entity-fields-registry.ts +57 -0
- package/jest.config.cjs +7 -0
- package/package.json +4 -4
- package/src/index.ts +215 -0
- package/src/modules/ai_assistant/__integration__/README.md +5 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-002-agent-policy.spec.ts +115 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-SETTINGS-005-settings-page.spec.ts +574 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-PLAYGROUND-004-playground.spec.ts +333 -0
- package/src/modules/ai_assistant/__integration__/TC-INT-AI-TOOLS.spec.ts +135 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +145 -0
- package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +1015 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +235 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +330 -0
- package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +285 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/attachments-pack.test.ts +322 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +218 -0
- package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +192 -0
- package/src/modules/ai_assistant/ai-tools/attachments-pack.ts +269 -0
- package/src/modules/ai_assistant/ai-tools/meta-pack.ts +140 -0
- package/src/modules/ai_assistant/ai-tools/search-pack.ts +122 -0
- package/src/modules/ai_assistant/ai-tools.ts +21 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +222 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +286 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +237 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +339 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +229 -0
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +142 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/__tests__/route.test.ts +367 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +380 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/__tests__/route.test.ts +333 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +307 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +107 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +207 -0
- package/src/modules/ai_assistant/api/ai/run-object/__tests__/route.test.ts +282 -0
- package/src/modules/ai_assistant/api/ai/run-object/route.ts +204 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1419 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.meta.ts +28 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.meta.ts +8 -23
- package/src/modules/ai_assistant/backend/config/ai-assistant/page.tsx +15 -10
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +604 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.meta.ts +26 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +99 -24
- package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +69 -0
- package/src/modules/ai_assistant/data/entities/AiAgentMutationPolicyOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiAgentPromptOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiPendingAction.ts +7 -0
- package/src/modules/ai_assistant/data/entities.ts +270 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentMutationPolicyOverrideRepository.ts +129 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentPromptOverrideRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +334 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentMutationPolicyOverrideRepository.test.ts +195 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentPromptOverrideRepository.test.ts +197 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiPendingActionRepository.test.ts +357 -0
- package/src/modules/ai_assistant/events.ts +112 -0
- package/src/modules/ai_assistant/i18n/de.json +252 -0
- package/src/modules/ai_assistant/i18n/en.json +252 -0
- package/src/modules/ai_assistant/i18n/es.json +252 -0
- package/src/modules/ai_assistant/i18n/pl.json +252 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +203 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.test.ts +385 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +217 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-object.test.ts +329 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +573 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +291 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +172 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-transport.test.ts +41 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +432 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +308 -0
- package/src/modules/ai_assistant/lib/__tests__/api-backed-tool.test.ts +302 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-bridge-and-prompt-types.test.ts +188 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +531 -0
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +263 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +183 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +168 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +235 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-client.test.ts +148 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-executor.test.ts +348 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck.test.ts +378 -0
- package/src/modules/ai_assistant/lib/__tests__/phase-0-additive-contract.test.ts +299 -0
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +610 -0
- package/src/modules/ai_assistant/lib/__tests__/prompt-override-merge.test.ts +136 -0
- package/src/modules/ai_assistant/lib/__tests__/tool-loader.test.ts +125 -0
- package/src/modules/ai_assistant/lib/agent-policy.ts +270 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +277 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +751 -0
- package/src/modules/ai_assistant/lib/agent-tools.ts +396 -0
- package/src/modules/ai_assistant/lib/agent-transport.ts +51 -0
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +86 -0
- package/src/modules/ai_assistant/lib/ai-agents-generated.d.ts +18 -0
- package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +333 -0
- package/src/modules/ai_assistant/lib/ai-overrides.ts +389 -0
- package/src/modules/ai_assistant/lib/ai-tool-definition.ts +7 -0
- package/src/modules/ai_assistant/lib/ai-tools-generated.d.ts +7 -0
- package/src/modules/ai_assistant/lib/api-backed-tool.ts +85 -0
- package/src/modules/ai_assistant/lib/attachment-bridge-types.ts +24 -0
- package/src/modules/ai_assistant/lib/attachment-parts.ts +433 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +212 -0
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +179 -0
- package/src/modules/ai_assistant/lib/pending-action-client.ts +126 -0
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +424 -0
- package/src/modules/ai_assistant/lib/pending-action-recheck.ts +410 -0
- package/src/modules/ai_assistant/lib/pending-action-types.ts +194 -0
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +448 -0
- package/src/modules/ai_assistant/lib/prompt-composition-types.ts +24 -0
- package/src/modules/ai_assistant/lib/prompt-override-merge.ts +253 -0
- package/src/modules/ai_assistant/lib/schema-utils.ts +14 -2
- package/src/modules/ai_assistant/lib/tool-executor.ts +25 -3
- package/src/modules/ai_assistant/lib/tool-loader.ts +159 -13
- package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +160 -0
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +596 -0
- package/src/modules/ai_assistant/lib/types.ts +105 -2
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +871 -0
- package/src/modules/ai_assistant/migrations/Migration20260419100521.ts +17 -0
- package/src/modules/ai_assistant/migrations/Migration20260419132948.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260419134235.ts +17 -0
- package/src/modules/ai_assistant/setup.ts +53 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-pending-action-cleanup.test.ts +333 -0
- package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +269 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const authMock = jest.fn()
|
|
2
|
+
const loadAclMock = jest.fn()
|
|
3
|
+
const createRequestContainerMock = jest.fn()
|
|
4
|
+
const repoGetByIdMock = jest.fn()
|
|
5
|
+
const repoSetStatusMock = jest.fn()
|
|
6
|
+
const emitEventMock = jest.fn()
|
|
7
|
+
|
|
8
|
+
jest.mock('@open-mercato/shared/lib/auth/server', () => ({
|
|
9
|
+
getAuthFromRequest: (...args: unknown[]) => authMock(...args),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
jest.mock('@open-mercato/shared/lib/di/container', () => ({
|
|
13
|
+
createRequestContainer: (...args: unknown[]) => createRequestContainerMock(...args),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
jest.mock('../../../../../../data/repositories/AiPendingActionRepository', () => ({
|
|
17
|
+
AiPendingActionRepository: jest.fn().mockImplementation(() => ({
|
|
18
|
+
getById: repoGetByIdMock,
|
|
19
|
+
setStatus: repoSetStatusMock,
|
|
20
|
+
})),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
import { setGlobalEventBus } from '@open-mercato/shared/modules/events'
|
|
24
|
+
import { POST } from '../route'
|
|
25
|
+
|
|
26
|
+
function buildRequest(body?: unknown): Request {
|
|
27
|
+
const init: RequestInit = {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
}
|
|
31
|
+
if (body !== undefined) {
|
|
32
|
+
init.body = typeof body === 'string' ? body : JSON.stringify(body)
|
|
33
|
+
}
|
|
34
|
+
return new Request('http://localhost/api/ai_assistant/ai/actions/pa_123/cancel', init)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildContext(id: string) {
|
|
38
|
+
return { params: Promise.resolve({ id }) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeRow(overrides: Record<string, unknown> = {}) {
|
|
42
|
+
return {
|
|
43
|
+
id: 'pa_123',
|
|
44
|
+
tenantId: 'tenant-1',
|
|
45
|
+
organizationId: 'org-1',
|
|
46
|
+
agentId: 'catalog.merchandising_assistant',
|
|
47
|
+
toolName: 'catalog.update_product',
|
|
48
|
+
status: 'pending',
|
|
49
|
+
fieldDiff: [{ field: 'title', before: 'Old', after: 'New' }],
|
|
50
|
+
records: null,
|
|
51
|
+
failedRecords: null,
|
|
52
|
+
sideEffectsSummary: null,
|
|
53
|
+
recordVersion: 'v-1',
|
|
54
|
+
attachmentIds: [],
|
|
55
|
+
normalizedInput: { productId: 'p-1', patch: { title: 'New' } },
|
|
56
|
+
queueMode: 'inline',
|
|
57
|
+
executionResult: null,
|
|
58
|
+
targetEntityType: 'product',
|
|
59
|
+
targetRecordId: 'p-1',
|
|
60
|
+
conversationId: null,
|
|
61
|
+
idempotencyKey: 'idem_1',
|
|
62
|
+
createdByUserId: 'user-1',
|
|
63
|
+
createdAt: new Date(Date.now() - 60_000),
|
|
64
|
+
expiresAt: new Date(Date.now() + 3_600_000),
|
|
65
|
+
resolvedAt: null,
|
|
66
|
+
resolvedByUserId: null,
|
|
67
|
+
...overrides,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('POST /api/ai/actions/:id/cancel route (Step 5.9)', () => {
|
|
72
|
+
let consoleErrorSpy: jest.SpyInstance
|
|
73
|
+
let consoleWarnSpy: jest.SpyInstance
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
jest.clearAllMocks()
|
|
77
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
78
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
79
|
+
|
|
80
|
+
authMock.mockResolvedValue({
|
|
81
|
+
sub: 'user-1',
|
|
82
|
+
tenantId: 'tenant-1',
|
|
83
|
+
orgId: 'org-1',
|
|
84
|
+
})
|
|
85
|
+
loadAclMock.mockResolvedValue({
|
|
86
|
+
features: ['ai_assistant.view'],
|
|
87
|
+
isSuperAdmin: false,
|
|
88
|
+
})
|
|
89
|
+
createRequestContainerMock.mockResolvedValue({
|
|
90
|
+
resolve: (name: string) => {
|
|
91
|
+
if (name === 'rbacService') {
|
|
92
|
+
return {
|
|
93
|
+
loadAcl: loadAclMock,
|
|
94
|
+
hasAllFeatures: (required: string[], granted: string[]) =>
|
|
95
|
+
required.every((feature) => granted.includes(feature)),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (name === 'em') return {}
|
|
99
|
+
if (name === 'eventBus') return { emitEvent: emitEventMock }
|
|
100
|
+
return null
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
emitEventMock.mockResolvedValue(undefined)
|
|
105
|
+
setGlobalEventBus({
|
|
106
|
+
emit: (eventId, payload, options) => emitEventMock(eventId, payload, options),
|
|
107
|
+
})
|
|
108
|
+
repoSetStatusMock.mockImplementation(
|
|
109
|
+
async (id: string, status: string, _scope: unknown, extra?: any) => {
|
|
110
|
+
return {
|
|
111
|
+
...makeRow({
|
|
112
|
+
id,
|
|
113
|
+
status,
|
|
114
|
+
executionResult: extra?.executionResult ?? null,
|
|
115
|
+
resolvedAt: extra?.now ?? new Date(),
|
|
116
|
+
resolvedByUserId: extra?.resolvedByUserId ?? null,
|
|
117
|
+
}),
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
consoleErrorSpy.mockRestore()
|
|
125
|
+
consoleWarnSpy.mockRestore()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns 401 when unauthenticated', async () => {
|
|
129
|
+
authMock.mockResolvedValueOnce(null)
|
|
130
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
131
|
+
expect(response.status).toBe(401)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('happy path: pending → cancelled returns 200 with pendingAction.status === cancelled', async () => {
|
|
135
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow())
|
|
136
|
+
|
|
137
|
+
const response = await POST(buildRequest({ reason: 'Wrong price' }) as any, buildContext('pa_123'))
|
|
138
|
+
expect(response.status).toBe(200)
|
|
139
|
+
const body = await response.json()
|
|
140
|
+
expect(body.ok).toBe(true)
|
|
141
|
+
expect(body.pendingAction.status).toBe('cancelled')
|
|
142
|
+
expect(body.pendingAction.executionResult).toEqual({
|
|
143
|
+
error: { code: 'cancelled_by_user', message: 'Wrong price' },
|
|
144
|
+
})
|
|
145
|
+
expect(repoSetStatusMock).toHaveBeenCalledTimes(1)
|
|
146
|
+
const [, nextStatus, , extra] = repoSetStatusMock.mock.calls[0]
|
|
147
|
+
expect(nextStatus).toBe('cancelled')
|
|
148
|
+
expect(extra).toMatchObject({ resolvedByUserId: 'user-1' })
|
|
149
|
+
expect(emitEventMock).toHaveBeenCalledTimes(1)
|
|
150
|
+
expect(emitEventMock.mock.calls[0][0]).toBe('ai.action.cancelled')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('idempotent: second cancel on cancelled row returns 200 + same row without re-emitting event', async () => {
|
|
154
|
+
const cancelledRow = makeRow({
|
|
155
|
+
status: 'cancelled',
|
|
156
|
+
resolvedAt: new Date('2026-04-18T10:30:00.000Z'),
|
|
157
|
+
resolvedByUserId: 'user-1',
|
|
158
|
+
executionResult: { error: { code: 'cancelled_by_user', message: 'Cancelled by user' } },
|
|
159
|
+
})
|
|
160
|
+
repoGetByIdMock.mockResolvedValueOnce(cancelledRow)
|
|
161
|
+
|
|
162
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
163
|
+
expect(response.status).toBe(200)
|
|
164
|
+
const body = await response.json()
|
|
165
|
+
expect(body.ok).toBe(true)
|
|
166
|
+
expect(body.pendingAction.status).toBe('cancelled')
|
|
167
|
+
expect(repoSetStatusMock).not.toHaveBeenCalled()
|
|
168
|
+
expect(emitEventMock).not.toHaveBeenCalled()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('409 expired: expiresAt in the past flips to expired and returns 409', async () => {
|
|
172
|
+
const expiredRow = makeRow({ expiresAt: new Date('2020-01-01T00:00:00.000Z') })
|
|
173
|
+
repoGetByIdMock.mockResolvedValueOnce(expiredRow)
|
|
174
|
+
|
|
175
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
176
|
+
expect(response.status).toBe(409)
|
|
177
|
+
const body = await response.json()
|
|
178
|
+
expect(body.code).toBe('expired')
|
|
179
|
+
// expired branch performs a setStatus('expired', ...) + emits ai.action.expired
|
|
180
|
+
expect(repoSetStatusMock).toHaveBeenCalledTimes(1)
|
|
181
|
+
const [, nextStatus] = repoSetStatusMock.mock.calls[0]
|
|
182
|
+
expect(nextStatus).toBe('expired')
|
|
183
|
+
expect(emitEventMock).toHaveBeenCalledTimes(1)
|
|
184
|
+
expect(emitEventMock.mock.calls[0][0]).toBe('ai.action.expired')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('409 invalid_status: already confirmed', async () => {
|
|
188
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow({ status: 'confirmed' }))
|
|
189
|
+
|
|
190
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
191
|
+
expect(response.status).toBe(409)
|
|
192
|
+
const body = await response.json()
|
|
193
|
+
expect(body.code).toBe('invalid_status')
|
|
194
|
+
expect(repoSetStatusMock).not.toHaveBeenCalled()
|
|
195
|
+
expect(emitEventMock).not.toHaveBeenCalled()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('409 invalid_status: already executing', async () => {
|
|
199
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow({ status: 'executing' }))
|
|
200
|
+
|
|
201
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
202
|
+
expect(response.status).toBe(409)
|
|
203
|
+
const body = await response.json()
|
|
204
|
+
expect(body.code).toBe('invalid_status')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('409 invalid_status: already failed', async () => {
|
|
208
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow({ status: 'failed' }))
|
|
209
|
+
|
|
210
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
211
|
+
expect(response.status).toBe(409)
|
|
212
|
+
const body = await response.json()
|
|
213
|
+
expect(body.code).toBe('invalid_status')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('404 pending_action_not_found for cross-tenant / unknown id', async () => {
|
|
217
|
+
repoGetByIdMock.mockResolvedValueOnce(null)
|
|
218
|
+
|
|
219
|
+
const response = await POST(buildRequest() as any, buildContext('pa_missing'))
|
|
220
|
+
expect(response.status).toBe(404)
|
|
221
|
+
const body = await response.json()
|
|
222
|
+
expect(body.code).toBe('pending_action_not_found')
|
|
223
|
+
expect(repoSetStatusMock).not.toHaveBeenCalled()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('403 forbidden when caller lacks ai_assistant.view', async () => {
|
|
227
|
+
loadAclMock.mockResolvedValueOnce({ features: ['catalog.view'], isSuperAdmin: false })
|
|
228
|
+
|
|
229
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
230
|
+
expect(response.status).toBe(403)
|
|
231
|
+
const body = await response.json()
|
|
232
|
+
expect(body.code).toBe('forbidden')
|
|
233
|
+
expect(repoGetByIdMock).not.toHaveBeenCalled()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('whitespace-only reason becomes empty → default "Cancelled by user" message', async () => {
|
|
237
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow())
|
|
238
|
+
|
|
239
|
+
const response = await POST(buildRequest({ reason: ' \t\n ' }) as any, buildContext('pa_123'))
|
|
240
|
+
expect(response.status).toBe(200)
|
|
241
|
+
const body = await response.json()
|
|
242
|
+
expect(body.pendingAction.executionResult).toEqual({
|
|
243
|
+
error: { code: 'cancelled_by_user', message: 'Cancelled by user' },
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('400 validation_error when reason exceeds 500 characters', async () => {
|
|
248
|
+
const longReason = 'x'.repeat(501)
|
|
249
|
+
const response = await POST(buildRequest({ reason: longReason }) as any, buildContext('pa_123'))
|
|
250
|
+
expect(response.status).toBe(400)
|
|
251
|
+
const body = await response.json()
|
|
252
|
+
expect(body.code).toBe('validation_error')
|
|
253
|
+
expect(repoGetByIdMock).not.toHaveBeenCalled()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('400 validation_error when body contains unknown fields', async () => {
|
|
257
|
+
const response = await POST(
|
|
258
|
+
buildRequest({ reason: 'ok', evil: 'payload' }) as any,
|
|
259
|
+
buildContext('pa_123'),
|
|
260
|
+
)
|
|
261
|
+
expect(response.status).toBe(400)
|
|
262
|
+
const body = await response.json()
|
|
263
|
+
expect(body.code).toBe('validation_error')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('accepts an empty body (no reason)', async () => {
|
|
267
|
+
repoGetByIdMock.mockResolvedValueOnce(makeRow())
|
|
268
|
+
|
|
269
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
270
|
+
expect(response.status).toBe(200)
|
|
271
|
+
const body = await response.json()
|
|
272
|
+
expect(body.ok).toBe(true)
|
|
273
|
+
expect(body.pendingAction.executionResult).toEqual({
|
|
274
|
+
error: { code: 'cancelled_by_user', message: 'Cancelled by user' },
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('500 cancel_internal_error when the repo throws unexpectedly', async () => {
|
|
279
|
+
repoGetByIdMock.mockRejectedValueOnce(new Error('db down'))
|
|
280
|
+
|
|
281
|
+
const response = await POST(buildRequest() as any, buildContext('pa_123'))
|
|
282
|
+
expect(response.status).toBe(500)
|
|
283
|
+
const body = await response.json()
|
|
284
|
+
expect(body.code).toBe('cancel_internal_error')
|
|
285
|
+
})
|
|
286
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { AiPendingActionRepository } from '../../../../../data/repositories/AiPendingActionRepository'
|
|
9
|
+
import { hasRequiredFeatures } from '../../../../../lib/auth'
|
|
10
|
+
import { serializePendingActionForClient } from '../../../../../lib/pending-action-client'
|
|
11
|
+
import { checkStatusAndExpiry } from '../../../../../lib/pending-action-recheck'
|
|
12
|
+
import { executePendingActionCancel } from '../../../../../lib/pending-action-cancel'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* POST `/api/ai/actions/:id/cancel` — mutation approval gate cancel
|
|
16
|
+
* endpoint (spec §9.4, Step 5.9).
|
|
17
|
+
*
|
|
18
|
+
* Siblings the Step 5.8 confirm route: flips `pending → cancelled` and
|
|
19
|
+
* emits `ai.action.cancelled`. The tool handler is NEVER invoked.
|
|
20
|
+
*
|
|
21
|
+
* The route re-uses only the `status + expiry + tenant-scope` guards
|
|
22
|
+
* from `pending-action-recheck.ts` — the agent / tool / attachment /
|
|
23
|
+
* record-version guards are confirm-only. A caller may cancel even when
|
|
24
|
+
* they'd be blocked from confirming; cancelling does not touch data.
|
|
25
|
+
*
|
|
26
|
+
* Idempotency: calling this endpoint twice on an already-`cancelled`
|
|
27
|
+
* row returns 200 with the current row without re-emitting the event.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const REQUIRED_FEATURE = 'ai_assistant.view'
|
|
31
|
+
|
|
32
|
+
const idParamSchema = z.object({
|
|
33
|
+
id: z
|
|
34
|
+
.string()
|
|
35
|
+
.trim()
|
|
36
|
+
.min(1, 'id must be a non-empty string')
|
|
37
|
+
.max(128, 'id exceeds the maximum length of 128 characters'),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const bodySchema = z
|
|
41
|
+
.object({
|
|
42
|
+
reason: z
|
|
43
|
+
.string()
|
|
44
|
+
.max(500, 'reason must be at most 500 characters')
|
|
45
|
+
.optional(),
|
|
46
|
+
})
|
|
47
|
+
.strict()
|
|
48
|
+
.optional()
|
|
49
|
+
|
|
50
|
+
export const openApi: OpenApiRouteDoc = {
|
|
51
|
+
tag: 'AI Assistant',
|
|
52
|
+
summary: 'Pending action (mutation approval gate) cancel',
|
|
53
|
+
methods: {
|
|
54
|
+
POST: {
|
|
55
|
+
operationId: 'aiAssistantCancelPendingAction',
|
|
56
|
+
summary: 'Cancel an AI pending action without executing the wrapped tool.',
|
|
57
|
+
description:
|
|
58
|
+
'Flips a pending AI action from `pending` to `cancelled` and emits the ' +
|
|
59
|
+
'`ai.action.cancelled` event. The tool handler is never invoked. Idempotent: ' +
|
|
60
|
+
'a second call on a row already in `cancelled` status returns 200 with the ' +
|
|
61
|
+
'current row without re-emitting the event. Rows whose `expiresAt` is in the ' +
|
|
62
|
+
'past are flipped to `expired` and returned as 409 `expired` so the client can ' +
|
|
63
|
+
'surface the TTL loss instead of silently masking it as a cancellation.',
|
|
64
|
+
responses: [
|
|
65
|
+
{
|
|
66
|
+
status: 200,
|
|
67
|
+
description: 'Cancellation complete (or idempotent replay); body includes the serialized pending action with status `cancelled`.',
|
|
68
|
+
mediaType: 'application/json',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
errors: [
|
|
72
|
+
{ status: 400, description: 'Invalid cancel request body (unknown field, reason exceeds 500 chars, wrong type).' },
|
|
73
|
+
{ status: 401, description: 'Unauthenticated caller.' },
|
|
74
|
+
{ status: 403, description: 'Caller lacks `ai_assistant.view`.' },
|
|
75
|
+
{ status: 404, description: 'Pending action not found in the caller scope.' },
|
|
76
|
+
{ status: 409, description: 'Pending action is not in `pending` status (already confirmed/failed/executing) or has expired.' },
|
|
77
|
+
{ status: 500, description: 'Unexpected server failure during cancel.' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const metadata = {
|
|
84
|
+
POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface RouteContext {
|
|
88
|
+
params: Promise<{ id: string }>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function jsonError(
|
|
92
|
+
status: number,
|
|
93
|
+
message: string,
|
|
94
|
+
code: string,
|
|
95
|
+
extra?: Record<string, unknown>,
|
|
96
|
+
): NextResponse {
|
|
97
|
+
return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function readRequestBody(req: NextRequest): Promise<unknown> {
|
|
101
|
+
try {
|
|
102
|
+
const text = await req.text()
|
|
103
|
+
if (!text || text.trim().length === 0) return undefined
|
|
104
|
+
return JSON.parse(text)
|
|
105
|
+
} catch {
|
|
106
|
+
return Symbol.for('ai_assistant.cancel.bad_json')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function POST(req: NextRequest, context: RouteContext): Promise<Response> {
|
|
111
|
+
const auth = await getAuthFromRequest(req)
|
|
112
|
+
if (!auth) {
|
|
113
|
+
return jsonError(401, 'Unauthorized', 'unauthenticated')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rawParams = await context.params
|
|
117
|
+
const paramResult = idParamSchema.safeParse(rawParams)
|
|
118
|
+
if (!paramResult.success) {
|
|
119
|
+
return jsonError(400, 'Invalid pending action id.', 'validation_error', {
|
|
120
|
+
issues: paramResult.error.issues,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
const pendingActionId = paramResult.data.id
|
|
124
|
+
|
|
125
|
+
const rawBody = await readRequestBody(req)
|
|
126
|
+
if (rawBody === Symbol.for('ai_assistant.cancel.bad_json')) {
|
|
127
|
+
return jsonError(400, 'Invalid JSON body.', 'validation_error')
|
|
128
|
+
}
|
|
129
|
+
const bodyResult = bodySchema.safeParse(rawBody)
|
|
130
|
+
if (!bodyResult.success) {
|
|
131
|
+
return jsonError(400, 'Invalid cancel body.', 'validation_error', {
|
|
132
|
+
issues: bodyResult.error.issues,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
const parsedBody = bodyResult.data ?? {}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const container = await createRequestContainer()
|
|
139
|
+
const rbacService = container.resolve<RbacService>('rbacService')
|
|
140
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
141
|
+
tenantId: auth.tenantId,
|
|
142
|
+
organizationId: auth.orgId,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
|
|
146
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, 'forbidden')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!auth.tenantId) {
|
|
150
|
+
return jsonError(
|
|
151
|
+
404,
|
|
152
|
+
`No pending action "${pendingActionId}" accessible to the caller.`,
|
|
153
|
+
'pending_action_not_found',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const em = container.resolve<EntityManager>('em')
|
|
158
|
+
const repo = new AiPendingActionRepository(em)
|
|
159
|
+
const row = await repo.getById(pendingActionId, {
|
|
160
|
+
tenantId: auth.tenantId,
|
|
161
|
+
organizationId: auth.orgId ?? null,
|
|
162
|
+
userId: auth.sub,
|
|
163
|
+
})
|
|
164
|
+
if (!row) {
|
|
165
|
+
return jsonError(
|
|
166
|
+
404,
|
|
167
|
+
`No pending action "${pendingActionId}" accessible to the caller.`,
|
|
168
|
+
'pending_action_not_found',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Idempotent replay: second cancel on an already-cancelled row returns
|
|
173
|
+
// 200 with the current row and does NOT emit a second event.
|
|
174
|
+
if (row.status === 'cancelled') {
|
|
175
|
+
return NextResponse.json({
|
|
176
|
+
ok: true,
|
|
177
|
+
pendingAction: serializePendingActionForClient(row),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const statusCheck = checkStatusAndExpiry(row)
|
|
182
|
+
if (!statusCheck.ok) {
|
|
183
|
+
// Expired short-circuit: flip to `expired` + emit `ai.action.expired`
|
|
184
|
+
// so Step 5.12 does not race to do it. Return 409 so the client
|
|
185
|
+
// surfaces the TTL loss rather than silently cancelling.
|
|
186
|
+
if (statusCheck.code === 'expired') {
|
|
187
|
+
const cancelResult = await executePendingActionCancel({
|
|
188
|
+
action: row,
|
|
189
|
+
ctx: {
|
|
190
|
+
tenantId: auth.tenantId,
|
|
191
|
+
organizationId: auth.orgId ?? null,
|
|
192
|
+
userId: auth.sub,
|
|
193
|
+
container,
|
|
194
|
+
},
|
|
195
|
+
repo,
|
|
196
|
+
})
|
|
197
|
+
return jsonError(409, statusCheck.message, 'expired', {
|
|
198
|
+
pendingAction: serializePendingActionForClient(cancelResult.row),
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
return jsonError(statusCheck.status, statusCheck.message, statusCheck.code, statusCheck.extra)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const cancelResult = await executePendingActionCancel({
|
|
205
|
+
action: row,
|
|
206
|
+
ctx: {
|
|
207
|
+
tenantId: auth.tenantId,
|
|
208
|
+
organizationId: auth.orgId ?? null,
|
|
209
|
+
userId: auth.sub,
|
|
210
|
+
container,
|
|
211
|
+
},
|
|
212
|
+
reason: parsedBody.reason,
|
|
213
|
+
repo,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (cancelResult.status === 'expired') {
|
|
217
|
+
return jsonError(
|
|
218
|
+
409,
|
|
219
|
+
'Pending action has expired. The model must re-propose the mutation.',
|
|
220
|
+
'expired',
|
|
221
|
+
{ pendingAction: serializePendingActionForClient(cancelResult.row) },
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return NextResponse.json({
|
|
226
|
+
ok: true,
|
|
227
|
+
pendingAction: serializePendingActionForClient(cancelResult.row),
|
|
228
|
+
})
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('[AI Pending Action CANCEL] Failure:', error)
|
|
231
|
+
return jsonError(
|
|
232
|
+
500,
|
|
233
|
+
error instanceof Error ? error.message : 'Failed to cancel pending action.',
|
|
234
|
+
'cancel_internal_error',
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
}
|