@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,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 3.8 — `meta.*` tool pack unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers `list_agents` empty-registry graceful case, RBAC filtering,
|
|
5
|
+
* super-admin bypass, `describe_agent` not-found / forbidden / happy,
|
|
6
|
+
* and the `output.schema` JSON-Schema fallback.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import type { AiAgentDefinition } from '../../lib/ai-agent-definition'
|
|
10
|
+
import {
|
|
11
|
+
resetAgentRegistryForTests,
|
|
12
|
+
seedAgentRegistryForTests,
|
|
13
|
+
} from '../../lib/agent-registry'
|
|
14
|
+
import metaAiTools from '../meta-pack'
|
|
15
|
+
|
|
16
|
+
function findTool(name: string) {
|
|
17
|
+
const tool = metaAiTools.find((entry) => entry.name === name)
|
|
18
|
+
if (!tool) throw new Error(`tool ${name} missing`)
|
|
19
|
+
return tool
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeAgent(overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>): AiAgentDefinition {
|
|
23
|
+
return {
|
|
24
|
+
label: `${overrides.id} label`,
|
|
25
|
+
description: `${overrides.id} description`,
|
|
26
|
+
systemPrompt: 'You are a test agent.',
|
|
27
|
+
allowedTools: [],
|
|
28
|
+
...overrides,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeCtx(overrides: Partial<{
|
|
33
|
+
tenantId: string | null
|
|
34
|
+
organizationId: string | null
|
|
35
|
+
userId: string | null
|
|
36
|
+
userFeatures: string[]
|
|
37
|
+
isSuperAdmin: boolean
|
|
38
|
+
}> = {}) {
|
|
39
|
+
return {
|
|
40
|
+
tenantId: 'tenant-1',
|
|
41
|
+
organizationId: 'org-1',
|
|
42
|
+
userId: 'user-1',
|
|
43
|
+
container: { resolve: jest.fn() },
|
|
44
|
+
userFeatures: ['ai_assistant.view'],
|
|
45
|
+
isSuperAdmin: false,
|
|
46
|
+
...overrides,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('meta.list_agents', () => {
|
|
51
|
+
const tool = findTool('meta.list_agents')
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
resetAgentRegistryForTests()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
resetAgentRegistryForTests()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns an empty array when the registry is empty (never throws)', async () => {
|
|
62
|
+
const ctx = makeCtx()
|
|
63
|
+
const result = (await tool.handler({}, ctx as any)) as Record<string, unknown>
|
|
64
|
+
expect(result.agents).toEqual([])
|
|
65
|
+
expect(result.total).toBe(0)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('filters by requiredFeatures based on the caller user features', async () => {
|
|
69
|
+
seedAgentRegistryForTests([
|
|
70
|
+
makeAgent({ id: 'catalog.read', moduleId: 'catalog', requiredFeatures: ['catalog.view'] }),
|
|
71
|
+
makeAgent({ id: 'catalog.write', moduleId: 'catalog', requiredFeatures: ['catalog.manage'] }),
|
|
72
|
+
makeAgent({ id: 'customers.read', moduleId: 'customers' }),
|
|
73
|
+
])
|
|
74
|
+
const ctx = makeCtx({ userFeatures: ['catalog.view', 'ai_assistant.view'] })
|
|
75
|
+
const result = (await tool.handler({}, ctx as any)) as Record<string, unknown>
|
|
76
|
+
const agents = result.agents as Array<Record<string, unknown>>
|
|
77
|
+
const ids = agents.map((agent) => agent.id).sort()
|
|
78
|
+
expect(ids).toEqual(['catalog.read', 'customers.read'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('super-admin sees every agent regardless of requiredFeatures', async () => {
|
|
82
|
+
seedAgentRegistryForTests([
|
|
83
|
+
makeAgent({ id: 'catalog.admin', moduleId: 'catalog', requiredFeatures: ['catalog.admin_only'] }),
|
|
84
|
+
makeAgent({ id: 'customers.mgr', moduleId: 'customers', requiredFeatures: ['customers.manage'] }),
|
|
85
|
+
])
|
|
86
|
+
const ctx = makeCtx({ userFeatures: [], isSuperAdmin: true })
|
|
87
|
+
const result = (await tool.handler({}, ctx as any)) as Record<string, unknown>
|
|
88
|
+
const agents = result.agents as Array<Record<string, unknown>>
|
|
89
|
+
expect(agents.map((agent) => agent.id).sort()).toEqual(['catalog.admin', 'customers.mgr'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('filters by moduleId when provided', async () => {
|
|
93
|
+
seedAgentRegistryForTests([
|
|
94
|
+
makeAgent({ id: 'catalog.a', moduleId: 'catalog' }),
|
|
95
|
+
makeAgent({ id: 'customers.a', moduleId: 'customers' }),
|
|
96
|
+
])
|
|
97
|
+
const ctx = makeCtx({ userFeatures: ['*'] })
|
|
98
|
+
const result = (await tool.handler({ moduleId: 'customers' }, ctx as any)) as Record<string, unknown>
|
|
99
|
+
const agents = result.agents as Array<Record<string, unknown>>
|
|
100
|
+
expect(agents.map((agent) => agent.id)).toEqual(['customers.a'])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('meta.describe_agent', () => {
|
|
105
|
+
const tool = findTool('meta.describe_agent')
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
resetAgentRegistryForTests()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
resetAgentRegistryForTests()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns { agent: null, reason: "not_found" } when the id is unknown', async () => {
|
|
116
|
+
const ctx = makeCtx()
|
|
117
|
+
const result = (await tool.handler({ agentId: 'no.such.agent' }, ctx as any)) as Record<string, unknown>
|
|
118
|
+
expect(result.agent).toBeNull()
|
|
119
|
+
expect(result.reason).toBe('not_found')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('returns { agent: null, reason: "forbidden" } when RBAC denies access', async () => {
|
|
123
|
+
seedAgentRegistryForTests([
|
|
124
|
+
makeAgent({
|
|
125
|
+
id: 'catalog.private',
|
|
126
|
+
moduleId: 'catalog',
|
|
127
|
+
requiredFeatures: ['catalog.private_feature'],
|
|
128
|
+
}),
|
|
129
|
+
])
|
|
130
|
+
const ctx = makeCtx({ userFeatures: ['ai_assistant.view'] })
|
|
131
|
+
const result = (await tool.handler({ agentId: 'catalog.private' }, ctx as any)) as Record<string, unknown>
|
|
132
|
+
expect(result.agent).toBeNull()
|
|
133
|
+
expect(result.reason).toBe('forbidden')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('returns a serialized description with JSON-Schema output when representable', async () => {
|
|
137
|
+
const schema = z.object({
|
|
138
|
+
title: z.string(),
|
|
139
|
+
price: z.number(),
|
|
140
|
+
})
|
|
141
|
+
seedAgentRegistryForTests([
|
|
142
|
+
makeAgent({
|
|
143
|
+
id: 'catalog.merch',
|
|
144
|
+
moduleId: 'catalog',
|
|
145
|
+
description: 'Merchandising helper',
|
|
146
|
+
allowedTools: ['search.hybrid_search', 'catalog.get_product_bundle'],
|
|
147
|
+
executionMode: 'object',
|
|
148
|
+
readOnly: true,
|
|
149
|
+
mutationPolicy: 'read-only',
|
|
150
|
+
acceptedMediaTypes: ['image', 'pdf'],
|
|
151
|
+
maxSteps: 6,
|
|
152
|
+
output: { schemaName: 'MerchProposal', schema, mode: 'generate' },
|
|
153
|
+
keywords: ['catalog', 'merch'],
|
|
154
|
+
domain: 'catalog',
|
|
155
|
+
}),
|
|
156
|
+
])
|
|
157
|
+
const ctx = makeCtx({ userFeatures: ['ai_assistant.view'] })
|
|
158
|
+
const result = (await tool.handler({ agentId: 'catalog.merch' }, ctx as any)) as Record<string, unknown>
|
|
159
|
+
const agent = result.agent as Record<string, unknown>
|
|
160
|
+
expect(agent.id).toBe('catalog.merch')
|
|
161
|
+
expect(agent.executionMode).toBe('object')
|
|
162
|
+
expect(agent.allowedTools).toEqual(['search.hybrid_search', 'catalog.get_product_bundle'])
|
|
163
|
+
expect(agent.readOnly).toBe(true)
|
|
164
|
+
expect(agent.acceptedMediaTypes).toEqual(['image', 'pdf'])
|
|
165
|
+
const output = agent.output as Record<string, unknown>
|
|
166
|
+
expect(output.schemaName).toBe('MerchProposal')
|
|
167
|
+
expect(output.jsonSchema).toBeDefined()
|
|
168
|
+
const prompt = agent.prompt as Record<string, unknown>
|
|
169
|
+
expect(prompt.systemPrompt).toBe('You are a test agent.')
|
|
170
|
+
expect(prompt.hasDynamicPageContext).toBe(false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('still returns the agent when output.schema is non-serializable — falls back to a note', async () => {
|
|
174
|
+
const brokenSchema = { _def: { typeName: 'ZodUnknown' } } as unknown as z.ZodType
|
|
175
|
+
seedAgentRegistryForTests([
|
|
176
|
+
makeAgent({
|
|
177
|
+
id: 'catalog.broken',
|
|
178
|
+
moduleId: 'catalog',
|
|
179
|
+
output: { schemaName: 'Broken', schema: brokenSchema },
|
|
180
|
+
}),
|
|
181
|
+
])
|
|
182
|
+
const ctx = makeCtx({ userFeatures: ['ai_assistant.view'] })
|
|
183
|
+
const result = (await tool.handler({ agentId: 'catalog.broken' }, ctx as any)) as Record<string, unknown>
|
|
184
|
+
const agent = result.agent as Record<string, unknown>
|
|
185
|
+
const output = agent.output as Record<string, unknown>
|
|
186
|
+
expect(output.schemaName).toBe('Broken')
|
|
187
|
+
const hasJsonSchema = Object.prototype.hasOwnProperty.call(output, 'jsonSchema')
|
|
188
|
+
const hasNote = Object.prototype.hasOwnProperty.call(output, 'note')
|
|
189
|
+
expect(hasJsonSchema || hasNote).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('hasPageContextResolver reflects whether the agent declared a resolvePageContext callback', async () => {
|
|
193
|
+
seedAgentRegistryForTests([
|
|
194
|
+
makeAgent({
|
|
195
|
+
id: 'catalog.page',
|
|
196
|
+
moduleId: 'catalog',
|
|
197
|
+
resolvePageContext: async () => 'context',
|
|
198
|
+
}),
|
|
199
|
+
])
|
|
200
|
+
const ctx = makeCtx({ userFeatures: ['ai_assistant.view'] })
|
|
201
|
+
const result = (await tool.handler({ agentId: 'catalog.page' }, ctx as any)) as Record<string, unknown>
|
|
202
|
+
const agent = result.agent as Record<string, unknown>
|
|
203
|
+
expect(agent.hasPageContextResolver).toBe(true)
|
|
204
|
+
const prompt = agent.prompt as Record<string, unknown>
|
|
205
|
+
expect(prompt.hasDynamicPageContext).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('meta-pack tool surface', () => {
|
|
210
|
+
it('exports the two read-only meta tools', () => {
|
|
211
|
+
const names = metaAiTools.map((tool) => tool.name)
|
|
212
|
+
expect(names).toEqual(['meta.list_agents', 'meta.describe_agent'])
|
|
213
|
+
for (const tool of metaAiTools) {
|
|
214
|
+
expect(tool.isMutation).not.toBe(true)
|
|
215
|
+
expect(tool.requiredFeatures).toEqual(['ai_assistant.view'])
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 3.8 — `search.*` tool pack unit tests.
|
|
3
|
+
*
|
|
4
|
+
* Covers `search.hybrid_search` happy path and `search.get_record_context`
|
|
5
|
+
* happy / miss / tenant isolation.
|
|
6
|
+
*/
|
|
7
|
+
import searchAiTools from '../search-pack'
|
|
8
|
+
|
|
9
|
+
type SearchCall = {
|
|
10
|
+
query: string
|
|
11
|
+
options: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ToolContext = {
|
|
15
|
+
tenantId: string | null
|
|
16
|
+
organizationId: string | null
|
|
17
|
+
userId: string | null
|
|
18
|
+
container: { resolve: (name: string) => unknown }
|
|
19
|
+
userFeatures: string[]
|
|
20
|
+
isSuperAdmin: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findTool(name: string) {
|
|
24
|
+
const tool = searchAiTools.find((entry) => entry.name === name)
|
|
25
|
+
if (!tool) throw new Error(`tool ${name} missing`)
|
|
26
|
+
return tool
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeCtx(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
30
|
+
const container = {
|
|
31
|
+
resolve: jest.fn(),
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
tenantId: 'tenant-1',
|
|
35
|
+
organizationId: 'org-1',
|
|
36
|
+
userId: 'user-1',
|
|
37
|
+
container,
|
|
38
|
+
userFeatures: ['search.view'],
|
|
39
|
+
isSuperAdmin: false,
|
|
40
|
+
...overrides,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeSearchService(results: unknown[]): {
|
|
45
|
+
service: { search: (query: string, options: Record<string, unknown>) => Promise<unknown[]> }
|
|
46
|
+
calls: SearchCall[]
|
|
47
|
+
} {
|
|
48
|
+
const calls: SearchCall[] = []
|
|
49
|
+
return {
|
|
50
|
+
calls,
|
|
51
|
+
service: {
|
|
52
|
+
search: async (query: string, options: Record<string, unknown>) => {
|
|
53
|
+
calls.push({ query, options })
|
|
54
|
+
return results
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('search.hybrid_search', () => {
|
|
61
|
+
const tool = findTool('search.hybrid_search')
|
|
62
|
+
|
|
63
|
+
it('passes tenant + organization scope and limits through to SearchService', async () => {
|
|
64
|
+
const { service, calls } = makeSearchService([
|
|
65
|
+
{
|
|
66
|
+
entityId: 'catalog:product',
|
|
67
|
+
recordId: 'rec-1',
|
|
68
|
+
score: 0.9,
|
|
69
|
+
source: 'fulltext',
|
|
70
|
+
presenter: { title: 'Product A' },
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
const ctx = makeCtx()
|
|
74
|
+
;(ctx.container.resolve as jest.Mock).mockImplementation((name: string) => {
|
|
75
|
+
if (name === 'searchService') return service
|
|
76
|
+
throw new Error(`unexpected resolve ${name}`)
|
|
77
|
+
})
|
|
78
|
+
const result = (await tool.handler(
|
|
79
|
+
{ q: 'widget', limit: 10, strategies: ['fulltext', 'vector'], entityTypes: ['catalog:product'] },
|
|
80
|
+
ctx as any,
|
|
81
|
+
)) as Record<string, unknown>
|
|
82
|
+
expect(calls).toHaveLength(1)
|
|
83
|
+
expect(calls[0].query).toBe('widget')
|
|
84
|
+
expect(calls[0].options).toMatchObject({
|
|
85
|
+
tenantId: 'tenant-1',
|
|
86
|
+
organizationId: 'org-1',
|
|
87
|
+
limit: 10,
|
|
88
|
+
strategies: ['fulltext', 'vector'],
|
|
89
|
+
entityTypes: ['catalog:product'],
|
|
90
|
+
})
|
|
91
|
+
expect(result.totalResults).toBe(1)
|
|
92
|
+
expect(result.strategiesUsed).toEqual(['fulltext'])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('defaults limit to 20 when omitted', async () => {
|
|
96
|
+
const { service, calls } = makeSearchService([])
|
|
97
|
+
const ctx = makeCtx()
|
|
98
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue(service)
|
|
99
|
+
await tool.handler({ q: 'hello' }, ctx as any)
|
|
100
|
+
expect(calls[0].options.limit).toBe(20)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('throws when tenant context is missing', async () => {
|
|
104
|
+
const ctx = makeCtx({ tenantId: null })
|
|
105
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue({ search: jest.fn() })
|
|
106
|
+
await expect(tool.handler({ q: 'x' }, ctx as any)).rejects.toThrow(/Tenant context/)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('search.get_record_context', () => {
|
|
111
|
+
const tool = findTool('search.get_record_context')
|
|
112
|
+
|
|
113
|
+
it('returns the matching hit with presenter/url/links', async () => {
|
|
114
|
+
const match = {
|
|
115
|
+
entityId: 'catalog:product',
|
|
116
|
+
recordId: 'rec-42',
|
|
117
|
+
score: 1,
|
|
118
|
+
source: 'fulltext',
|
|
119
|
+
presenter: { title: 'Widget' },
|
|
120
|
+
url: '/backend/catalog/catalog/products/rec-42',
|
|
121
|
+
links: [{ href: '/backend/catalog/catalog/products/rec-42', label: 'Open', kind: 'primary' }],
|
|
122
|
+
}
|
|
123
|
+
const { service, calls } = makeSearchService([
|
|
124
|
+
{ entityId: 'catalog:product', recordId: 'rec-99', score: 0.5, source: 'fulltext' },
|
|
125
|
+
match,
|
|
126
|
+
])
|
|
127
|
+
const ctx = makeCtx()
|
|
128
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue(service)
|
|
129
|
+
const result = (await tool.handler(
|
|
130
|
+
{ entityId: 'catalog:product', recordId: 'rec-42' },
|
|
131
|
+
ctx as any,
|
|
132
|
+
)) as Record<string, unknown>
|
|
133
|
+
expect(calls[0].query).toBe('rec-42')
|
|
134
|
+
expect(calls[0].options).toMatchObject({
|
|
135
|
+
tenantId: 'tenant-1',
|
|
136
|
+
organizationId: 'org-1',
|
|
137
|
+
limit: 5,
|
|
138
|
+
entityTypes: ['catalog:product'],
|
|
139
|
+
})
|
|
140
|
+
expect(result.found).toBe(true)
|
|
141
|
+
expect(result.recordId).toBe('rec-42')
|
|
142
|
+
expect(result.presenter).toEqual(match.presenter)
|
|
143
|
+
expect(result.url).toBe(match.url)
|
|
144
|
+
expect(result.links).toEqual(match.links)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns { found: false } when no hit matches the recordId', async () => {
|
|
148
|
+
const { service } = makeSearchService([
|
|
149
|
+
{ entityId: 'catalog:product', recordId: 'other', score: 0.2, source: 'fulltext' },
|
|
150
|
+
])
|
|
151
|
+
const ctx = makeCtx()
|
|
152
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue(service)
|
|
153
|
+
const result = (await tool.handler(
|
|
154
|
+
{ entityId: 'catalog:product', recordId: 'missing' },
|
|
155
|
+
ctx as any,
|
|
156
|
+
)) as Record<string, unknown>
|
|
157
|
+
expect(result.found).toBe(false)
|
|
158
|
+
expect(result.recordId).toBe('missing')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('passes the caller tenant/org and never leaks another tenant', async () => {
|
|
162
|
+
const { service, calls } = makeSearchService([])
|
|
163
|
+
const ctx = makeCtx({ tenantId: 'tenant-A', organizationId: 'org-A' })
|
|
164
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue(service)
|
|
165
|
+
await tool.handler({ entityId: 'x:y', recordId: 'z' }, ctx as any)
|
|
166
|
+
expect(calls[0].options).toMatchObject({
|
|
167
|
+
tenantId: 'tenant-A',
|
|
168
|
+
organizationId: 'org-A',
|
|
169
|
+
})
|
|
170
|
+
expect(calls[0].options).not.toHaveProperty('bypassTenantScope')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('throws when tenant context is missing', async () => {
|
|
174
|
+
const ctx = makeCtx({ tenantId: null })
|
|
175
|
+
;(ctx.container.resolve as jest.Mock).mockReturnValue({ search: jest.fn() })
|
|
176
|
+
await expect(
|
|
177
|
+
tool.handler({ entityId: 'x:y', recordId: 'z' }, ctx as any),
|
|
178
|
+
).rejects.toThrow(/Tenant context/)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('search-pack tool surface', () => {
|
|
183
|
+
it('exports exactly the expected tool names and shapes', () => {
|
|
184
|
+
const names = searchAiTools.map((tool) => tool.name)
|
|
185
|
+
expect(names).toEqual(['search.hybrid_search', 'search.get_record_context'])
|
|
186
|
+
for (const tool of searchAiTools) {
|
|
187
|
+
expect(typeof tool.description).toBe('string')
|
|
188
|
+
expect(tool.isMutation).not.toBe(true)
|
|
189
|
+
expect(tool.requiredFeatures).toContain('search.view')
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-purpose `attachments.*` tool pack (Phase 1 WS-C, Step 3.8).
|
|
3
|
+
*
|
|
4
|
+
* Read-only tools return metadata + optional extracted text; the
|
|
5
|
+
* attachment-to-model bridge (Step 3.7) owns raw bytes / signed URLs.
|
|
6
|
+
* The transfer tool is the only mutation — agents with `readOnly: true`
|
|
7
|
+
* are already filtered by the Step 3.2 policy gate.
|
|
8
|
+
*/
|
|
9
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import { defineAiTool } from '../lib/ai-tool-definition'
|
|
13
|
+
import type { AiToolDefinition } from '../lib/types'
|
|
14
|
+
|
|
15
|
+
type AttachmentMetadataModule = {
|
|
16
|
+
readAttachmentMetadata: (raw: unknown) => {
|
|
17
|
+
tags?: string[]
|
|
18
|
+
assignments?: Array<{ type: string; id: string; href?: string | null; label?: string | null }>
|
|
19
|
+
}
|
|
20
|
+
mergeAttachmentMetadata: (
|
|
21
|
+
raw: unknown,
|
|
22
|
+
patch: { assignments?: unknown; tags?: unknown },
|
|
23
|
+
) => Record<string, unknown>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AttachmentEntityModule = {
|
|
27
|
+
Attachment: new () => unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadAttachmentEntity(): Promise<AttachmentEntityModule['Attachment']> {
|
|
31
|
+
const mod = (await import(
|
|
32
|
+
'@open-mercato/core/modules/attachments/data/entities'
|
|
33
|
+
)) as AttachmentEntityModule
|
|
34
|
+
return mod.Attachment
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadAttachmentMetadata(): Promise<AttachmentMetadataModule> {
|
|
38
|
+
const mod = (await import(
|
|
39
|
+
'@open-mercato/core/modules/attachments/lib/metadata'
|
|
40
|
+
)) as AttachmentMetadataModule
|
|
41
|
+
return mod
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type AttachmentRow = {
|
|
45
|
+
id: string
|
|
46
|
+
entityId: string
|
|
47
|
+
recordId: string
|
|
48
|
+
fileName: string
|
|
49
|
+
mimeType: string
|
|
50
|
+
fileSize: number
|
|
51
|
+
storageMetadata?: Record<string, unknown> | null
|
|
52
|
+
url?: string
|
|
53
|
+
content?: string | null
|
|
54
|
+
tenantId?: string | null
|
|
55
|
+
organizationId?: string | null
|
|
56
|
+
partitionCode?: string
|
|
57
|
+
createdAt?: Date
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertTenantScope(ctx: { tenantId: string | null }): string {
|
|
61
|
+
if (!ctx.tenantId) {
|
|
62
|
+
throw new Error('Tenant context is required for attachments tools')
|
|
63
|
+
}
|
|
64
|
+
return ctx.tenantId
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveEm(ctx: {
|
|
68
|
+
container: { resolve: <T = unknown>(name: string) => T }
|
|
69
|
+
}): EntityManager {
|
|
70
|
+
return ctx.container.resolve<EntityManager>('em')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const listInput = z.object({
|
|
74
|
+
entityType: z.string().min(1).describe('Entity identifier (e.g. "customers:customer_person_profile").'),
|
|
75
|
+
recordId: z.string().min(1).describe('Record identifier within that entity.'),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const listRecordAttachmentsTool = defineAiTool({
|
|
79
|
+
name: 'attachments.list_record_attachments',
|
|
80
|
+
displayName: 'List record attachments',
|
|
81
|
+
description:
|
|
82
|
+
'List attachments bound to a record, scoped to the caller tenant and organization. Returns metadata only (no bytes, no signed URL).',
|
|
83
|
+
inputSchema: listInput,
|
|
84
|
+
requiredFeatures: ['attachments.view'],
|
|
85
|
+
tags: ['read', 'attachments'],
|
|
86
|
+
handler: async (rawInput, ctx) => {
|
|
87
|
+
const tenantId = assertTenantScope(ctx)
|
|
88
|
+
const input = listInput.parse(rawInput)
|
|
89
|
+
const em = resolveEm(ctx)
|
|
90
|
+
const Attachment = await loadAttachmentEntity()
|
|
91
|
+
const where: Record<string, unknown> = {
|
|
92
|
+
entityId: input.entityType,
|
|
93
|
+
recordId: input.recordId,
|
|
94
|
+
tenantId,
|
|
95
|
+
}
|
|
96
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
97
|
+
const rows = (await findWithDecryption<AttachmentRow>(
|
|
98
|
+
em,
|
|
99
|
+
Attachment as unknown as new () => AttachmentRow,
|
|
100
|
+
where,
|
|
101
|
+
{ orderBy: { createdAt: 'desc' } as any },
|
|
102
|
+
{ tenantId, organizationId: ctx.organizationId },
|
|
103
|
+
)) as AttachmentRow[]
|
|
104
|
+
return {
|
|
105
|
+
entityType: input.entityType,
|
|
106
|
+
recordId: input.recordId,
|
|
107
|
+
total: rows.length,
|
|
108
|
+
items: rows.map((row) => ({
|
|
109
|
+
id: row.id,
|
|
110
|
+
entityType: row.entityId,
|
|
111
|
+
recordId: row.recordId,
|
|
112
|
+
fileName: row.fileName,
|
|
113
|
+
mediaType: row.mimeType,
|
|
114
|
+
size: row.fileSize,
|
|
115
|
+
partitionCode: row.partitionCode,
|
|
116
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
117
|
+
})),
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const readInput = z.object({
|
|
123
|
+
attachmentId: z.string().uuid().describe('Attachment identifier.'),
|
|
124
|
+
includeExtractedText: z
|
|
125
|
+
.boolean()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('When true, include the stored extracted / OCR text if present (default false).'),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const readAttachmentTool = defineAiTool({
|
|
131
|
+
name: 'attachments.read_attachment',
|
|
132
|
+
displayName: 'Read attachment metadata',
|
|
133
|
+
description:
|
|
134
|
+
'Return attachment metadata, tags, assignments, and optionally the stored extracted text. Never returns raw bytes or signed URLs.',
|
|
135
|
+
inputSchema: readInput,
|
|
136
|
+
requiredFeatures: ['attachments.view'],
|
|
137
|
+
tags: ['read', 'attachments'],
|
|
138
|
+
handler: async (rawInput, ctx) => {
|
|
139
|
+
const tenantId = assertTenantScope(ctx)
|
|
140
|
+
const input = readInput.parse(rawInput)
|
|
141
|
+
const em = resolveEm(ctx)
|
|
142
|
+
const Attachment = await loadAttachmentEntity()
|
|
143
|
+
const { readAttachmentMetadata } = await loadAttachmentMetadata()
|
|
144
|
+
const where: Record<string, unknown> = { id: input.attachmentId, tenantId }
|
|
145
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
146
|
+
const row = (await findOneWithDecryption<AttachmentRow>(
|
|
147
|
+
em,
|
|
148
|
+
Attachment as unknown as new () => AttachmentRow,
|
|
149
|
+
where,
|
|
150
|
+
undefined,
|
|
151
|
+
{ tenantId, organizationId: ctx.organizationId },
|
|
152
|
+
)) as AttachmentRow | null
|
|
153
|
+
if (!row) {
|
|
154
|
+
return { found: false as const, attachmentId: input.attachmentId }
|
|
155
|
+
}
|
|
156
|
+
const metadata = readAttachmentMetadata(row.storageMetadata)
|
|
157
|
+
return {
|
|
158
|
+
found: true as const,
|
|
159
|
+
id: row.id,
|
|
160
|
+
entityType: row.entityId,
|
|
161
|
+
recordId: row.recordId,
|
|
162
|
+
fileName: row.fileName,
|
|
163
|
+
mediaType: row.mimeType,
|
|
164
|
+
size: row.fileSize,
|
|
165
|
+
partitionCode: row.partitionCode,
|
|
166
|
+
createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
|
|
167
|
+
tags: metadata.tags ?? [],
|
|
168
|
+
assignments: metadata.assignments ?? [],
|
|
169
|
+
extractedText:
|
|
170
|
+
input.includeExtractedText === true && typeof row.content === 'string' ? row.content : null,
|
|
171
|
+
hasExtractedText: typeof row.content === 'string' && row.content.length > 0,
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const transferInput = z.object({
|
|
177
|
+
fromEntityType: z.string().min(1).describe('Current entity type of the source attachments.'),
|
|
178
|
+
fromRecordId: z.string().min(1).describe('Current record id the attachments are bound to.'),
|
|
179
|
+
toEntityType: z.string().min(1).describe('Target entity type (must match the source).'),
|
|
180
|
+
toRecordId: z.string().min(1).describe('Target record id to re-bind the attachments to.'),
|
|
181
|
+
attachmentIds: z
|
|
182
|
+
.array(z.string().uuid())
|
|
183
|
+
.min(1)
|
|
184
|
+
.max(100)
|
|
185
|
+
.optional()
|
|
186
|
+
.describe('Optional subset; defaults to every attachment on the source record.'),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const transferRecordAttachmentsTool = defineAiTool({
|
|
190
|
+
name: 'attachments.transfer_record_attachments',
|
|
191
|
+
displayName: 'Transfer record attachments',
|
|
192
|
+
description:
|
|
193
|
+
'Move uploaded files from a temporary/draft record to a saved record. Mutation tool — agents with readOnly=true are blocked by the policy gate.',
|
|
194
|
+
inputSchema: transferInput,
|
|
195
|
+
isMutation: true,
|
|
196
|
+
requiredFeatures: ['attachments.manage'],
|
|
197
|
+
tags: ['write', 'attachments'],
|
|
198
|
+
handler: async (rawInput, ctx) => {
|
|
199
|
+
const tenantId = assertTenantScope(ctx)
|
|
200
|
+
const input = transferInput.parse(rawInput)
|
|
201
|
+
if (input.fromEntityType !== input.toEntityType) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
'attachments.transfer_record_attachments requires fromEntityType and toEntityType to match',
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
const em = resolveEm(ctx)
|
|
207
|
+
const Attachment = await loadAttachmentEntity()
|
|
208
|
+
const { readAttachmentMetadata, mergeAttachmentMetadata } = await loadAttachmentMetadata()
|
|
209
|
+
const where: Record<string, unknown> = {
|
|
210
|
+
entityId: input.fromEntityType,
|
|
211
|
+
recordId: input.fromRecordId,
|
|
212
|
+
tenantId,
|
|
213
|
+
}
|
|
214
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
215
|
+
if (input.attachmentIds && input.attachmentIds.length > 0) {
|
|
216
|
+
where.id = { $in: input.attachmentIds }
|
|
217
|
+
}
|
|
218
|
+
const rows = (await findWithDecryption<AttachmentRow>(
|
|
219
|
+
em,
|
|
220
|
+
Attachment as unknown as new () => AttachmentRow,
|
|
221
|
+
where,
|
|
222
|
+
undefined,
|
|
223
|
+
{ tenantId, organizationId: ctx.organizationId },
|
|
224
|
+
)) as AttachmentRow[]
|
|
225
|
+
if (!rows.length) {
|
|
226
|
+
return {
|
|
227
|
+
transferred: 0,
|
|
228
|
+
fromEntityType: input.fromEntityType,
|
|
229
|
+
fromRecordId: input.fromRecordId,
|
|
230
|
+
toEntityType: input.toEntityType,
|
|
231
|
+
toRecordId: input.toRecordId,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
const previousRecordId = row.recordId
|
|
236
|
+
row.recordId = input.toRecordId
|
|
237
|
+
const metadata = readAttachmentMetadata(row.storageMetadata)
|
|
238
|
+
const nextAssignments =
|
|
239
|
+
metadata.assignments?.map((assignment) => {
|
|
240
|
+
const matchesType = assignment.type === input.fromEntityType
|
|
241
|
+
const matchesRecord = assignment.id === previousRecordId
|
|
242
|
+
if (matchesType && matchesRecord) {
|
|
243
|
+
return { ...assignment, id: input.toRecordId }
|
|
244
|
+
}
|
|
245
|
+
return assignment
|
|
246
|
+
}) ?? []
|
|
247
|
+
row.storageMetadata = mergeAttachmentMetadata(row.storageMetadata, {
|
|
248
|
+
assignments: nextAssignments,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
await em.persist(rows).flush()
|
|
252
|
+
return {
|
|
253
|
+
transferred: rows.length,
|
|
254
|
+
fromEntityType: input.fromEntityType,
|
|
255
|
+
fromRecordId: input.fromRecordId,
|
|
256
|
+
toEntityType: input.toEntityType,
|
|
257
|
+
toRecordId: input.toRecordId,
|
|
258
|
+
attachmentIds: rows.map((row) => row.id),
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
export const attachmentsAiTools: AiToolDefinition<any, any>[] = [
|
|
264
|
+
listRecordAttachmentsTool,
|
|
265
|
+
readAttachmentTool,
|
|
266
|
+
transferRecordAttachmentsTool,
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
export default attachmentsAiTools
|