@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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 5.16 — Phase 3 WS-D integration tests for the execution-budget
|
|
3
|
+
* (`maxSteps`) contract on `runAiAgentText` and `runAiAgentObject`.
|
|
4
|
+
*
|
|
5
|
+
* Pins the Step 3.4 / 3.5 `stopWhen: stepCountIs(agent.maxSteps)` plumbing:
|
|
6
|
+
*
|
|
7
|
+
* - agent declares `maxSteps: n (n > 0)` → `stopWhen: stepCountIs(n)`
|
|
8
|
+
* - agent omits `maxSteps` (or sets 0) → no `stopWhen` on the SDK args
|
|
9
|
+
* - `runAiAgentObject` preserves the exact same precedence — object-mode
|
|
10
|
+
* must not silently diverge from chat-mode (spec §1.5).
|
|
11
|
+
*
|
|
12
|
+
* The Step description also enumerates a "caller-passed stopWhen overrides
|
|
13
|
+
* the agent's maxSteps" scenario. The current `RunAiAgentTextInput` /
|
|
14
|
+
* `RunAiAgentObjectInput` shapes do NOT expose a per-call override surface
|
|
15
|
+
* (only `modelOverride`). Introducing a public `maxStepsOverride` field
|
|
16
|
+
* would require production code changes, and Step 5.16 is strictly
|
|
17
|
+
* additive-test-only ("No new production code in this Step"). That
|
|
18
|
+
* scenario is therefore documented as a deliberate gap in step-5.16-checks.md
|
|
19
|
+
* rather than forced through a test-only seam that would misrepresent the
|
|
20
|
+
* public contract.
|
|
21
|
+
*
|
|
22
|
+
* The AI SDK module is stubbed at the Jest module boundary. `streamText`,
|
|
23
|
+
* `generateObject`, `streamObject`, `convertToModelMessages`, and
|
|
24
|
+
* `stepCountIs` are all replaced by jest.fn()s so the test never hits a
|
|
25
|
+
* real provider. The provider registry is stubbed the same way as in
|
|
26
|
+
* `agent-runtime.test.ts`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const streamTextMock = jest.fn()
|
|
30
|
+
const generateObjectMock = jest.fn()
|
|
31
|
+
const streamObjectMock = jest.fn()
|
|
32
|
+
const convertToModelMessagesMock = jest.fn((messages: unknown) => messages)
|
|
33
|
+
const stepCountIsMock = jest.fn(
|
|
34
|
+
(count: number) => ({ __stopWhen: 'stepCount', count }) as const,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
jest.mock('ai', () => {
|
|
38
|
+
const actual = jest.requireActual('ai')
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
streamText: (...args: unknown[]) => streamTextMock(...args),
|
|
42
|
+
generateObject: (...args: unknown[]) => generateObjectMock(...args),
|
|
43
|
+
streamObject: (...args: unknown[]) => streamObjectMock(...args),
|
|
44
|
+
stepCountIs: (...args: unknown[]) => stepCountIsMock(...(args as [number])),
|
|
45
|
+
convertToModelMessages: (...args: unknown[]) => convertToModelMessagesMock(...args),
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const createModelMock = jest.fn((options: { modelId: string; apiKey: string }) => ({
|
|
50
|
+
id: options.modelId,
|
|
51
|
+
apiKey: options.apiKey,
|
|
52
|
+
}))
|
|
53
|
+
const resolveApiKeyMock = jest.fn(() => 'test-api-key')
|
|
54
|
+
|
|
55
|
+
jest.mock('@open-mercato/shared/lib/ai/llm-provider-registry', () => ({
|
|
56
|
+
llmProviderRegistry: {
|
|
57
|
+
resolveFirstConfigured: () => ({
|
|
58
|
+
id: 'test-provider',
|
|
59
|
+
defaultModel: 'provider-default-model',
|
|
60
|
+
resolveApiKey: resolveApiKeyMock,
|
|
61
|
+
createModel: createModelMock,
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
import { z } from 'zod'
|
|
67
|
+
import type { AiAgentDefinition } from '../ai-agent-definition'
|
|
68
|
+
import {
|
|
69
|
+
resetAgentRegistryForTests,
|
|
70
|
+
seedAgentRegistryForTests,
|
|
71
|
+
} from '../agent-registry'
|
|
72
|
+
import { toolRegistry } from '../tool-registry'
|
|
73
|
+
import { runAiAgentObject, runAiAgentText } from '../agent-runtime'
|
|
74
|
+
|
|
75
|
+
function makeAgent(
|
|
76
|
+
overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>,
|
|
77
|
+
): AiAgentDefinition {
|
|
78
|
+
return {
|
|
79
|
+
label: `${overrides.id} label`,
|
|
80
|
+
description: `${overrides.id} description`,
|
|
81
|
+
systemPrompt: 'System prompt base.',
|
|
82
|
+
allowedTools: [],
|
|
83
|
+
...overrides,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const baseAuth = {
|
|
88
|
+
tenantId: 'tenant-1',
|
|
89
|
+
organizationId: 'org-1',
|
|
90
|
+
userId: 'user-1',
|
|
91
|
+
features: ['*'],
|
|
92
|
+
isSuperAdmin: true,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const baseMessages = [
|
|
96
|
+
{ role: 'user' as const, id: 'm1', parts: [{ type: 'text' as const, text: 'hi' }] },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
function fakeStreamResult(): {
|
|
100
|
+
toTextStreamResponse: jest.Mock
|
|
101
|
+
toUIMessageStreamResponse: jest.Mock
|
|
102
|
+
} {
|
|
103
|
+
return {
|
|
104
|
+
toTextStreamResponse: jest.fn(
|
|
105
|
+
() =>
|
|
106
|
+
new Response('streamed', {
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
toUIMessageStreamResponse: jest.fn(
|
|
112
|
+
() =>
|
|
113
|
+
new Response('streamed', {
|
|
114
|
+
status: 200,
|
|
115
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('Step 5.16 — runAiAgentText maxSteps budget (integration)', () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
jest.clearAllMocks()
|
|
124
|
+
resetAgentRegistryForTests()
|
|
125
|
+
toolRegistry.clear()
|
|
126
|
+
streamTextMock.mockImplementation(() => fakeStreamResult())
|
|
127
|
+
})
|
|
128
|
+
afterAll(() => {
|
|
129
|
+
resetAgentRegistryForTests()
|
|
130
|
+
toolRegistry.clear()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('passes stopWhen: stepCountIs(agent.maxSteps) when maxSteps is a positive integer', async () => {
|
|
134
|
+
seedAgentRegistryForTests([
|
|
135
|
+
makeAgent({
|
|
136
|
+
id: 'customers.account_assistant',
|
|
137
|
+
moduleId: 'customers',
|
|
138
|
+
maxSteps: 3,
|
|
139
|
+
}),
|
|
140
|
+
])
|
|
141
|
+
await runAiAgentText({
|
|
142
|
+
agentId: 'customers.account_assistant',
|
|
143
|
+
messages: baseMessages as never,
|
|
144
|
+
authContext: baseAuth,
|
|
145
|
+
})
|
|
146
|
+
expect(stepCountIsMock).toHaveBeenCalledWith(3)
|
|
147
|
+
const callArg = streamTextMock.mock.calls[0][0] as { stopWhen: unknown }
|
|
148
|
+
expect(callArg.stopWhen).toEqual({ __stopWhen: 'stepCount', count: 3 })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('applies default stopWhen: stepCountIs(10) when maxSteps is undefined (tool-call-enabling default)', async () => {
|
|
152
|
+
// PR #1593 (commit 5873fcee5) added a default of 10 when maxSteps is
|
|
153
|
+
// undefined — without stopWhen the AI SDK runs a single model call and
|
|
154
|
+
// never executes tool calls, which makes every tool-using query return
|
|
155
|
+
// an empty stream. The test pins that behavior.
|
|
156
|
+
seedAgentRegistryForTests([
|
|
157
|
+
makeAgent({
|
|
158
|
+
id: 'customers.account_assistant',
|
|
159
|
+
moduleId: 'customers',
|
|
160
|
+
// Explicit undefined — the default case for most agents.
|
|
161
|
+
}),
|
|
162
|
+
])
|
|
163
|
+
await runAiAgentText({
|
|
164
|
+
agentId: 'customers.account_assistant',
|
|
165
|
+
messages: baseMessages as never,
|
|
166
|
+
authContext: baseAuth,
|
|
167
|
+
})
|
|
168
|
+
expect(stepCountIsMock).toHaveBeenCalledWith(10)
|
|
169
|
+
const callArg = streamTextMock.mock.calls[0][0] as { stopWhen: unknown }
|
|
170
|
+
expect(callArg.stopWhen).toEqual({ __stopWhen: 'stepCount', count: 10 })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('falls back to default stopWhen: stepCountIs(10) when maxSteps is 0', async () => {
|
|
174
|
+
// Spec §1.4: maxSteps must be a positive integer; 0 is treated the same
|
|
175
|
+
// as undefined. Post-#1593 that means the default-10 guard kicks in so
|
|
176
|
+
// tool calls still work, instead of short-circuiting to a single model
|
|
177
|
+
// call.
|
|
178
|
+
seedAgentRegistryForTests([
|
|
179
|
+
makeAgent({
|
|
180
|
+
id: 'customers.account_assistant',
|
|
181
|
+
moduleId: 'customers',
|
|
182
|
+
maxSteps: 0,
|
|
183
|
+
}),
|
|
184
|
+
])
|
|
185
|
+
await runAiAgentText({
|
|
186
|
+
agentId: 'customers.account_assistant',
|
|
187
|
+
messages: baseMessages as never,
|
|
188
|
+
authContext: baseAuth,
|
|
189
|
+
})
|
|
190
|
+
expect(stepCountIsMock).toHaveBeenCalledWith(10)
|
|
191
|
+
const callArg = streamTextMock.mock.calls[0][0] as { stopWhen: unknown }
|
|
192
|
+
expect(callArg.stopWhen).toEqual({ __stopWhen: 'stepCount', count: 10 })
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('Step 5.16 — runAiAgentObject maxSteps budget parity (integration)', () => {
|
|
197
|
+
const schema = z.object({ summary: z.string() })
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
jest.clearAllMocks()
|
|
201
|
+
resetAgentRegistryForTests()
|
|
202
|
+
toolRegistry.clear()
|
|
203
|
+
generateObjectMock.mockImplementation(async () => ({
|
|
204
|
+
object: { summary: 'stub' },
|
|
205
|
+
finishReason: 'stop',
|
|
206
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
207
|
+
}))
|
|
208
|
+
})
|
|
209
|
+
afterAll(() => {
|
|
210
|
+
resetAgentRegistryForTests()
|
|
211
|
+
toolRegistry.clear()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('preserves agent.maxSteps → stopWhen on generateObject (object-mode parity)', async () => {
|
|
215
|
+
seedAgentRegistryForTests([
|
|
216
|
+
makeAgent({
|
|
217
|
+
id: 'catalog.merchandising_assistant',
|
|
218
|
+
moduleId: 'catalog',
|
|
219
|
+
executionMode: 'object',
|
|
220
|
+
output: {
|
|
221
|
+
schemaName: 'MerchandisingProposal',
|
|
222
|
+
schema,
|
|
223
|
+
mode: 'generate',
|
|
224
|
+
} as never,
|
|
225
|
+
maxSteps: 4,
|
|
226
|
+
}),
|
|
227
|
+
])
|
|
228
|
+
await runAiAgentObject({
|
|
229
|
+
agentId: 'catalog.merchandising_assistant',
|
|
230
|
+
input: 'draft title variants',
|
|
231
|
+
authContext: baseAuth,
|
|
232
|
+
})
|
|
233
|
+
expect(stepCountIsMock).toHaveBeenCalledWith(4)
|
|
234
|
+
// runAiAgentObject augments the generateObject args dynamically — the
|
|
235
|
+
// typed SDK surface ignores stopWhen but we MUST still forward it so
|
|
236
|
+
// providers that honor it behave identically across chat / object.
|
|
237
|
+
const callArg = generateObjectMock.mock.calls[0][0] as { stopWhen?: unknown }
|
|
238
|
+
expect(callArg.stopWhen).toEqual({ __stopWhen: 'stepCount', count: 4 })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('omits stopWhen on generateObject when the agent declares no maxSteps', async () => {
|
|
242
|
+
seedAgentRegistryForTests([
|
|
243
|
+
makeAgent({
|
|
244
|
+
id: 'catalog.merchandising_assistant',
|
|
245
|
+
moduleId: 'catalog',
|
|
246
|
+
executionMode: 'object',
|
|
247
|
+
output: {
|
|
248
|
+
schemaName: 'MerchandisingProposal',
|
|
249
|
+
schema,
|
|
250
|
+
mode: 'generate',
|
|
251
|
+
} as never,
|
|
252
|
+
}),
|
|
253
|
+
])
|
|
254
|
+
await runAiAgentObject({
|
|
255
|
+
agentId: 'catalog.merchandising_assistant',
|
|
256
|
+
input: 'draft title variants',
|
|
257
|
+
authContext: baseAuth,
|
|
258
|
+
})
|
|
259
|
+
expect(stepCountIsMock).not.toHaveBeenCalled()
|
|
260
|
+
const callArg = generateObjectMock.mock.calls[0][0] as { stopWhen?: unknown }
|
|
261
|
+
expect('stopWhen' in callArg).toBe(false)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 5.16 — Phase 3 WS-D integration tests for the shared AI model
|
|
3
|
+
* factory (Step 5.1).
|
|
4
|
+
*
|
|
5
|
+
* Pins the full four-layer resolution chain against a provider-registry
|
|
6
|
+
* shim that mirrors the real `LlmProviderRegistry.resolveFirstConfigured`
|
|
7
|
+
* contract. The factory is stateless and re-reads env + registry on every
|
|
8
|
+
* `resolveModel` call, so we drive each scenario from the dep-injected
|
|
9
|
+
* `env` + `registry` fields (the Step 5.1 test seam) rather than mutating
|
|
10
|
+
* `process.env` on the shared test run. This keeps the test hermetic and
|
|
11
|
+
* the env cleanup trivial.
|
|
12
|
+
*
|
|
13
|
+
* Scenarios (per Step 5.16 spec):
|
|
14
|
+
* - callerOverride non-empty → wins over env + agent default + provider
|
|
15
|
+
* - env `<MODULE>_AI_MODEL` → wins over agent default + provider default
|
|
16
|
+
* - agentDefaultModel → wins over provider default
|
|
17
|
+
* - provider default → chosen last
|
|
18
|
+
* - no provider registered → throws `AiModelFactoryError`
|
|
19
|
+
* `code: 'no_provider_configured'`
|
|
20
|
+
* - moduleId: undefined → env-override lookup skipped (regression)
|
|
21
|
+
* - empty-string callerOverride → falls through to env, not override
|
|
22
|
+
*
|
|
23
|
+
* Fixture rule: every test constructs its own provider + env shim, so no
|
|
24
|
+
* ordering coupling exists between scenarios.
|
|
25
|
+
*/
|
|
26
|
+
import type { AwilixContainer } from 'awilix'
|
|
27
|
+
import {
|
|
28
|
+
AiModelFactoryError,
|
|
29
|
+
createModelFactory,
|
|
30
|
+
type CreateModelFactoryDependencies,
|
|
31
|
+
} from '../model-factory'
|
|
32
|
+
|
|
33
|
+
type FakeProvider = {
|
|
34
|
+
id: string
|
|
35
|
+
defaultModel: string
|
|
36
|
+
resolveApiKey: () => string | null
|
|
37
|
+
createModel: (options: { modelId: string; apiKey: string }) => unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeProvider(overrides: Partial<FakeProvider> = {}): FakeProvider {
|
|
41
|
+
return {
|
|
42
|
+
id: overrides.id ?? 'test-provider',
|
|
43
|
+
defaultModel: overrides.defaultModel ?? 'provider-default-model',
|
|
44
|
+
resolveApiKey: overrides.resolveApiKey ?? (() => 'test-api-key'),
|
|
45
|
+
createModel:
|
|
46
|
+
overrides.createModel ??
|
|
47
|
+
((options: { modelId: string; apiKey: string }) => ({
|
|
48
|
+
kind: 'fake-model',
|
|
49
|
+
modelId: options.modelId,
|
|
50
|
+
apiKey: options.apiKey,
|
|
51
|
+
})),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeDeps(
|
|
56
|
+
provider: FakeProvider | null,
|
|
57
|
+
env: Record<string, string | undefined> = {},
|
|
58
|
+
): CreateModelFactoryDependencies {
|
|
59
|
+
return {
|
|
60
|
+
registry: {
|
|
61
|
+
resolveFirstConfigured: () =>
|
|
62
|
+
provider as unknown as ReturnType<
|
|
63
|
+
NonNullable<CreateModelFactoryDependencies['registry']>['resolveFirstConfigured']
|
|
64
|
+
>,
|
|
65
|
+
},
|
|
66
|
+
env,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fakeContainer = {} as unknown as AwilixContainer
|
|
71
|
+
|
|
72
|
+
describe('Step 5.16 — model factory fallback chain (integration)', () => {
|
|
73
|
+
it('callerOverride wins over env + agentDefaultModel + provider default', () => {
|
|
74
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
75
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned' }
|
|
76
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, env))
|
|
77
|
+
const resolution = factory.resolveModel({
|
|
78
|
+
moduleId: 'inbox_ops',
|
|
79
|
+
agentDefaultModel: 'agent-pinned',
|
|
80
|
+
callerOverride: 'caller-wins',
|
|
81
|
+
})
|
|
82
|
+
expect(resolution.source).toBe('caller_override')
|
|
83
|
+
expect(resolution.modelId).toBe('caller-wins')
|
|
84
|
+
// Verify the model plumbing received the resolved id (not a later layer).
|
|
85
|
+
expect(resolution.model).toMatchObject({
|
|
86
|
+
kind: 'fake-model',
|
|
87
|
+
modelId: 'caller-wins',
|
|
88
|
+
apiKey: 'test-api-key',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('env <MODULE>_AI_MODEL wins over agentDefaultModel + provider default when moduleId is set', () => {
|
|
93
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
94
|
+
const env = { CATALOG_AI_MODEL: 'catalog-env-model' }
|
|
95
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, env))
|
|
96
|
+
const resolution = factory.resolveModel({
|
|
97
|
+
moduleId: 'catalog',
|
|
98
|
+
agentDefaultModel: 'agent-pinned',
|
|
99
|
+
})
|
|
100
|
+
expect(resolution.source).toBe('module_env')
|
|
101
|
+
expect(resolution.modelId).toBe('catalog-env-model')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('agentDefaultModel wins over provider default when callerOverride + env are absent', () => {
|
|
105
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
106
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, {}))
|
|
107
|
+
const resolution = factory.resolveModel({
|
|
108
|
+
moduleId: 'inbox_ops',
|
|
109
|
+
agentDefaultModel: 'agent-pinned',
|
|
110
|
+
})
|
|
111
|
+
expect(resolution.source).toBe('agent_default')
|
|
112
|
+
expect(resolution.modelId).toBe('agent-pinned')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('provider default is chosen last when no other source applies', () => {
|
|
116
|
+
const provider = makeProvider({ defaultModel: 'provider-last-resort' })
|
|
117
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, {}))
|
|
118
|
+
const resolution = factory.resolveModel({})
|
|
119
|
+
expect(resolution.source).toBe('provider_default')
|
|
120
|
+
expect(resolution.modelId).toBe('provider-last-resort')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('throws AiModelFactoryError with code "no_provider_configured" when no provider is registered', () => {
|
|
124
|
+
const factory = createModelFactory(fakeContainer, makeDeps(null))
|
|
125
|
+
expect(() => factory.resolveModel({})).toThrow(AiModelFactoryError)
|
|
126
|
+
try {
|
|
127
|
+
factory.resolveModel({})
|
|
128
|
+
fail('expected AiModelFactoryError')
|
|
129
|
+
} catch (err) {
|
|
130
|
+
expect(err).toBeInstanceOf(AiModelFactoryError)
|
|
131
|
+
const typed = err as AiModelFactoryError
|
|
132
|
+
expect(typed.code).toBe('no_provider_configured')
|
|
133
|
+
expect(typed.message).toMatch(/No LLM provider is configured/i)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('moduleId: undefined skips the env-override lookup (regression)', () => {
|
|
138
|
+
// Even if a `<MODULE>_AI_MODEL` env var exists in the environment, the
|
|
139
|
+
// factory does NOT construct a module-scoped env var name when moduleId
|
|
140
|
+
// is undefined — it falls straight through to agentDefaultModel /
|
|
141
|
+
// provider default. This guards against a past bug where `String(undefined)`
|
|
142
|
+
// yielded the literal env name `"UNDEFINED_AI_MODEL"`.
|
|
143
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
144
|
+
const env = {
|
|
145
|
+
INBOX_OPS_AI_MODEL: 'should-be-ignored',
|
|
146
|
+
UNDEFINED_AI_MODEL: 'also-ignored',
|
|
147
|
+
}
|
|
148
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, env))
|
|
149
|
+
const resolution = factory.resolveModel({
|
|
150
|
+
agentDefaultModel: 'agent-pinned',
|
|
151
|
+
})
|
|
152
|
+
expect(resolution.source).toBe('agent_default')
|
|
153
|
+
expect(resolution.modelId).toBe('agent-pinned')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('empty-string callerOverride falls through to env, not treated as override', () => {
|
|
157
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
158
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned' }
|
|
159
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, env))
|
|
160
|
+
const resolution = factory.resolveModel({
|
|
161
|
+
moduleId: 'inbox_ops',
|
|
162
|
+
agentDefaultModel: 'agent-pinned',
|
|
163
|
+
callerOverride: '',
|
|
164
|
+
})
|
|
165
|
+
expect(resolution.source).toBe('module_env')
|
|
166
|
+
expect(resolution.modelId).toBe('env-pinned')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('whitespace-only callerOverride is treated the same as empty (falls through)', () => {
|
|
170
|
+
// Defense-in-depth: a caller passing `" "` (e.g. from a UI text input)
|
|
171
|
+
// should not bypass lower-priority layers.
|
|
172
|
+
const provider = makeProvider({ defaultModel: 'provider-default' })
|
|
173
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned' }
|
|
174
|
+
const factory = createModelFactory(fakeContainer, makeDeps(provider, env))
|
|
175
|
+
const resolution = factory.resolveModel({
|
|
176
|
+
moduleId: 'inbox_ops',
|
|
177
|
+
agentDefaultModel: 'agent-pinned',
|
|
178
|
+
callerOverride: ' ',
|
|
179
|
+
})
|
|
180
|
+
expect(resolution.source).toBe('module_env')
|
|
181
|
+
expect(resolution.modelId).toBe('env-pinned')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import {
|
|
3
|
+
AiModelFactoryError,
|
|
4
|
+
createModelFactory,
|
|
5
|
+
type AiModelFactoryInput,
|
|
6
|
+
type CreateModelFactoryDependencies,
|
|
7
|
+
} from '../model-factory'
|
|
8
|
+
|
|
9
|
+
function makeProvider(overrides: Partial<{
|
|
10
|
+
id: string
|
|
11
|
+
defaultModel: string
|
|
12
|
+
resolveApiKey: () => string | null
|
|
13
|
+
createModel: (options: { modelId: string; apiKey: string }) => unknown
|
|
14
|
+
}> = {}) {
|
|
15
|
+
const createModel =
|
|
16
|
+
overrides.createModel ??
|
|
17
|
+
((options: { modelId: string; apiKey: string }) => ({
|
|
18
|
+
kind: 'fake-model',
|
|
19
|
+
modelId: options.modelId,
|
|
20
|
+
apiKey: options.apiKey,
|
|
21
|
+
}))
|
|
22
|
+
return {
|
|
23
|
+
id: overrides.id ?? 'test-provider',
|
|
24
|
+
defaultModel: overrides.defaultModel ?? 'provider-default-model',
|
|
25
|
+
resolveApiKey: overrides.resolveApiKey ?? (() => 'test-api-key'),
|
|
26
|
+
createModel,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeFactoryDeps(
|
|
31
|
+
provider: ReturnType<typeof makeProvider> | null,
|
|
32
|
+
env: Record<string, string | undefined> = {},
|
|
33
|
+
): CreateModelFactoryDependencies {
|
|
34
|
+
return {
|
|
35
|
+
registry: {
|
|
36
|
+
resolveFirstConfigured: () =>
|
|
37
|
+
provider as unknown as ReturnType<
|
|
38
|
+
NonNullable<CreateModelFactoryDependencies['registry']>['resolveFirstConfigured']
|
|
39
|
+
>,
|
|
40
|
+
},
|
|
41
|
+
env,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fakeContainer = {} as unknown as AwilixContainer
|
|
46
|
+
|
|
47
|
+
describe('createModelFactory', () => {
|
|
48
|
+
it('returns the provider default when no override is supplied', () => {
|
|
49
|
+
const provider = makeProvider()
|
|
50
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider))
|
|
51
|
+
const resolution = factory.resolveModel({})
|
|
52
|
+
expect(resolution.source).toBe('provider_default')
|
|
53
|
+
expect(resolution.modelId).toBe('provider-default-model')
|
|
54
|
+
expect(resolution.providerId).toBe('test-provider')
|
|
55
|
+
expect(resolution.model).toMatchObject({
|
|
56
|
+
kind: 'fake-model',
|
|
57
|
+
modelId: 'provider-default-model',
|
|
58
|
+
apiKey: 'test-api-key',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('prefers agentDefaultModel over provider default', () => {
|
|
63
|
+
const provider = makeProvider()
|
|
64
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider))
|
|
65
|
+
const resolution = factory.resolveModel({ agentDefaultModel: 'agent-pinned-model' })
|
|
66
|
+
expect(resolution.source).toBe('agent_default')
|
|
67
|
+
expect(resolution.modelId).toBe('agent-pinned-model')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('prefers <MODULE>_AI_MODEL env override over agent default', () => {
|
|
71
|
+
const provider = makeProvider()
|
|
72
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned-model' }
|
|
73
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
74
|
+
const resolution = factory.resolveModel({
|
|
75
|
+
moduleId: 'inbox_ops',
|
|
76
|
+
agentDefaultModel: 'agent-pinned-model',
|
|
77
|
+
})
|
|
78
|
+
expect(resolution.source).toBe('module_env')
|
|
79
|
+
expect(resolution.modelId).toBe('env-pinned-model')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('uppercases moduleId when deriving the env var name', () => {
|
|
83
|
+
const provider = makeProvider()
|
|
84
|
+
const env = { INBOX_OPS_AI_MODEL: 'from-env' }
|
|
85
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
86
|
+
const resolution = factory.resolveModel({ moduleId: 'inbox_ops' })
|
|
87
|
+
expect(resolution.modelId).toBe('from-env')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('prefers non-empty callerOverride over every other source', () => {
|
|
91
|
+
const provider = makeProvider()
|
|
92
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned-model' }
|
|
93
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
94
|
+
const resolution = factory.resolveModel({
|
|
95
|
+
moduleId: 'inbox_ops',
|
|
96
|
+
agentDefaultModel: 'agent-pinned-model',
|
|
97
|
+
callerOverride: 'caller-wins',
|
|
98
|
+
})
|
|
99
|
+
expect(resolution.source).toBe('caller_override')
|
|
100
|
+
expect(resolution.modelId).toBe('caller-wins')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('treats empty callerOverride as "no override" and falls through to env', () => {
|
|
104
|
+
const provider = makeProvider()
|
|
105
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned-model' }
|
|
106
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
107
|
+
const resolution = factory.resolveModel({
|
|
108
|
+
moduleId: 'inbox_ops',
|
|
109
|
+
agentDefaultModel: 'agent-pinned-model',
|
|
110
|
+
callerOverride: ' ',
|
|
111
|
+
})
|
|
112
|
+
expect(resolution.source).toBe('module_env')
|
|
113
|
+
expect(resolution.modelId).toBe('env-pinned-model')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('skips env-override lookup when moduleId is undefined (does not crash)', () => {
|
|
117
|
+
const provider = makeProvider()
|
|
118
|
+
// Even if an `_AI_MODEL` var is present, an absent moduleId means no
|
|
119
|
+
// module-scoped env var name can be constructed, so the lookup is skipped.
|
|
120
|
+
const env = { INBOX_OPS_AI_MODEL: 'env-pinned-model' }
|
|
121
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
122
|
+
const resolution = factory.resolveModel({
|
|
123
|
+
agentDefaultModel: 'agent-pinned-model',
|
|
124
|
+
} satisfies AiModelFactoryInput)
|
|
125
|
+
expect(resolution.source).toBe('agent_default')
|
|
126
|
+
expect(resolution.modelId).toBe('agent-pinned-model')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('throws AiModelFactoryError with code "no_provider_configured" when no provider is configured', () => {
|
|
130
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(null))
|
|
131
|
+
try {
|
|
132
|
+
factory.resolveModel({})
|
|
133
|
+
fail('expected AiModelFactoryError')
|
|
134
|
+
} catch (err) {
|
|
135
|
+
expect(err).toBeInstanceOf(AiModelFactoryError)
|
|
136
|
+
const typed = err as AiModelFactoryError
|
|
137
|
+
expect(typed.code).toBe('no_provider_configured')
|
|
138
|
+
expect(typed.message).toMatch(/No LLM provider is configured/i)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('throws AiModelFactoryError with code "api_key_missing" when the provider returns no key', () => {
|
|
143
|
+
const provider = makeProvider({ resolveApiKey: () => null })
|
|
144
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider))
|
|
145
|
+
try {
|
|
146
|
+
factory.resolveModel({})
|
|
147
|
+
fail('expected AiModelFactoryError')
|
|
148
|
+
} catch (err) {
|
|
149
|
+
expect(err).toBeInstanceOf(AiModelFactoryError)
|
|
150
|
+
expect((err as AiModelFactoryError).code).toBe('api_key_missing')
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('passes the resolved modelId and apiKey through to provider.createModel', () => {
|
|
155
|
+
const createModel = jest.fn((options: { modelId: string; apiKey: string }) => ({
|
|
156
|
+
spy: true,
|
|
157
|
+
...options,
|
|
158
|
+
}))
|
|
159
|
+
const provider = makeProvider({ createModel })
|
|
160
|
+
const env = { CATALOG_AI_MODEL: 'catalog-env-model' }
|
|
161
|
+
const factory = createModelFactory(fakeContainer, makeFactoryDeps(provider, env))
|
|
162
|
+
factory.resolveModel({ moduleId: 'catalog' })
|
|
163
|
+
expect(createModel).toHaveBeenCalledWith({
|
|
164
|
+
modelId: 'catalog-env-model',
|
|
165
|
+
apiKey: 'test-api-key',
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|