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