@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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/attachment-parts.ts"],
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n AiAgentAcceptedMediaType,\n AiAgentDefinition,\n} from './ai-agent-definition'\nimport type {\n AiChatRequestContext,\n AiResolvedAttachmentPart,\n} from './attachment-bridge-types'\n\n// Provider-native inline byte limit. Most AI providers accept inline image/PDF\n// payloads comfortably under 4 MB; anything larger SHOULD travel as a short-lived\n// signed URL (see AttachmentSigner below). Above this ceiling and with no signer\n// configured, the helper downgrades to `metadata-only` so the model at least sees\n// that the attachment exists.\nconst DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024\n\n// Extracted text cap. The `content` column on the `attachments` table is the\n// OCR/text-extraction output; we forward it verbatim up to this character count\n// so the system prompt + messages combined do not blow past model context\n// limits. Truncation is signaled to the model via a trailing `[... truncated]`\n// marker.\nconst DEFAULT_MAX_TEXT_CHARS = 64 * 1024\n\n/**\n * Optional attachment-signer. When the DI container resolves a value under\n * `attachmentSigner`, the resolver uses it to mint a short-lived URL for\n * images/PDFs that exceed the inline-bytes threshold. Phase 1 does not ship a\n * concrete signer; the hook exists so the `signed-url` branch of\n * {@link AiResolvedAttachmentPart} is reachable as soon as a provider wires one\n * up without requiring another runtime change.\n */\nexport interface AttachmentSigner {\n sign(input: {\n attachmentId: string\n fileName: string\n mediaType: string\n tenantId: string | null\n organizationId: string | null\n }): Promise<string | null>\n}\n\nexport interface ResolveAttachmentPartsInput {\n attachmentIds: readonly string[]\n authContext: AiChatRequestContext\n acceptedMediaTypes?: readonly AiAgentAcceptedMediaType[]\n container?: AwilixContainer\n /**\n * Optional override for the inline bytes threshold. Callers SHOULD leave\n * this untouched; the default tracks a safe cross-provider ceiling.\n */\n maxInlineBytes?: number\n /**\n * Optional override for the extracted-text character cap.\n */\n maxTextChars?: number\n}\n\nfunction classifyMediaType(mimeType: string | null | undefined): AiAgentAcceptedMediaType {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (normalized.startsWith('image/')) return 'image'\n if (normalized === 'application/pdf') return 'pdf'\n return 'file'\n}\n\nfunction isTextLikeMime(mimeType: string | null | undefined): boolean {\n const normalized = (mimeType ?? '').toLowerCase().trim()\n if (!normalized) return false\n if (normalized.startsWith('text/')) return true\n if (normalized === 'application/json') return true\n if (normalized === 'application/xml') return true\n if (normalized === 'application/x-yaml' || normalized === 'text/yaml') return true\n if (normalized === 'application/csv') return true\n return false\n}\n\nfunction truncateText(value: string, maxChars: number): string {\n if (value.length <= maxChars) return value\n return `${value.slice(0, Math.max(0, maxChars - 16))}\\n[... truncated]`\n}\n\nfunction resolveEm(container: AwilixContainer | undefined): EntityManager | null {\n if (!container) return null\n try {\n const candidate = container.resolve('em') as EntityManager | undefined\n return candidate ?? null\n } catch {\n return null\n }\n}\n\nfunction resolveSigner(container: AwilixContainer | undefined): AttachmentSigner | null {\n if (!container) return null\n try {\n const candidate = container.resolve('attachmentSigner') as AttachmentSigner | undefined\n if (candidate && typeof candidate.sign === 'function') {\n return candidate\n }\n } catch {\n return null\n }\n return null\n}\n\ntype AttachmentRow = {\n id: string\n entityId: string\n fileName: string\n mimeType: string\n fileSize: number\n storagePath: string\n storageDriver: string\n partitionCode: string\n tenantId: string | null\n organizationId: string | null\n content: string | null\n}\n\nasync function loadAttachmentRow(\n em: EntityManager,\n attachmentId: string,\n authContext: AiChatRequestContext,\n): Promise<AttachmentRow | null> {\n // Attachment entity is imported lazily to keep ai-assistant isomorphic \u2014 the\n // core package owns the MikroORM metadata and is the only place tests would\n // need to bootstrap for real DB access.\n const { Attachment } = await import('@open-mercato/core/modules/attachments/data/entities')\n const record = await findOneWithDecryption(\n em,\n Attachment as never,\n { id: attachmentId } as never,\n undefined,\n {\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n },\n )\n if (!record) return null\n const row = record as unknown as AttachmentRow\n return {\n id: row.id,\n entityId: row.entityId,\n fileName: row.fileName,\n mimeType: row.mimeType,\n fileSize: row.fileSize,\n storagePath: row.storagePath,\n storageDriver: row.storageDriver,\n partitionCode: row.partitionCode,\n tenantId: row.tenantId ?? null,\n organizationId: row.organizationId ?? null,\n content: row.content ?? null,\n }\n}\n\nfunction rowBelongsToCaller(row: AttachmentRow, authContext: AiChatRequestContext): boolean {\n if (authContext.isSuperAdmin) return true\n // Tenant scope: if the record is tenant-scoped, it MUST match the caller tenant.\n if (row.tenantId && row.tenantId !== authContext.tenantId) return false\n // Organization scope: if the record is org-scoped, it MUST match the caller org.\n if (row.organizationId && row.organizationId !== authContext.organizationId) return false\n return true\n}\n\nasync function readAttachmentBytes(row: AttachmentRow): Promise<Uint8Array | null> {\n const { resolveAttachmentAbsolutePath } = await import(\n '@open-mercato/core/modules/attachments/lib/storage'\n )\n const absolutePath = resolveAttachmentAbsolutePath(\n row.partitionCode,\n row.storagePath,\n row.storageDriver,\n )\n try {\n const buffer = await fs.readFile(absolutePath)\n return new Uint8Array(buffer)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to read attachment ${row.id} from storage; falling back to metadata-only:`,\n error,\n )\n return null\n }\n}\n\nasync function classifyAndBuildPart(\n row: AttachmentRow,\n mediaClass: AiAgentAcceptedMediaType,\n maxInlineBytes: number,\n maxTextChars: number,\n signer: AttachmentSigner | null,\n authContext: AiChatRequestContext,\n): Promise<AiResolvedAttachmentPart> {\n const base: Pick<AiResolvedAttachmentPart, 'attachmentId' | 'fileName' | 'mediaType'> = {\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType || 'application/octet-stream',\n }\n\n // Text-like generic files \u2014 use the pre-extracted content column if present.\n if (mediaClass === 'file' && isTextLikeMime(row.mimeType) && typeof row.content === 'string' && row.content.length > 0) {\n return {\n ...base,\n source: 'text',\n textContent: truncateText(row.content, maxTextChars),\n }\n }\n\n // Images + PDFs \u2014 prefer inline bytes when small enough; otherwise signed URL\n // if the container registered an attachmentSigner; otherwise metadata-only.\n if (mediaClass === 'image' || mediaClass === 'pdf') {\n if (row.fileSize > 0 && row.fileSize <= maxInlineBytes) {\n const bytes = await readAttachmentBytes(row)\n if (bytes) {\n return {\n ...base,\n source: 'bytes',\n data: bytes,\n }\n }\n }\n if (signer) {\n try {\n const url = await signer.sign({\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType,\n tenantId: authContext.tenantId,\n organizationId: authContext.organizationId,\n })\n if (typeof url === 'string' && url.length > 0) {\n return {\n ...base,\n source: 'signed-url',\n url,\n }\n }\n } catch (error) {\n console.warn(\n `[AI Agents] attachmentSigner failed for ${row.id}; falling back to metadata-only:`,\n error,\n )\n }\n }\n return { ...base, source: 'metadata-only' }\n }\n\n // Generic file without extracted text \u2014 metadata-only so the model at least\n // knows the attachment is present.\n return { ...base, source: 'metadata-only' }\n}\n\n/**\n * Resolves each `attachmentId` into a model-ready {@link AiResolvedAttachmentPart}.\n *\n * Contract:\n *\n * - Tenant/org scope is enforced: records that don't belong to the caller are\n * dropped with a `console.warn`. Super-admin callers bypass the scope check.\n * - When the agent declares `acceptedMediaTypes`, parts whose classified media\n * type is not in the whitelist are dropped with a `console.warn`.\n * `acceptedMediaTypes: undefined` means \"no filter\".\n * - When the DI container is missing or the attachments service is\n * unavailable, the helper returns `[]` with a single `console.warn` and\n * does NOT throw \u2014 the caller's `attachmentIds` pass-through to\n * {@link resolveAiAgentTools} remains the Step 3.6 parity behavior.\n * - The returned parts are ordered to match `attachmentIds`. Any id that\n * cannot be resolved (not found, out-of-scope, unreadable) is silently\n * dropped from the result \u2014 the caller observes a shorter list.\n */\nexport async function resolveAttachmentParts(\n input: ResolveAttachmentPartsInput,\n): Promise<AiResolvedAttachmentPart[]> {\n const ids = Array.from(input.attachmentIds ?? [])\n if (ids.length === 0) return []\n\n const em = resolveEm(input.container)\n if (!em) {\n console.warn(\n '[AI Agents] resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution.',\n )\n return []\n }\n\n const maxInlineBytes = input.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES\n const maxTextChars = input.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS\n const signer = resolveSigner(input.container)\n const acceptedSet = input.acceptedMediaTypes\n ? new Set<AiAgentAcceptedMediaType>(input.acceptedMediaTypes)\n : null\n\n const parts: AiResolvedAttachmentPart[] = []\n for (const id of ids) {\n if (typeof id !== 'string' || id.length === 0) continue\n let row: AttachmentRow | null\n try {\n row = await loadAttachmentRow(em, id, input.authContext)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to load attachment ${id}; skipping:`,\n error,\n )\n continue\n }\n if (!row) {\n console.warn(`[AI Agents] Attachment ${id} not found; skipping.`)\n continue\n }\n if (!rowBelongsToCaller(row, input.authContext)) {\n console.warn(\n `[AI Agents] Attachment ${id} is out of scope for caller (tenant=${input.authContext.tenantId}, org=${input.authContext.organizationId}); skipping.`,\n )\n continue\n }\n const mediaClass = classifyMediaType(row.mimeType)\n if (acceptedSet && !acceptedSet.has(mediaClass)) {\n console.warn(\n `[AI Agents] Attachment ${id} (${row.mimeType}) is not in agent acceptedMediaTypes=${[...acceptedSet].join(',')}; skipping.`,\n )\n continue\n }\n try {\n const part = await classifyAndBuildPart(\n row,\n mediaClass,\n maxInlineBytes,\n maxTextChars,\n signer,\n input.authContext,\n )\n parts.push(part)\n } catch (error) {\n console.warn(\n `[AI Agents] Failed to build attachment part for ${id}; skipping:`,\n error,\n )\n }\n }\n\n return parts\n}\n\n/**\n * Helper used by {@link ./agent-runtime} to fan out attachment resolution for\n * an agent. Kept separate so the runtime helpers share identical semantics\n * (Step 3.6 parity invariant #7 widened: resolved parts flow into both the\n * chat and object paths through the same code).\n */\nexport async function resolveAttachmentPartsForAgent(input: {\n agent: AiAgentDefinition\n attachmentIds: readonly string[] | undefined\n authContext: AiChatRequestContext\n container?: AwilixContainer\n}): Promise<AiResolvedAttachmentPart[]> {\n if (!input.attachmentIds || input.attachmentIds.length === 0) return []\n return resolveAttachmentParts({\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n acceptedMediaTypes: input.agent.acceptedMediaTypes,\n container: input.container,\n })\n}\n\n/**\n * Converts resolved attachment parts into AI SDK v6 `FileUIPart` shapes so\n * they can be appended to the last user `UIMessage.parts`. `metadata-only`\n * parts are dropped \u2014 there is no provider-safe file-part shape for them;\n * their presence is surfaced through the system prompt instead by\n * {@link summarizeAttachmentPartsForPrompt}.\n */\nexport function attachmentPartsToUiFileParts(\n parts: readonly AiResolvedAttachmentPart[],\n): Array<{ type: 'file'; mediaType: string; filename: string; url: string }> {\n const output: Array<{ type: 'file'; mediaType: string; filename: string; url: string }> = []\n for (const part of parts) {\n if (part.source === 'bytes' && part.data) {\n const base64 = toBase64(part.data)\n if (base64) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: `data:${part.mediaType};base64,${base64}`,\n })\n }\n continue\n }\n if (part.source === 'signed-url' && typeof part.url === 'string' && part.url.length > 0) {\n output.push({\n type: 'file',\n mediaType: part.mediaType,\n filename: part.fileName,\n url: part.url,\n })\n }\n }\n return output\n}\n\n/**\n * Renders a compact, human-readable attachment summary to append to the\n * system prompt. Covers `text`, `metadata-only`, and as a fallback the\n * `bytes`/`signed-url` kinds so the model can always reason about which\n * attachments are in scope. Keeping this as a string keeps provider-agnostic\n * behavior \u2014 object-mode and chat-mode both consume the same surface.\n */\nexport function summarizeAttachmentPartsForPrompt(\n parts: readonly AiResolvedAttachmentPart[],\n): string | null {\n if (parts.length === 0) return null\n const lines: string[] = ['[ATTACHMENTS]']\n for (const part of parts) {\n const header = `- ${part.fileName} (${part.mediaType}, source=${part.source})`\n if (part.source === 'text' && typeof part.textContent === 'string' && part.textContent.length > 0) {\n lines.push(header)\n lines.push(part.textContent)\n } else {\n lines.push(header)\n }\n }\n return lines.join('\\n')\n}\n\nfunction toBase64(data: Uint8Array | string): string | null {\n if (typeof data === 'string') return data\n try {\n return Buffer.from(data).toString('base64')\n } catch {\n return null\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
2
|
+
class AiModelFactoryError extends Error {
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "AiModelFactoryError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function normalizeOverride(value) {
|
|
10
|
+
if (typeof value !== "string") return null;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
function moduleEnvVarName(moduleId) {
|
|
15
|
+
return `${moduleId.toUpperCase()}_AI_MODEL`;
|
|
16
|
+
}
|
|
17
|
+
function createModelFactory(_container, deps = {}) {
|
|
18
|
+
const registry = deps.registry ?? llmProviderRegistry;
|
|
19
|
+
const env = deps.env ?? process.env;
|
|
20
|
+
return {
|
|
21
|
+
resolveModel(input) {
|
|
22
|
+
const provider = registry.resolveFirstConfigured({ env });
|
|
23
|
+
if (!provider) {
|
|
24
|
+
throw new AiModelFactoryError(
|
|
25
|
+
"no_provider_configured",
|
|
26
|
+
"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."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const apiKey = provider.resolveApiKey(env);
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
throw new AiModelFactoryError(
|
|
32
|
+
"api_key_missing",
|
|
33
|
+
`LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const callerOverride = normalizeOverride(input.callerOverride);
|
|
37
|
+
const moduleEnvOverride = input.moduleId && input.moduleId.length > 0 ? normalizeOverride(env[moduleEnvVarName(input.moduleId)]) : null;
|
|
38
|
+
const agentDefault = normalizeOverride(input.agentDefaultModel);
|
|
39
|
+
let modelId;
|
|
40
|
+
let source;
|
|
41
|
+
if (callerOverride) {
|
|
42
|
+
modelId = callerOverride;
|
|
43
|
+
source = "caller_override";
|
|
44
|
+
} else if (moduleEnvOverride) {
|
|
45
|
+
modelId = moduleEnvOverride;
|
|
46
|
+
source = "module_env";
|
|
47
|
+
} else if (agentDefault) {
|
|
48
|
+
modelId = agentDefault;
|
|
49
|
+
source = "agent_default";
|
|
50
|
+
} else {
|
|
51
|
+
modelId = provider.defaultModel;
|
|
52
|
+
source = "provider_default";
|
|
53
|
+
}
|
|
54
|
+
const model = provider.createModel({ modelId, apiKey });
|
|
55
|
+
return {
|
|
56
|
+
model,
|
|
57
|
+
modelId,
|
|
58
|
+
providerId: provider.id,
|
|
59
|
+
source
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
AiModelFactoryError,
|
|
66
|
+
createModelFactory
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=model-factory.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/model-factory.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared AI model factory (Phase 3 WS-A \u2014 Step 5.1).\n *\n * Consolidates the previously-per-module model-creation plumbing (inbox_ops's\n * `llmProvider.ts`, the agent-runtime's inline `resolveAgentModel`) behind a\n * single DI-friendly port. Every AI-runtime caller (chat, object, inbox-ops\n * extraction, future agents) resolves the `LanguageModelV1` it hands to the\n * Vercel AI SDK through `createModelFactory(container).resolveModel(...)` so\n * all of them share one resolution order:\n *\n * 1. `callerOverride` (non-empty string) \u2014 highest precedence, e.g. the\n * `modelOverride` field on `runAiAgentText`/`runAiAgentObject`.\n * 2. Env variable `<MODULE>_AI_MODEL` (uppercased `moduleId`) when\n * `moduleId` is provided. Example: `INBOX_OPS_AI_MODEL=claude-haiku-4-5`,\n * `CATALOG_AI_MODEL=gpt-4o-mini`.\n * 3. `agentDefaultModel` \u2014 typically `AiAgentDefinition.defaultModel`.\n * 4. The configured provider's own default model id\n * (`provider.defaultModel`).\n *\n * Resolution walks the `llmProviderRegistry`'s `resolveFirstConfigured()`\n * output so it honors the same env-driven provider discovery that existing\n * callers already rely on. The factory throws {@link AiModelFactoryError}\n * when no provider is configured \u2014 every current call site already expects\n * the throw (see the bare `throw new Error('No LLM provider is configured...')`\n * in `agent-runtime.ts` prior to this Step).\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see packages/ai-assistant/src/modules/ai_assistant/lib/agent-runtime.ts\n * @see packages/core/src/modules/inbox_ops/lib/llmProvider.ts\n */\n\nimport type { AwilixContainer } from 'awilix'\nimport type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\n\n/**\n * Minimal AI SDK LanguageModel shape \u2014 the factory exposes the protocol-\n * agnostic `unknown`-typed return from {@link LlmProvider.createModel} under a\n * dedicated alias so callers can document intent without importing the AI SDK\n * here. Call sites that hand the result to `generateText` / `streamText` /\n * `generateObject` / `streamObject` continue to cast to the SDK's\n * `LanguageModelV1` / `LanguageModel` union exactly as they already do.\n */\nexport type AiModelInstance = unknown\n\n/**\n * Input accepted by {@link AiModelFactory.resolveModel}. All fields are\n * optional \u2014 passing an empty input resolves the provider default.\n */\nexport interface AiModelFactoryInput {\n /**\n * Owning module id (matches `Module.id`). When set, the factory checks\n * `<MODULE>_AI_MODEL` (uppercased) as the env-override source. Example:\n * `moduleId: 'inbox_ops'` \u2192 env var `INBOX_OPS_AI_MODEL`.\n */\n moduleId?: string\n /**\n * Agent-level default, typically `AiAgentDefinition.defaultModel`. Used\n * when neither `callerOverride` nor the module env override is present.\n */\n agentDefaultModel?: string\n /**\n * Per-call override (e.g. `runAiAgentText({ modelOverride })`). Wins over\n * every other source when it is a non-empty trimmed string. Empty strings\n * are treated as \"no override\" so the next source in the chain wins \u2014\n * callers MUST NOT need a separate \"clear override\" API.\n */\n callerOverride?: string\n}\n\n/**\n * Materialized output returned by {@link AiModelFactory.resolveModel}.\n */\nexport interface AiModelResolution {\n /**\n * Concrete AI SDK model instance ready to pass to\n * `generateText`/`streamText`/`generateObject`/`streamObject`. Typed as\n * {@link AiModelInstance} to avoid coupling this port to a specific SDK\n * major version.\n */\n model: AiModelInstance\n /** Resolved upstream model id (e.g. `claude-haiku-4-5-20251001`). */\n modelId: string\n /** Stable provider id from {@link LlmProvider.id}. */\n providerId: string\n /**\n * Which source won resolution. Useful for logs and tests; never exposed\n * as a public contract beyond these four enum values.\n */\n source: 'caller_override' | 'module_env' | 'agent_default' | 'provider_default'\n}\n\n/**\n * Port exposed by {@link createModelFactory}. Stateless \u2014 the factory\n * re-reads the registry + env on every `resolveModel` call so hot-reload\n * and test overrides work without needing factory re-creation.\n */\nexport interface AiModelFactory {\n resolveModel(input: AiModelFactoryInput): AiModelResolution\n}\n\n/**\n * Typed error thrown by the factory when it cannot materialize a model.\n *\n * `code` is a stable string union so downstream callers can branch without\n * parsing error messages. `AiModelFactoryError`s bubble through\n * `runAiAgentText`/`runAiAgentObject` unchanged \u2014 the agent runtime does\n * NOT catch them, matching the pre-Step-5.1 behavior of the inline\n * resolver.\n */\nexport type AiModelFactoryErrorCode =\n | 'no_provider_configured'\n | 'api_key_missing'\n\nexport class AiModelFactoryError extends Error {\n readonly code: AiModelFactoryErrorCode\n\n constructor(code: AiModelFactoryErrorCode, message: string) {\n super(message)\n this.name = 'AiModelFactoryError'\n this.code = code\n }\n}\n\n/**\n * Internal dependencies of the factory. Exposed for tests only; production\n * callers rely on the defaults wired by {@link createModelFactory}.\n */\nexport interface CreateModelFactoryDependencies {\n /**\n * Registry used to resolve the first configured provider. Defaults to the\n * singleton `llmProviderRegistry`.\n */\n registry?: { resolveFirstConfigured: (options?: { env?: EnvLookup }) => LlmProvider | null }\n /** Env lookup for `<MODULE>_AI_MODEL` + provider credentials. */\n env?: EnvLookup\n}\n\nfunction normalizeOverride(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction moduleEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_MODEL`\n}\n\n/**\n * Creates an {@link AiModelFactory} bound to the DI container. The container\n * reference is accepted for API symmetry with other runtime helpers (and so\n * future work can read provider overrides registered on the container); the\n * current implementation only needs the registry + env. No breaking change\n * when later implementations DO consult the container.\n */\nexport function createModelFactory(\n _container: AwilixContainer,\n deps: CreateModelFactoryDependencies = {},\n): AiModelFactory {\n const registry = deps.registry ?? llmProviderRegistry\n const env = deps.env ?? process.env\n\n return {\n resolveModel(input: AiModelFactoryInput): AiModelResolution {\n const provider = registry.resolveFirstConfigured({ env })\n if (!provider) {\n throw new AiModelFactoryError(\n 'no_provider_configured',\n '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.',\n )\n }\n const apiKey = provider.resolveApiKey(env)\n if (!apiKey) {\n throw new AiModelFactoryError(\n 'api_key_missing',\n `LLM provider \"${provider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n\n const callerOverride = normalizeOverride(input.callerOverride)\n const moduleEnvOverride =\n input.moduleId && input.moduleId.length > 0\n ? normalizeOverride(env[moduleEnvVarName(input.moduleId)])\n : null\n const agentDefault = normalizeOverride(input.agentDefaultModel)\n\n let modelId: string\n let source: AiModelResolution['source']\n if (callerOverride) {\n modelId = callerOverride\n source = 'caller_override'\n } else if (moduleEnvOverride) {\n modelId = moduleEnvOverride\n source = 'module_env'\n } else if (agentDefault) {\n modelId = agentDefault\n source = 'agent_default'\n } else {\n modelId = provider.defaultModel\n source = 'provider_default'\n }\n\n const model = provider.createModel({ modelId, apiKey })\n return {\n model,\n modelId,\n providerId: provider.id,\n source,\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAiCA,SAAS,2BAA2B;AAiF7B,MAAM,4BAA4B,MAAM;AAAA,EAG7C,YAAY,MAA+B,SAAiB;AAC1D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAgBA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEA,SAAS,iBAAiB,UAA0B;AAClD,SAAO,GAAG,SAAS,YAAY,CAAC;AAClC;AASO,SAAS,mBACd,YACA,OAAuC,CAAC,GACxB;AAChB,QAAM,WAAW,KAAK,YAAY;AAClC,QAAM,MAAM,KAAK,OAAO,QAAQ;AAEhC,SAAO;AAAA,IACL,aAAa,OAA+C;AAC1D,YAAM,WAAW,SAAS,uBAAuB,EAAE,IAAI,CAAC;AACxD,UAAI,CAAC,UAAU;AACb,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA,YAAM,SAAS,SAAS,cAAc,GAAG;AACzC,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI;AAAA,UACR;AAAA,UACA,iBAAiB,SAAS,EAAE;AAAA,QAC9B;AAAA,MACF;AAEA,YAAM,iBAAiB,kBAAkB,MAAM,cAAc;AAC7D,YAAM,oBACJ,MAAM,YAAY,MAAM,SAAS,SAAS,IACtC,kBAAkB,IAAI,iBAAiB,MAAM,QAAQ,CAAC,CAAC,IACvD;AACN,YAAM,eAAe,kBAAkB,MAAM,iBAAiB;AAE9D,UAAI;AACJ,UAAI;AACJ,UAAI,gBAAgB;AAClB,kBAAU;AACV,iBAAS;AAAA,MACX,WAAW,mBAAmB;AAC5B,kBAAU;AACV,iBAAS;AAAA,MACX,WAAW,cAAc;AACvB,kBAAU;AACV,iBAAS;AAAA,MACX,OAAO;AACL,kBAAU,SAAS;AACnB,iBAAS;AAAA,MACX;AAEA,YAAM,QAAQ,SAAS,YAAY,EAAE,SAAS,OAAO,CAAC;AACtD,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,YAAY,SAAS;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AiPendingActionRepository } from "../data/repositories/AiPendingActionRepository.js";
|
|
2
|
+
import { emitAiAssistantEvent } from "../events.js";
|
|
3
|
+
const CANCELLED_EVENT_ID = "ai.action.cancelled";
|
|
4
|
+
const EXPIRED_EVENT_ID = "ai.action.expired";
|
|
5
|
+
const defaultCancelEmitter = async (eventId, payload) => {
|
|
6
|
+
await emitAiAssistantEvent(eventId, payload, {
|
|
7
|
+
persistent: true
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
async function emitEventSafe(emitter, eventId, payload) {
|
|
11
|
+
try {
|
|
12
|
+
await emitter(eventId, payload);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.warn(`[AI Pending Action] Failed to emit ${eventId}:`, error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function executePendingActionCancel(input) {
|
|
18
|
+
const { action, ctx, now } = input;
|
|
19
|
+
const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve("em"));
|
|
20
|
+
const scope = {
|
|
21
|
+
tenantId: ctx.tenantId,
|
|
22
|
+
organizationId: ctx.organizationId,
|
|
23
|
+
userId: ctx.userId
|
|
24
|
+
};
|
|
25
|
+
const clock = now ?? /* @__PURE__ */ new Date();
|
|
26
|
+
const emitter = input.emitEvent ?? defaultCancelEmitter;
|
|
27
|
+
if (action.status === "cancelled") {
|
|
28
|
+
return { row: action, status: "cancelled" };
|
|
29
|
+
}
|
|
30
|
+
const expiresAt = action.expiresAt instanceof Date ? action.expiresAt : new Date(action.expiresAt);
|
|
31
|
+
if (expiresAt.getTime() <= clock.getTime()) {
|
|
32
|
+
const expiredRow = await repo.setStatus(action.id, "expired", scope, { now: clock });
|
|
33
|
+
const resolvedAtIso = (expiredRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString();
|
|
34
|
+
const expiresAtIso = (expiresAt ?? clock).toISOString?.() ?? new Date(clock).toISOString();
|
|
35
|
+
const expiredPayload = {
|
|
36
|
+
pendingActionId: expiredRow.id,
|
|
37
|
+
agentId: expiredRow.agentId,
|
|
38
|
+
toolName: expiredRow.toolName,
|
|
39
|
+
status: expiredRow.status,
|
|
40
|
+
tenantId: ctx.tenantId,
|
|
41
|
+
organizationId: ctx.organizationId ?? null,
|
|
42
|
+
userId: ctx.userId,
|
|
43
|
+
resolvedByUserId: null,
|
|
44
|
+
resolvedAt: resolvedAtIso,
|
|
45
|
+
expiresAt: expiresAtIso,
|
|
46
|
+
expiredAt: resolvedAtIso
|
|
47
|
+
};
|
|
48
|
+
await emitEventSafe(emitter, EXPIRED_EVENT_ID, expiredPayload);
|
|
49
|
+
return { row: expiredRow, status: "expired" };
|
|
50
|
+
}
|
|
51
|
+
const trimmedReason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
52
|
+
const executionResult = {
|
|
53
|
+
error: {
|
|
54
|
+
code: "cancelled_by_user",
|
|
55
|
+
message: trimmedReason.length > 0 ? trimmedReason : "Cancelled by user"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const cancelledRow = await repo.setStatus(action.id, "cancelled", scope, {
|
|
59
|
+
resolvedByUserId: ctx.userId,
|
|
60
|
+
executionResult,
|
|
61
|
+
now: clock
|
|
62
|
+
});
|
|
63
|
+
const cancelledPayload = {
|
|
64
|
+
pendingActionId: cancelledRow.id,
|
|
65
|
+
agentId: cancelledRow.agentId,
|
|
66
|
+
toolName: cancelledRow.toolName,
|
|
67
|
+
status: cancelledRow.status,
|
|
68
|
+
tenantId: ctx.tenantId,
|
|
69
|
+
organizationId: ctx.organizationId ?? null,
|
|
70
|
+
userId: ctx.userId,
|
|
71
|
+
resolvedByUserId: ctx.userId,
|
|
72
|
+
resolvedAt: (cancelledRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),
|
|
73
|
+
executionResult,
|
|
74
|
+
...trimmedReason.length > 0 ? { reason: trimmedReason } : {}
|
|
75
|
+
};
|
|
76
|
+
await emitEventSafe(emitter, CANCELLED_EVENT_ID, cancelledPayload);
|
|
77
|
+
return { row: cancelledRow, status: "cancelled" };
|
|
78
|
+
}
|
|
79
|
+
const PENDING_ACTION_CANCELLED_EVENT_ID = CANCELLED_EVENT_ID;
|
|
80
|
+
const PENDING_ACTION_EXPIRED_EVENT_ID = EXPIRED_EVENT_ID;
|
|
81
|
+
export {
|
|
82
|
+
PENDING_ACTION_CANCELLED_EVENT_ID,
|
|
83
|
+
PENDING_ACTION_EXPIRED_EVENT_ID,
|
|
84
|
+
executePendingActionCancel
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=pending-action-cancel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/pending-action-cancel.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Pending-action cancel executor (spec \u00A79.4, Step 5.9).\n *\n * Flips an `AiPendingAction` from `pending \u2192 cancelled` and emits the\n * typed `ai.action.cancelled` event via `emitAiAssistantEvent`. Unlike\n * {@link executePendingActionConfirm} the tool handler is NEVER invoked \u2014\n * cancellation is a pure state-machine transition plus an event emission.\n * Any other status short-circuits: already-`cancelled` is idempotent (no\n * second emit, same row returned); `confirmed` / `executing` / `failed`\n * are treated as invariant violations and bubble up as 409 via the route.\n *\n * If the row's `expiresAt` is in the past at the time of this call we\n * flip it to `expired` (not `cancelled`) \u2014 the Step 5.12 cleanup worker\n * races with this code path and we want a single canonical terminal\n * status for a row that reached its TTL.\n *\n * Idempotency: the caller MUST handle the already-`cancelled` branch\n * BEFORE invoking this helper so the event is emitted exactly once per\n * cancellation.\n */\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type {\n AiActionCancelledPayload,\n AiActionExpiredPayload,\n AiAssistantEventId,\n} from '../events'\nimport type { AiPendingActionExecutionResult } from './pending-action-types'\n\nexport interface PendingActionCancelContext {\n tenantId: string\n organizationId: string | null\n userId: string\n container: import('awilix').AwilixContainer\n}\n\nexport type CancelEmitter = (\n eventId: Extract<AiAssistantEventId, 'ai.action.cancelled' | 'ai.action.expired'>,\n payload: AiActionCancelledPayload | AiActionExpiredPayload,\n) => Promise<void>\n\nexport interface PendingActionCancelInput {\n action: AiPendingAction\n ctx: PendingActionCancelContext\n /** Optional, caller-supplied cancellation reason (already trimmed by the route). */\n reason?: string | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n */\n emitEvent?: CancelEmitter\n now?: Date\n}\n\nexport type PendingActionCancelStatus = 'cancelled' | 'expired'\n\nexport interface PendingActionCancelResult {\n row: AiPendingAction\n status: PendingActionCancelStatus\n}\n\nconst CANCELLED_EVENT_ID = 'ai.action.cancelled' as const\nconst EXPIRED_EVENT_ID = 'ai.action.expired' as const\n\nconst defaultCancelEmitter: CancelEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitEventSafe(\n emitter: CancelEmitter,\n eventId: Parameters<CancelEmitter>[0],\n payload: Parameters<CancelEmitter>[1],\n): Promise<void> {\n try {\n await emitter(eventId, payload)\n } catch (error) {\n console.warn(`[AI Pending Action] Failed to emit ${eventId}:`, error)\n }\n}\n\n/**\n * Atomic `pending \u2192 cancelled` transition with TTL-race safety.\n *\n * - If `action.status === 'cancelled'`, returns the current row WITHOUT\n * emitting a second event. Callers should typically short-circuit on\n * this branch BEFORE invoking the helper (see the Step 5.9 route).\n * - If `action.expiresAt <= now`, the row is flipped to `expired` and the\n * `ai.action.expired` event is emitted via the typed\n * `emitAiAssistantEvent` helper (see `../events`). Returns\n * `{ status: 'expired' }` so the route can translate to a 409\n * `expired` envelope.\n * - Otherwise flips `pending \u2192 cancelled`, writes `resolvedAt` + the\n * optional cancellation reason onto `executionResult.error`, and emits\n * `ai.action.cancelled`.\n *\n * Any status other than `pending` / `cancelled` is treated as an\n * invariant violation \u2014 the route returns 409 `invalid_status` before\n * reaching this helper. If the caller invokes the helper on such a row\n * it will throw via the repo's state-machine guard.\n */\nexport async function executePendingActionCancel(\n input: PendingActionCancelInput,\n): Promise<PendingActionCancelResult> {\n const { action, ctx, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: CancelEmitter = input.emitEvent ?? defaultCancelEmitter\n\n if (action.status === 'cancelled') {\n return { row: action, status: 'cancelled' }\n }\n\n const expiresAt =\n action.expiresAt instanceof Date ? action.expiresAt : new Date(action.expiresAt)\n if (expiresAt.getTime() <= clock.getTime()) {\n const expiredRow = await repo.setStatus(action.id, 'expired', scope, { now: clock })\n const resolvedAtIso =\n (expiredRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiresAtIso =\n (expiresAt ?? clock).toISOString?.() ?? new Date(clock).toISOString()\n const expiredPayload: AiActionExpiredPayload = {\n pendingActionId: expiredRow.id,\n agentId: expiredRow.agentId,\n toolName: expiredRow.toolName,\n status: expiredRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: null,\n resolvedAt: resolvedAtIso,\n expiresAt: expiresAtIso,\n expiredAt: resolvedAtIso,\n }\n await emitEventSafe(emitter, EXPIRED_EVENT_ID, expiredPayload)\n return { row: expiredRow, status: 'expired' }\n }\n\n const trimmedReason = typeof input.reason === 'string' ? input.reason.trim() : ''\n const executionResult: AiPendingActionExecutionResult = {\n error: {\n code: 'cancelled_by_user',\n message: trimmedReason.length > 0 ? trimmedReason : 'Cancelled by user',\n },\n }\n\n const cancelledRow = await repo.setStatus(action.id, 'cancelled', scope, {\n resolvedByUserId: ctx.userId,\n executionResult,\n now: clock,\n })\n const cancelledPayload: AiActionCancelledPayload = {\n pendingActionId: cancelledRow.id,\n agentId: cancelledRow.agentId,\n toolName: cancelledRow.toolName,\n status: cancelledRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (cancelledRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult,\n ...(trimmedReason.length > 0 ? { reason: trimmedReason } : {}),\n }\n await emitEventSafe(emitter, CANCELLED_EVENT_ID, cancelledPayload)\n\n return { row: cancelledRow, status: 'cancelled' }\n}\n\nexport const PENDING_ACTION_CANCELLED_EVENT_ID = CANCELLED_EVENT_ID\nexport const PENDING_ACTION_EXPIRED_EVENT_ID = EXPIRED_EVENT_ID\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function dateToIso(value) {
|
|
2
|
+
if (value instanceof Date) return value.toISOString();
|
|
3
|
+
if (typeof value === "string") return value;
|
|
4
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
5
|
+
}
|
|
6
|
+
function optionalDateToIso(value) {
|
|
7
|
+
if (value == null) return null;
|
|
8
|
+
return dateToIso(value);
|
|
9
|
+
}
|
|
10
|
+
function serializePendingActionForClient(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
agentId: row.agentId,
|
|
14
|
+
toolName: row.toolName,
|
|
15
|
+
status: row.status,
|
|
16
|
+
fieldDiff: Array.isArray(row.fieldDiff) ? row.fieldDiff : [],
|
|
17
|
+
records: Array.isArray(row.records) && row.records.length > 0 ? row.records : null,
|
|
18
|
+
failedRecords: Array.isArray(row.failedRecords) && row.failedRecords.length > 0 ? row.failedRecords : null,
|
|
19
|
+
sideEffectsSummary: row.sideEffectsSummary ?? null,
|
|
20
|
+
attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],
|
|
21
|
+
targetEntityType: row.targetEntityType ?? null,
|
|
22
|
+
targetRecordId: row.targetRecordId ?? null,
|
|
23
|
+
recordVersion: row.recordVersion ?? null,
|
|
24
|
+
queueMode: row.queueMode ?? "inline",
|
|
25
|
+
executionResult: row.executionResult ?? null,
|
|
26
|
+
createdAt: dateToIso(row.createdAt),
|
|
27
|
+
expiresAt: dateToIso(row.expiresAt),
|
|
28
|
+
resolvedAt: optionalDateToIso(row.resolvedAt),
|
|
29
|
+
resolvedByUserId: row.resolvedByUserId ?? null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
serializePendingActionForClient
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=pending-action-client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/pending-action-client.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Whitelist-based client serializer for {@link AiPendingAction} (Phase 3 WS-C,\n * Step 5.7). The pending-action table carries server-internal fields\n * (`normalizedInput`, `createdByUserId`, `idempotencyKey`) that MUST NOT be\n * exposed to the browser: `normalizedInput` can contain raw tool arguments\n * including PII or credentials; `createdByUserId` leaks an internal principal\n * when the UI never needs it (the `resolvedByUserId` field is the only\n * actor the UI renders); `idempotencyKey` is a deterministic hash that,\n * combined with `(tenantId, organizationId, agentId)`, lets an attacker\n * collide deduplication windows by crafting identical normalized inputs.\n *\n * This helper is shared by the GET /api/ai/actions/:id reconnect route\n * (Step 5.7) and re-used by the confirm (Step 5.8) and cancel (Step 5.9)\n * response bodies so the UI always sees the same shape.\n *\n * The serializer is deliberately WHITELIST-based: adding a new internal\n * column to the entity must never leak to the client as a side-effect of\n * a generic `{...row}` copy. Any new client-visible field MUST be added\n * here explicitly with a matching update to {@link SerializedPendingAction}.\n */\n\nimport type {\n AiPendingActionExecutionResult,\n AiPendingActionFailedRecord,\n AiPendingActionFieldDiff,\n AiPendingActionQueueMode,\n AiPendingActionRecordDiff,\n AiPendingActionStatus,\n} from './pending-action-types'\n\n/**\n * Client-visible subset of {@link AiPendingAction}. Never includes\n * `normalizedInput`, `createdByUserId`, or `idempotencyKey` \u2014 see the\n * module-level doc above.\n */\nexport interface SerializedPendingAction {\n id: string\n agentId: string\n toolName: string\n status: AiPendingActionStatus\n fieldDiff: AiPendingActionFieldDiff[]\n records: AiPendingActionRecordDiff[] | null\n failedRecords: AiPendingActionFailedRecord[] | null\n sideEffectsSummary: string | null\n attachmentIds: string[]\n targetEntityType: string | null\n targetRecordId: string | null\n recordVersion: string | null\n queueMode: AiPendingActionQueueMode\n executionResult: AiPendingActionExecutionResult | null\n createdAt: string\n expiresAt: string\n resolvedAt: string | null\n resolvedByUserId: string | null\n}\n\n/**\n * Minimal row shape the serializer accepts. Defined by name rather than\n * importing the entity class directly so this module stays usable in test\n * contexts that stub the ORM row without loading MikroORM decorators.\n */\nexport interface SerializablePendingActionRow {\n id: string\n agentId: string\n toolName: string\n status: AiPendingActionStatus\n fieldDiff?: AiPendingActionFieldDiff[] | null\n records?: AiPendingActionRecordDiff[] | null\n failedRecords?: AiPendingActionFailedRecord[] | null\n sideEffectsSummary?: string | null\n attachmentIds?: string[] | null\n targetEntityType?: string | null\n targetRecordId?: string | null\n recordVersion?: string | null\n queueMode?: AiPendingActionQueueMode | null\n executionResult?: AiPendingActionExecutionResult | null\n createdAt: Date | string\n expiresAt: Date | string\n resolvedAt?: Date | string | null\n resolvedByUserId?: string | null\n}\n\nfunction dateToIso(value: Date | string): string {\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') return value\n return new Date().toISOString()\n}\n\nfunction optionalDateToIso(value: Date | string | null | undefined): string | null {\n if (value == null) return null\n return dateToIso(value)\n}\n\n/**\n * Build the client-facing view of a pending action. Strips server-internal\n * fields (`normalizedInput`, `createdByUserId`, `idempotencyKey`) and\n * normalizes Date instances to ISO-8601 strings so the result round-trips\n * through JSON without losing precision.\n */\nexport function serializePendingActionForClient(\n row: SerializablePendingActionRow,\n): SerializedPendingAction {\n return {\n id: row.id,\n agentId: row.agentId,\n toolName: row.toolName,\n status: row.status,\n fieldDiff: Array.isArray(row.fieldDiff) ? row.fieldDiff : [],\n records: Array.isArray(row.records) && row.records.length > 0 ? row.records : null,\n failedRecords:\n Array.isArray(row.failedRecords) && row.failedRecords.length > 0\n ? row.failedRecords\n : null,\n sideEffectsSummary: row.sideEffectsSummary ?? null,\n attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],\n targetEntityType: row.targetEntityType ?? null,\n targetRecordId: row.targetRecordId ?? null,\n recordVersion: row.recordVersion ?? null,\n queueMode: (row.queueMode ?? 'inline') as AiPendingActionQueueMode,\n executionResult: row.executionResult ?? null,\n createdAt: dateToIso(row.createdAt),\n expiresAt: dateToIso(row.expiresAt),\n resolvedAt: optionalDateToIso(row.resolvedAt),\n resolvedByUserId: row.resolvedByUserId ?? null,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAkFA,SAAS,UAAU,OAA8B;AAC/C,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,UAAO,oBAAI,KAAK,GAAE,YAAY;AAChC;AAEA,SAAS,kBAAkB,OAAwD;AACjF,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,UAAU,KAAK;AACxB;AAQO,SAAS,gCACd,KACyB;AACzB,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA,IACb,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,WAAW,MAAM,QAAQ,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA,IAC3D,SAAS,MAAM,QAAQ,IAAI,OAAO,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI,UAAU;AAAA,IAC9E,eACE,MAAM,QAAQ,IAAI,aAAa,KAAK,IAAI,cAAc,SAAS,IAC3D,IAAI,gBACJ;AAAA,IACN,oBAAoB,IAAI,sBAAsB;AAAA,IAC9C,eAAe,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IACvE,kBAAkB,IAAI,oBAAoB;AAAA,IAC1C,gBAAgB,IAAI,kBAAkB;AAAA,IACtC,eAAe,IAAI,iBAAiB;AAAA,IACpC,WAAY,IAAI,aAAa;AAAA,IAC7B,iBAAiB,IAAI,mBAAmB;AAAA,IACxC,WAAW,UAAU,IAAI,SAAS;AAAA,IAClC,WAAW,UAAU,IAAI,SAAS;AAAA,IAClC,YAAY,kBAAkB,IAAI,UAAU;AAAA,IAC5C,kBAAkB,IAAI,oBAAoB;AAAA,EAC5C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { AiPendingActionRepository } from "../data/repositories/AiPendingActionRepository.js";
|
|
2
|
+
import { emitAiAssistantEvent } from "../events.js";
|
|
3
|
+
const CONFIRMED_EVENT_ID = "ai.action.confirmed";
|
|
4
|
+
const defaultConfirmedEmitter = async (eventId, payload) => {
|
|
5
|
+
await emitAiAssistantEvent(eventId, payload, {
|
|
6
|
+
persistent: true
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
async function emitConfirmed(emitter, payload) {
|
|
10
|
+
try {
|
|
11
|
+
await emitter(CONFIRMED_EVENT_ID, payload);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.warn(`[AI Pending Action] Failed to emit ${CONFIRMED_EVENT_ID}:`, error);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function normalizeExecutionResult(raw) {
|
|
17
|
+
if (!raw || typeof raw !== "object") return {};
|
|
18
|
+
const source = raw;
|
|
19
|
+
const result = {};
|
|
20
|
+
if (typeof source.recordId === "string") result.recordId = source.recordId;
|
|
21
|
+
if (typeof source.commandName === "string") result.commandName = source.commandName;
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function buildHandlerErrorFromThrown(error, input) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
const name = error instanceof Error ? error.name : void 0;
|
|
27
|
+
const out = {
|
|
28
|
+
code: "handler_error",
|
|
29
|
+
message: message || "Tool handler threw an error."
|
|
30
|
+
};
|
|
31
|
+
if (name) out.name = name;
|
|
32
|
+
if (input !== void 0) {
|
|
33
|
+
try {
|
|
34
|
+
out.input = JSON.parse(JSON.stringify(input));
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const details = {};
|
|
39
|
+
if (error && typeof error === "object") {
|
|
40
|
+
const err = error;
|
|
41
|
+
const issues = err.issues;
|
|
42
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
43
|
+
details.issues = issues.map((issue) => {
|
|
44
|
+
if (!issue || typeof issue !== "object") return issue;
|
|
45
|
+
const obj = issue;
|
|
46
|
+
return {
|
|
47
|
+
path: Array.isArray(obj.path) ? obj.path : void 0,
|
|
48
|
+
message: typeof obj.message === "string" ? obj.message : void 0,
|
|
49
|
+
code: typeof obj.code === "string" ? obj.code : void 0,
|
|
50
|
+
...typeof obj.expected === "string" ? { expected: obj.expected } : {},
|
|
51
|
+
...typeof obj.received === "string" ? { received: obj.received } : {}
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const fieldErrors = {};
|
|
55
|
+
for (const issue of issues) {
|
|
56
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
57
|
+
const msg = typeof issue.message === "string" ? issue.message : null;
|
|
58
|
+
if (!path || !msg) continue;
|
|
59
|
+
if (!fieldErrors[path]) fieldErrors[path] = [];
|
|
60
|
+
fieldErrors[path].push(msg);
|
|
61
|
+
}
|
|
62
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
63
|
+
details.fieldErrors = fieldErrors;
|
|
64
|
+
}
|
|
65
|
+
if (out.code === "handler_error") out.code = "validation_error";
|
|
66
|
+
}
|
|
67
|
+
if (typeof err.code === "string" && err.code.length > 0) {
|
|
68
|
+
out.code = err.code;
|
|
69
|
+
}
|
|
70
|
+
if (err.cause !== void 0) {
|
|
71
|
+
try {
|
|
72
|
+
details.cause = JSON.parse(JSON.stringify(err.cause));
|
|
73
|
+
} catch {
|
|
74
|
+
if (err.cause instanceof Error) {
|
|
75
|
+
details.cause = { message: err.cause.message, name: err.cause.name };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const key of Object.keys(err)) {
|
|
80
|
+
if (key === "issues" || key === "cause" || key === "code" || key === "message" || key === "name" || key === "stack") continue;
|
|
81
|
+
const value = err[key];
|
|
82
|
+
if (value === void 0) continue;
|
|
83
|
+
try {
|
|
84
|
+
details[key] = JSON.parse(JSON.stringify(value));
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(details).length > 0) {
|
|
90
|
+
out.details = details;
|
|
91
|
+
}
|
|
92
|
+
if (process.env.OM_AI_INCLUDE_HANDLER_STACK === "1" && error instanceof Error && error.stack) {
|
|
93
|
+
const lines = error.stack.split("\n").slice(0, 6);
|
|
94
|
+
out.stack = lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function extractHandlerFailedRecords(raw) {
|
|
99
|
+
if (!raw || typeof raw !== "object") return [];
|
|
100
|
+
const source = raw;
|
|
101
|
+
const records = source.records;
|
|
102
|
+
if (!Array.isArray(records) || records.length === 0) return [];
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const entry of records) {
|
|
105
|
+
if (!entry || typeof entry !== "object") continue;
|
|
106
|
+
const record = entry;
|
|
107
|
+
if (typeof record.recordId !== "string") continue;
|
|
108
|
+
const status = typeof record.status === "string" ? record.status : null;
|
|
109
|
+
if (status === "updated") continue;
|
|
110
|
+
const errorField = record.error;
|
|
111
|
+
if (!errorField || typeof errorField !== "object") continue;
|
|
112
|
+
const error = errorField;
|
|
113
|
+
const code = typeof error.code === "string" ? error.code : "handler_error";
|
|
114
|
+
const message = typeof error.message === "string" ? error.message : "Record update failed.";
|
|
115
|
+
out.push({
|
|
116
|
+
recordId: record.recordId,
|
|
117
|
+
error: { code, message }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function mergeFailedRecords(recheck, handler) {
|
|
123
|
+
const seen = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const entry of recheck ?? []) {
|
|
125
|
+
if (entry && typeof entry.recordId === "string" && !seen.has(entry.recordId)) {
|
|
126
|
+
seen.set(entry.recordId, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const entry of handler ?? []) {
|
|
130
|
+
if (entry && typeof entry.recordId === "string" && !seen.has(entry.recordId)) {
|
|
131
|
+
seen.set(entry.recordId, entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (seen.size === 0) return null;
|
|
135
|
+
return Array.from(seen.values());
|
|
136
|
+
}
|
|
137
|
+
function toToolHandlerContext(ctx, tool) {
|
|
138
|
+
return {
|
|
139
|
+
tenantId: ctx.tenantId,
|
|
140
|
+
organizationId: ctx.organizationId,
|
|
141
|
+
userId: ctx.userId,
|
|
142
|
+
container: ctx.container,
|
|
143
|
+
userFeatures: ctx.userFeatures,
|
|
144
|
+
isSuperAdmin: ctx.isSuperAdmin,
|
|
145
|
+
tool
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function executePendingActionConfirm(input) {
|
|
149
|
+
const { action, agent, tool, ctx, failedRecords, now } = input;
|
|
150
|
+
const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve("em"));
|
|
151
|
+
const scope = {
|
|
152
|
+
tenantId: ctx.tenantId,
|
|
153
|
+
organizationId: ctx.organizationId,
|
|
154
|
+
userId: ctx.userId
|
|
155
|
+
};
|
|
156
|
+
const clock = now ?? /* @__PURE__ */ new Date();
|
|
157
|
+
const emitter = input.emitEvent ?? defaultConfirmedEmitter;
|
|
158
|
+
if (action.status === "confirmed") {
|
|
159
|
+
const prior = action.executionResult ?? {};
|
|
160
|
+
return { ok: true, action, executionResult: prior };
|
|
161
|
+
}
|
|
162
|
+
if (action.status === "executing") {
|
|
163
|
+
const prior = action.executionResult ?? {};
|
|
164
|
+
return { ok: true, action, executionResult: prior };
|
|
165
|
+
}
|
|
166
|
+
if (action.status !== "pending") {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
action,
|
|
170
|
+
executionResult: {
|
|
171
|
+
error: { code: "invalid_status", message: `Action is in status "${action.status}".` }
|
|
172
|
+
},
|
|
173
|
+
cause: new Error(`Action is in status "${action.status}"`)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const partialFailedRecords = Array.isArray(failedRecords) && failedRecords.length > 0 ? failedRecords : null;
|
|
177
|
+
const confirmedRow = await repo.setStatus(action.id, "confirmed", scope, {
|
|
178
|
+
resolvedByUserId: ctx.userId,
|
|
179
|
+
now: clock,
|
|
180
|
+
...partialFailedRecords ? { failedRecords: partialFailedRecords } : {}
|
|
181
|
+
});
|
|
182
|
+
const executingRow = await repo.setStatus(confirmedRow.id, "executing", scope, { now: clock });
|
|
183
|
+
let handlerOutput;
|
|
184
|
+
try {
|
|
185
|
+
handlerOutput = await tool.handler(action.normalizedInput, toToolHandlerContext(ctx, tool));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const failureResult = {
|
|
188
|
+
error: buildHandlerErrorFromThrown(error, action.normalizedInput)
|
|
189
|
+
};
|
|
190
|
+
const failedRow = await repo.setStatus(executingRow.id, "failed", scope, {
|
|
191
|
+
executionResult: failureResult,
|
|
192
|
+
now: clock
|
|
193
|
+
});
|
|
194
|
+
await emitConfirmed(emitter, {
|
|
195
|
+
pendingActionId: failedRow.id,
|
|
196
|
+
agentId: agent.id,
|
|
197
|
+
toolName: tool.name,
|
|
198
|
+
status: failedRow.status,
|
|
199
|
+
tenantId: ctx.tenantId,
|
|
200
|
+
organizationId: ctx.organizationId ?? null,
|
|
201
|
+
userId: ctx.userId,
|
|
202
|
+
resolvedByUserId: ctx.userId,
|
|
203
|
+
resolvedAt: (failedRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),
|
|
204
|
+
executionResult: failureResult
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
action: failedRow,
|
|
209
|
+
executionResult: failureResult,
|
|
210
|
+
cause: error
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const successResult = normalizeExecutionResult(handlerOutput);
|
|
214
|
+
const handlerFailedRecords = extractHandlerFailedRecords(handlerOutput);
|
|
215
|
+
const mergedFailedRecords = mergeFailedRecords(partialFailedRecords, handlerFailedRecords);
|
|
216
|
+
const confirmedExtra = {
|
|
217
|
+
executionResult: successResult,
|
|
218
|
+
now: clock
|
|
219
|
+
};
|
|
220
|
+
confirmedExtra.failedRecords = mergedFailedRecords;
|
|
221
|
+
const confirmedFinal = await repo.setStatus(executingRow.id, "confirmed", scope, confirmedExtra);
|
|
222
|
+
const emitFailedRecordsPayload = Array.isArray(mergedFailedRecords) && mergedFailedRecords.length > 0 ? mergedFailedRecords : null;
|
|
223
|
+
await emitConfirmed(emitter, {
|
|
224
|
+
pendingActionId: confirmedFinal.id,
|
|
225
|
+
agentId: agent.id,
|
|
226
|
+
toolName: tool.name,
|
|
227
|
+
status: confirmedFinal.status,
|
|
228
|
+
tenantId: ctx.tenantId,
|
|
229
|
+
organizationId: ctx.organizationId ?? null,
|
|
230
|
+
userId: ctx.userId,
|
|
231
|
+
resolvedByUserId: ctx.userId,
|
|
232
|
+
resolvedAt: (confirmedFinal.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),
|
|
233
|
+
executionResult: successResult,
|
|
234
|
+
...emitFailedRecordsPayload ? { failedRecords: emitFailedRecordsPayload } : {}
|
|
235
|
+
});
|
|
236
|
+
return { ok: true, action: confirmedFinal, executionResult: successResult };
|
|
237
|
+
}
|
|
238
|
+
const PENDING_ACTION_CONFIRMED_EVENT_ID = CONFIRMED_EVENT_ID;
|
|
239
|
+
export {
|
|
240
|
+
PENDING_ACTION_CONFIRMED_EVENT_ID,
|
|
241
|
+
executePendingActionConfirm
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=pending-action-executor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/pending-action-executor.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Pending-action executor (spec \u00A79.4, Step 5.8).\n *\n * Transitions an `AiPendingAction` from `pending \u2192 confirmed \u2192 executing`,\n * invokes the wrapped tool handler, and records the outcome. Isolated from\n * the HTTP route so the unit suite can exercise the state-machine +\n * event-emission + idempotency guarantees without constructing a\n * `NextRequest`.\n *\n * Atomicity:\n * - The `pending \u2192 confirmed` and `confirmed \u2192 executing` transitions go\n * through the repository's `em.transactional` boundary. If the process\n * crashes between steps, the row is left in an intermediate terminal\n * state (`executing` or `confirmed`) that the operator can recover \u2014\n * NEVER in a partially-applied state that hides the crash.\n * - The tool handler itself runs OUTSIDE the repo transaction so that a\n * long-running write does not hold an `ai_pending_actions` row lock.\n * The handler's own transaction boundary (typically a command) is the\n * unit of atomicity for the underlying data change.\n */\nimport { AiPendingActionRepository } from '../data/repositories/AiPendingActionRepository'\nimport type { AiPendingAction } from '../data/entities'\nimport { emitAiAssistantEvent } from '../events'\nimport type { AiActionConfirmedPayload } from '../events'\nimport type { AiAgentDefinition } from './ai-agent-definition'\nimport type { AiToolDefinition, McpToolContext } from './types'\nimport type {\n AiPendingActionExecutionResult,\n AiPendingActionFailedRecord,\n} from './pending-action-types'\n\nexport interface PendingActionExecuteContext {\n tenantId: string\n organizationId: string | null\n userId: string\n userFeatures: string[]\n isSuperAdmin: boolean\n container: import('awilix').AwilixContainer\n}\n\nexport interface PendingActionExecuteInput {\n action: AiPendingAction\n agent: AiAgentDefinition\n tool: AiToolDefinition\n ctx: PendingActionExecuteContext\n /** Carried over from the re-check; written onto the row with status=confirmed. */\n failedRecords?: AiPendingActionFailedRecord[] | null\n repo?: AiPendingActionRepository\n /**\n * Injection seam for unit tests. When omitted, emission is routed via\n * the typed `emitAiAssistantEvent` helper (the normal production path).\n * When supplied, the raw bus is used directly \u2014 kept for legacy tests\n * that assert on the bus call surface.\n */\n emitEvent?: (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n ) => Promise<void>\n now?: Date\n}\n\nexport interface PendingActionExecuteOk {\n ok: true\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n}\n\nexport interface PendingActionExecuteFail {\n ok: false\n action: AiPendingAction\n executionResult: AiPendingActionExecutionResult\n /** The underlying error \u2014 the route translates into a 200 with `executionResult.error` set. */\n cause: unknown\n}\n\nexport type PendingActionExecuteResult = PendingActionExecuteOk | PendingActionExecuteFail\n\nconst CONFIRMED_EVENT_ID = 'ai.action.confirmed' as const\n\ntype ConfirmedEmitter = (\n eventId: 'ai.action.confirmed',\n payload: AiActionConfirmedPayload,\n) => Promise<void>\n\nconst defaultConfirmedEmitter: ConfirmedEmitter = async (eventId, payload) => {\n await emitAiAssistantEvent(eventId, payload as unknown as Record<string, unknown>, {\n persistent: true,\n })\n}\n\nasync function emitConfirmed(\n emitter: ConfirmedEmitter,\n payload: AiActionConfirmedPayload,\n): Promise<void> {\n try {\n await emitter(CONFIRMED_EVENT_ID, payload)\n } catch (error) {\n console.warn(`[AI Pending Action] Failed to emit ${CONFIRMED_EVENT_ID}:`, error)\n }\n}\n\nfunction normalizeExecutionResult(\n raw: unknown,\n): AiPendingActionExecutionResult {\n if (!raw || typeof raw !== 'object') return {}\n const source = raw as Record<string, unknown>\n const result: AiPendingActionExecutionResult = {}\n if (typeof source.recordId === 'string') result.recordId = source.recordId\n if (typeof source.commandName === 'string') result.commandName = source.commandName\n return result\n}\n\n/**\n * Extract per-record handler failures from a bulk tool's return value so\n * they can be persisted onto the pending-action row's `failedRecords[]`.\n *\n * Bulk mutation tools (Step 5.14) return a result of the shape:\n * { commandName, records: [{ recordId, status, before, after, error? }],\n * failedRecordIds: string[], error? }\n *\n * We pull the entries whose `status !== 'updated'` AND carry an `error`\n * object, coerce them to the `AiPendingActionFailedRecord` shape, and\n * return them so the executor can merge with re-check-sourced failures\n * at the final `executing \u2192 confirmed` transition (spec \u00A79.8 line 746:\n * \"a failure inside the confirm handler ... is recorded per-record in\n * executionResult.failedRecords[] / row.failedRecords\").\n *\n * Returns an empty array when the handler output does not carry the\n * batch shape (single-record tools never populate this \u2014 their failures\n * either throw from the handler and land in `executionResult.error` or\n * succeed cleanly).\n */\n/**\n * Convert a thrown handler error into the structured shape we persist on\n * `executionResult.error`. The previous implementation only kept\n * `{ code: 'handler_error', message }` \u2014 for ZodError that flattened to\n * the literal string \"Invalid input\", which was not enough context for\n * the operator's \"Fix with AI\" retry to actually fix anything. This\n * version preserves the original error name, Zod issues / fieldErrors,\n * an `input` echo of the arguments the handler was called with, and any\n * structured `cause` so the model can self-correct.\n *\n * Stack traces are deliberately gated behind `OM_AI_INCLUDE_HANDLER_STACK=1`\n * \u2014 they're noise to the model and a leak risk in tenant-visible UI.\n */\nfunction buildHandlerErrorFromThrown(\n error: unknown,\n input: unknown,\n): NonNullable<AiPendingActionExecutionResult['error']> {\n const message = error instanceof Error ? error.message : String(error)\n const name = error instanceof Error ? error.name : undefined\n const out: NonNullable<AiPendingActionExecutionResult['error']> = {\n code: 'handler_error',\n message: message || 'Tool handler threw an error.',\n }\n if (name) out.name = name\n\n // Echo the input so the model can compare what it sent vs. what the\n // schema expected. `normalizedInput` has already been Zod-parsed at\n // prepareMutation time so the values are JSON-safe.\n if (input !== undefined) {\n try {\n out.input = JSON.parse(JSON.stringify(input))\n } catch {\n // ignore \u2014 non-serializable input is rare and the model can still\n // work from the message + Zod issues.\n }\n }\n\n const details: Record<string, unknown> = {}\n\n if (error && typeof error === 'object') {\n const err = error as Record<string, unknown>\n // ZodError: forward `issues[]` verbatim and a flat `fieldErrors`\n // form so the model can locate the failing field by name even when\n // the message has been collapsed to \"Invalid input\".\n const issues = err.issues\n if (Array.isArray(issues) && issues.length > 0) {\n details.issues = issues.map((issue) => {\n if (!issue || typeof issue !== 'object') return issue\n const obj = issue as Record<string, unknown>\n return {\n path: Array.isArray(obj.path) ? obj.path : undefined,\n message: typeof obj.message === 'string' ? obj.message : undefined,\n code: typeof obj.code === 'string' ? obj.code : undefined,\n ...(typeof obj.expected === 'string' ? { expected: obj.expected } : {}),\n ...(typeof obj.received === 'string' ? { received: obj.received } : {}),\n }\n })\n const fieldErrors: Record<string, string[]> = {}\n for (const issue of issues as Array<Record<string, unknown>>) {\n const path = Array.isArray(issue.path) ? issue.path.join('.') : ''\n const msg = typeof issue.message === 'string' ? issue.message : null\n if (!path || !msg) continue\n if (!fieldErrors[path]) fieldErrors[path] = []\n fieldErrors[path].push(msg)\n }\n if (Object.keys(fieldErrors).length > 0) {\n details.fieldErrors = fieldErrors\n }\n if (out.code === 'handler_error') out.code = 'validation_error'\n }\n // Forward a known `code` if the handler error carries one.\n if (typeof err.code === 'string' && err.code.length > 0) {\n out.code = err.code\n }\n // Carry the cause when it is JSON-serializable (string, number, plain object).\n if (err.cause !== undefined) {\n try {\n details.cause = JSON.parse(JSON.stringify(err.cause))\n } catch {\n if (err.cause instanceof Error) {\n details.cause = { message: err.cause.message, name: err.cause.name }\n }\n }\n }\n // Pull through any other plain-object enumerable own props the handler\n // attached (e.g. `expected`, `actual`, `target`).\n for (const key of Object.keys(err)) {\n if (key === 'issues' || key === 'cause' || key === 'code' || key === 'message' || key === 'name' || key === 'stack') continue\n const value = err[key]\n if (value === undefined) continue\n try {\n details[key] = JSON.parse(JSON.stringify(value))\n } catch {\n // skip non-serializable\n }\n }\n }\n\n if (Object.keys(details).length > 0) {\n out.details = details\n }\n\n if (process.env.OM_AI_INCLUDE_HANDLER_STACK === '1' && error instanceof Error && error.stack) {\n // Trim to the top frames so the persisted result stays bounded.\n const lines = error.stack.split('\\n').slice(0, 6)\n out.stack = lines.join('\\n')\n }\n\n return out\n}\n\nfunction extractHandlerFailedRecords(raw: unknown): AiPendingActionFailedRecord[] {\n if (!raw || typeof raw !== 'object') return []\n const source = raw as Record<string, unknown>\n const records = source.records\n if (!Array.isArray(records) || records.length === 0) return []\n const out: AiPendingActionFailedRecord[] = []\n for (const entry of records) {\n if (!entry || typeof entry !== 'object') continue\n const record = entry as Record<string, unknown>\n if (typeof record.recordId !== 'string') continue\n const status = typeof record.status === 'string' ? record.status : null\n if (status === 'updated') continue\n const errorField = record.error\n if (!errorField || typeof errorField !== 'object') continue\n const error = errorField as Record<string, unknown>\n const code = typeof error.code === 'string' ? error.code : 'handler_error'\n const message = typeof error.message === 'string' ? error.message : 'Record update failed.'\n out.push({\n recordId: record.recordId,\n error: { code, message },\n })\n }\n return out\n}\n\nfunction mergeFailedRecords(\n recheck: AiPendingActionFailedRecord[] | null | undefined,\n handler: AiPendingActionFailedRecord[] | null | undefined,\n): AiPendingActionFailedRecord[] | null {\n const seen = new Map<string, AiPendingActionFailedRecord>()\n for (const entry of recheck ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n for (const entry of handler ?? []) {\n if (entry && typeof entry.recordId === 'string' && !seen.has(entry.recordId)) {\n seen.set(entry.recordId, entry)\n }\n }\n if (seen.size === 0) return null\n return Array.from(seen.values())\n}\n\nfunction toToolHandlerContext(\n ctx: PendingActionExecuteContext,\n tool: AiToolDefinition,\n): McpToolContext {\n return {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n container: ctx.container,\n userFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n tool,\n }\n}\n\n/**\n * Idempotent entry point for the Step 5.8 confirm route.\n *\n * - If the action is already `confirmed` with a stored `executionResult`,\n * returns that prior result without re-invoking the handler (double-click /\n * retry contract).\n * - If the action is already `confirmed` without a stored `executionResult`\n * (shouldn't happen in practice), returns a synthesized empty result.\n * - If the action is still `pending`, runs the transitions and the handler.\n * - Any other status is rejected at the re-check layer before this helper\n * is ever called; this helper treats them as invariant violations.\n */\nexport async function executePendingActionConfirm(\n input: PendingActionExecuteInput,\n): Promise<PendingActionExecuteResult> {\n const { action, agent, tool, ctx, failedRecords, now } = input\n const repo = input.repo ?? new AiPendingActionRepository(ctx.container.resolve('em'))\n const scope = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n }\n const clock = now ?? new Date()\n const emitter: ConfirmedEmitter = input.emitEvent ?? defaultConfirmedEmitter\n\n if (action.status === 'confirmed') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status === 'executing') {\n const prior = (action.executionResult ?? {}) as AiPendingActionExecutionResult\n return { ok: true, action, executionResult: prior }\n }\n\n if (action.status !== 'pending') {\n return {\n ok: false,\n action,\n executionResult: {\n error: { code: 'invalid_status', message: `Action is in status \"${action.status}\".` },\n },\n cause: new Error(`Action is in status \"${action.status}\"`),\n }\n }\n\n const partialFailedRecords =\n Array.isArray(failedRecords) && failedRecords.length > 0 ? failedRecords : null\n\n const confirmedRow = await repo.setStatus(action.id, 'confirmed', scope, {\n resolvedByUserId: ctx.userId,\n now: clock,\n ...(partialFailedRecords ? { failedRecords: partialFailedRecords } : {}),\n })\n const executingRow = await repo.setStatus(confirmedRow.id, 'executing', scope, { now: clock })\n\n let handlerOutput: unknown\n try {\n handlerOutput = await tool.handler(action.normalizedInput as never, toToolHandlerContext(ctx, tool))\n } catch (error) {\n const failureResult: AiPendingActionExecutionResult = {\n error: buildHandlerErrorFromThrown(error, action.normalizedInput),\n }\n const failedRow = await repo.setStatus(executingRow.id, 'failed', scope, {\n executionResult: failureResult,\n now: clock,\n })\n await emitConfirmed(emitter, {\n pendingActionId: failedRow.id,\n agentId: agent.id,\n toolName: tool.name,\n status: failedRow.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (failedRow.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: failureResult,\n })\n return {\n ok: false,\n action: failedRow,\n executionResult: failureResult,\n cause: error,\n }\n }\n\n const successResult = normalizeExecutionResult(handlerOutput)\n const handlerFailedRecords = extractHandlerFailedRecords(handlerOutput)\n const mergedFailedRecords = mergeFailedRecords(partialFailedRecords, handlerFailedRecords)\n const confirmedExtra: Record<string, unknown> = {\n executionResult: successResult,\n now: clock,\n }\n // Always write `failedRecords` on the final transition so a batch that\n // had re-check-stale records but zero handler failures keeps the\n // original list, and a batch with handler failures merges both sets.\n // Explicit `null` collapses to \"no failures\" in the repository's\n // `normalizeFailedRecords` helper.\n confirmedExtra.failedRecords = mergedFailedRecords\n const confirmedFinal = await repo.setStatus(executingRow.id, 'confirmed', scope, confirmedExtra)\n const emitFailedRecordsPayload =\n Array.isArray(mergedFailedRecords) && mergedFailedRecords.length > 0\n ? mergedFailedRecords\n : null\n await emitConfirmed(emitter, {\n pendingActionId: confirmedFinal.id,\n agentId: agent.id,\n toolName: tool.name,\n status: confirmedFinal.status,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n userId: ctx.userId,\n resolvedByUserId: ctx.userId,\n resolvedAt: (confirmedFinal.resolvedAt ?? clock).toISOString?.() ?? new Date(clock).toISOString(),\n executionResult: successResult,\n ...(emitFailedRecordsPayload ? { failedRecords: emitFailedRecordsPayload } : {}),\n })\n return { ok: true, action: confirmedFinal, executionResult: successResult }\n}\n\nexport const PENDING_ACTION_CONFIRMED_EVENT_ID = CONFIRMED_EVENT_ID\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|