@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,207 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
2
|
+
import type { UIMessage } from 'ai'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { loadAgentRegistry } from '../../../lib/agent-registry'
|
|
9
|
+
import { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'
|
|
10
|
+
import { runAiAgentText } from '../../../lib/agent-runtime'
|
|
11
|
+
import { AgentPolicyError } from '../../../lib/agent-tools'
|
|
12
|
+
|
|
13
|
+
const MAX_MESSAGES = 100
|
|
14
|
+
|
|
15
|
+
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/
|
|
16
|
+
|
|
17
|
+
const chatMessageSchema = z.object({
|
|
18
|
+
role: z.enum(['user', 'assistant', 'system']),
|
|
19
|
+
content: z.string(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const pageContextSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
pageId: z.string().nullable().optional(),
|
|
25
|
+
entityType: z.string().nullable().optional(),
|
|
26
|
+
recordId: z.string().nullable().optional(),
|
|
27
|
+
})
|
|
28
|
+
.passthrough()
|
|
29
|
+
|
|
30
|
+
const chatRequestSchema = z.object({
|
|
31
|
+
messages: z
|
|
32
|
+
.array(chatMessageSchema)
|
|
33
|
+
.min(1, 'messages must contain at least one message')
|
|
34
|
+
.max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),
|
|
35
|
+
attachmentIds: z.array(z.string()).optional(),
|
|
36
|
+
debug: z.boolean().optional(),
|
|
37
|
+
pageContext: pageContextSchema.optional(),
|
|
38
|
+
/**
|
|
39
|
+
* Optional stable conversation id forwarded from `<AiChat>`. Bridged into
|
|
40
|
+
* the Step 5.6 `prepareMutation` idempotency hash so repeated turns within
|
|
41
|
+
* the same chat collapse onto the same pending action. Additive; omitted
|
|
42
|
+
* bodies continue to work as before.
|
|
43
|
+
*/
|
|
44
|
+
conversationId: z.string().min(1).max(128).optional(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export type AiChatRequest = z.infer<typeof chatRequestSchema>
|
|
48
|
+
|
|
49
|
+
const agentQuerySchema = z.object({
|
|
50
|
+
agent: z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)'),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const openApi: OpenApiRouteDoc = {
|
|
56
|
+
tag: 'AI Assistant',
|
|
57
|
+
summary: 'AI agent dispatcher',
|
|
58
|
+
methods: {
|
|
59
|
+
POST: {
|
|
60
|
+
operationId: 'aiAssistantChatAgent',
|
|
61
|
+
summary: 'Stream a chat turn for a registered AI agent',
|
|
62
|
+
description:
|
|
63
|
+
'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +
|
|
64
|
+
'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +
|
|
65
|
+
'execution-mode compatibility, and attachment media-type policy. The streaming response ' +
|
|
66
|
+
'body uses an AI SDK-compatible `text/event-stream` transport.',
|
|
67
|
+
query: agentQuerySchema,
|
|
68
|
+
requestBody: {
|
|
69
|
+
contentType: 'application/json',
|
|
70
|
+
description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',
|
|
71
|
+
schema: chatRequestSchema,
|
|
72
|
+
},
|
|
73
|
+
responses: [
|
|
74
|
+
{ status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },
|
|
75
|
+
],
|
|
76
|
+
errors: [
|
|
77
|
+
{ status: 400, description: 'Invalid query param, malformed payload, or message count above the cap.' },
|
|
78
|
+
{ status: 401, description: 'Unauthenticated caller.' },
|
|
79
|
+
{ status: 403, description: 'Caller lacks agent-level or tool-level required features.' },
|
|
80
|
+
{ status: 404, description: 'Unknown agent id.' },
|
|
81
|
+
{ status: 409, description: 'Agent/tool/execution-mode policy violation.' },
|
|
82
|
+
{ status: 500, description: 'Internal runtime failure.' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const metadata = {
|
|
89
|
+
POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function jsonError(
|
|
93
|
+
status: number,
|
|
94
|
+
message: string,
|
|
95
|
+
code: string,
|
|
96
|
+
extra?: Record<string, unknown>,
|
|
97
|
+
): NextResponse {
|
|
98
|
+
return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function statusForDenyCode(code: AgentPolicyDenyCode): number {
|
|
102
|
+
switch (code) {
|
|
103
|
+
case 'agent_unknown':
|
|
104
|
+
return 404
|
|
105
|
+
case 'agent_features_denied':
|
|
106
|
+
case 'tool_features_denied':
|
|
107
|
+
return 403
|
|
108
|
+
case 'tool_not_whitelisted':
|
|
109
|
+
case 'tool_unknown':
|
|
110
|
+
case 'mutation_blocked_by_readonly':
|
|
111
|
+
case 'mutation_blocked_by_policy':
|
|
112
|
+
case 'execution_mode_not_supported':
|
|
113
|
+
return 409
|
|
114
|
+
case 'attachment_type_not_accepted':
|
|
115
|
+
return 400
|
|
116
|
+
default:
|
|
117
|
+
return 409
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
122
|
+
const auth = await getAuthFromRequest(req)
|
|
123
|
+
if (!auth) {
|
|
124
|
+
return jsonError(401, 'Unauthorized', 'unauthenticated')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const requestUrl = new URL(req.url)
|
|
128
|
+
const queryResult = agentQuerySchema.safeParse({
|
|
129
|
+
agent: requestUrl.searchParams.get('agent') ?? undefined,
|
|
130
|
+
})
|
|
131
|
+
if (!queryResult.success) {
|
|
132
|
+
return jsonError(400, 'Invalid or missing "agent" query parameter.', 'validation_error', {
|
|
133
|
+
issues: queryResult.error.issues,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
const agentId = queryResult.data.agent
|
|
137
|
+
|
|
138
|
+
let parsedBody: unknown
|
|
139
|
+
try {
|
|
140
|
+
parsedBody = await req.json()
|
|
141
|
+
} catch {
|
|
142
|
+
return jsonError(400, 'Request body must be valid JSON.', 'validation_error')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const bodyResult = chatRequestSchema.safeParse(parsedBody)
|
|
146
|
+
if (!bodyResult.success) {
|
|
147
|
+
return jsonError(400, 'Invalid request body.', 'validation_error', {
|
|
148
|
+
issues: bodyResult.error.issues,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await loadAgentRegistry()
|
|
154
|
+
|
|
155
|
+
const container = await createRequestContainer()
|
|
156
|
+
const rbacService = container.resolve<RbacService>('rbacService')
|
|
157
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
158
|
+
tenantId: auth.tenantId,
|
|
159
|
+
organizationId: auth.orgId,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const decision = checkAgentPolicy({
|
|
163
|
+
agentId,
|
|
164
|
+
authContext: {
|
|
165
|
+
userFeatures: acl.features,
|
|
166
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
167
|
+
},
|
|
168
|
+
requestedExecutionMode: 'chat',
|
|
169
|
+
// TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge
|
|
170
|
+
// once the attachment-to-model conversion bridge lands. Until then the
|
|
171
|
+
// policy gate skips attachment-type validation because media types are
|
|
172
|
+
// not known at dispatch time.
|
|
173
|
+
attachmentMediaTypes: undefined,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (!decision.ok) {
|
|
177
|
+
return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return await runAiAgentText({
|
|
181
|
+
agentId,
|
|
182
|
+
messages: bodyResult.data.messages as unknown as UIMessage[],
|
|
183
|
+
attachmentIds: bodyResult.data.attachmentIds,
|
|
184
|
+
pageContext: bodyResult.data.pageContext,
|
|
185
|
+
debug: bodyResult.data.debug,
|
|
186
|
+
conversationId: bodyResult.data.conversationId ?? null,
|
|
187
|
+
authContext: {
|
|
188
|
+
tenantId: auth.tenantId ?? null,
|
|
189
|
+
organizationId: auth.orgId ?? null,
|
|
190
|
+
userId: auth.sub,
|
|
191
|
+
features: acl.features,
|
|
192
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
193
|
+
},
|
|
194
|
+
container,
|
|
195
|
+
})
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error instanceof AgentPolicyError) {
|
|
198
|
+
return jsonError(statusForDenyCode(error.code), error.message, error.code)
|
|
199
|
+
}
|
|
200
|
+
console.error('[AI Chat Agent] Dispatch failure:', error)
|
|
201
|
+
return jsonError(
|
|
202
|
+
500,
|
|
203
|
+
error instanceof Error ? error.message : 'Agent dispatch failed.',
|
|
204
|
+
'internal_error',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { AiAgentDefinition } from '../../../../lib/ai-agent-definition'
|
|
3
|
+
import type { AiToolDefinition } from '../../../../lib/types'
|
|
4
|
+
import {
|
|
5
|
+
resetAgentRegistryForTests,
|
|
6
|
+
seedAgentRegistryForTests,
|
|
7
|
+
} from '../../../../lib/agent-registry'
|
|
8
|
+
import { toolRegistry, registerMcpTool } from '../../../../lib/tool-registry'
|
|
9
|
+
|
|
10
|
+
const authMock = jest.fn()
|
|
11
|
+
const loadAclMock = jest.fn()
|
|
12
|
+
const createRequestContainerMock = jest.fn()
|
|
13
|
+
const runAiAgentObjectMock = jest.fn()
|
|
14
|
+
|
|
15
|
+
jest.mock('@open-mercato/shared/lib/auth/server', () => ({
|
|
16
|
+
getAuthFromRequest: (...args: unknown[]) => authMock(...args),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
jest.mock('@open-mercato/shared/lib/di/container', () => ({
|
|
20
|
+
createRequestContainer: (...args: unknown[]) => createRequestContainerMock(...args),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
jest.mock('../../../../lib/agent-runtime', () => ({
|
|
24
|
+
runAiAgentObject: (...args: unknown[]) => runAiAgentObjectMock(...args),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
import { POST } from '../route'
|
|
28
|
+
|
|
29
|
+
function makeAgent(
|
|
30
|
+
overrides: Partial<AiAgentDefinition> & Pick<AiAgentDefinition, 'id' | 'moduleId'>,
|
|
31
|
+
): AiAgentDefinition {
|
|
32
|
+
return {
|
|
33
|
+
label: `${overrides.id} label`,
|
|
34
|
+
description: `${overrides.id} description`,
|
|
35
|
+
systemPrompt: 'You are a test agent.',
|
|
36
|
+
allowedTools: [],
|
|
37
|
+
...overrides,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeTool(
|
|
42
|
+
overrides: Partial<AiToolDefinition> & Pick<AiToolDefinition, 'name'>,
|
|
43
|
+
): AiToolDefinition {
|
|
44
|
+
return {
|
|
45
|
+
description: `${overrides.name} description`,
|
|
46
|
+
inputSchema: z.object({}),
|
|
47
|
+
handler: async () => ({ ok: true }),
|
|
48
|
+
...overrides,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildRequest(body: unknown): Request {
|
|
53
|
+
return new Request('http://localhost/api/ai_assistant/ai/run-object', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('POST /api/ai_assistant/ai/run-object', () => {
|
|
61
|
+
let consoleErrorSpy: jest.SpyInstance
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
jest.clearAllMocks()
|
|
65
|
+
resetAgentRegistryForTests()
|
|
66
|
+
toolRegistry.clear()
|
|
67
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
68
|
+
authMock.mockResolvedValue({
|
|
69
|
+
sub: 'user-1',
|
|
70
|
+
tenantId: 'tenant-1',
|
|
71
|
+
orgId: 'org-1',
|
|
72
|
+
})
|
|
73
|
+
loadAclMock.mockResolvedValue({ features: ['ai_assistant.view'], isSuperAdmin: false })
|
|
74
|
+
createRequestContainerMock.mockResolvedValue({
|
|
75
|
+
resolve: (name: string) => {
|
|
76
|
+
if (name === 'rbacService') return { loadAcl: loadAclMock }
|
|
77
|
+
return null
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
runAiAgentObjectMock.mockResolvedValue({
|
|
81
|
+
mode: 'generate',
|
|
82
|
+
object: { title: 'hello' },
|
|
83
|
+
finishReason: 'stop',
|
|
84
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
consoleErrorSpy.mockRestore()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
resetAgentRegistryForTests()
|
|
94
|
+
toolRegistry.clear()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns 401 when unauthenticated', async () => {
|
|
98
|
+
authMock.mockResolvedValueOnce(null)
|
|
99
|
+
|
|
100
|
+
const response = await POST(
|
|
101
|
+
buildRequest({
|
|
102
|
+
agent: 'catalog.extract',
|
|
103
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
104
|
+
}) as any,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(response.status).toBe(401)
|
|
108
|
+
const json = await response.json()
|
|
109
|
+
expect(json.code).toBe('unauthenticated')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('returns 400 when the body fails zod validation (missing messages)', async () => {
|
|
113
|
+
seedAgentRegistryForTests([
|
|
114
|
+
makeAgent({
|
|
115
|
+
id: 'catalog.extract',
|
|
116
|
+
moduleId: 'catalog',
|
|
117
|
+
executionMode: 'object',
|
|
118
|
+
output: { schemaName: 'Extract', schema: z.object({ title: z.string() }) },
|
|
119
|
+
}),
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
const response = await POST(buildRequest({ agent: 'catalog.extract' }) as any)
|
|
123
|
+
|
|
124
|
+
expect(response.status).toBe(400)
|
|
125
|
+
const json = await response.json()
|
|
126
|
+
expect(json.code).toBe('validation_error')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns 404 for an unknown agent', async () => {
|
|
130
|
+
const response = await POST(
|
|
131
|
+
buildRequest({
|
|
132
|
+
agent: 'catalog.missing',
|
|
133
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
134
|
+
}) as any,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
expect(response.status).toBe(404)
|
|
138
|
+
const json = await response.json()
|
|
139
|
+
expect(json.code).toBe('agent_unknown')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns 403 when the agent requires features the user lacks', async () => {
|
|
143
|
+
seedAgentRegistryForTests([
|
|
144
|
+
makeAgent({
|
|
145
|
+
id: 'catalog.extract',
|
|
146
|
+
moduleId: 'catalog',
|
|
147
|
+
executionMode: 'object',
|
|
148
|
+
requiredFeatures: ['catalog.extract.use'],
|
|
149
|
+
output: { schemaName: 'Extract', schema: z.object({ title: z.string() }) },
|
|
150
|
+
}),
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
const response = await POST(
|
|
154
|
+
buildRequest({
|
|
155
|
+
agent: 'catalog.extract',
|
|
156
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
157
|
+
}) as any,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(response.status).toBe(403)
|
|
161
|
+
const json = await response.json()
|
|
162
|
+
expect(json.code).toBe('agent_features_denied')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('returns 422 when a chat-mode agent is invoked via run-object', async () => {
|
|
166
|
+
seedAgentRegistryForTests([
|
|
167
|
+
makeAgent({
|
|
168
|
+
id: 'customers.assistant',
|
|
169
|
+
moduleId: 'customers',
|
|
170
|
+
}),
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
const response = await POST(
|
|
174
|
+
buildRequest({
|
|
175
|
+
agent: 'customers.assistant',
|
|
176
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
177
|
+
}) as any,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(response.status).toBe(422)
|
|
181
|
+
const json = await response.json()
|
|
182
|
+
expect(json.code).toBe('execution_mode_not_supported')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('returns 200 and delegates to runAiAgentObject on success', async () => {
|
|
186
|
+
registerMcpTool(
|
|
187
|
+
makeTool({ name: 'catalog.read_product', requiredFeatures: ['catalog.products.view'] }),
|
|
188
|
+
{ moduleId: 'catalog' },
|
|
189
|
+
)
|
|
190
|
+
seedAgentRegistryForTests([
|
|
191
|
+
makeAgent({
|
|
192
|
+
id: 'catalog.extract',
|
|
193
|
+
moduleId: 'catalog',
|
|
194
|
+
executionMode: 'object',
|
|
195
|
+
allowedTools: ['catalog.read_product'],
|
|
196
|
+
output: { schemaName: 'Extract', schema: z.object({ title: z.string() }) },
|
|
197
|
+
}),
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
const response = await POST(
|
|
201
|
+
buildRequest({
|
|
202
|
+
agent: 'catalog.extract',
|
|
203
|
+
messages: [{ role: 'user', content: 'Generate a product title' }],
|
|
204
|
+
pageContext: { pageId: 'ai_assistant.playground' },
|
|
205
|
+
}) as any,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
expect(response.status).toBe(200)
|
|
209
|
+
const json = await response.json()
|
|
210
|
+
expect(json).toEqual({
|
|
211
|
+
object: { title: 'hello' },
|
|
212
|
+
finishReason: 'stop',
|
|
213
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
214
|
+
})
|
|
215
|
+
expect(runAiAgentObjectMock).toHaveBeenCalledTimes(1)
|
|
216
|
+
const callArg = runAiAgentObjectMock.mock.calls[0][0] as {
|
|
217
|
+
agentId: string
|
|
218
|
+
input: unknown
|
|
219
|
+
pageContext?: { pageId?: string }
|
|
220
|
+
authContext: { userId: string; tenantId: string | null; organizationId: string | null }
|
|
221
|
+
}
|
|
222
|
+
expect(callArg.agentId).toBe('catalog.extract')
|
|
223
|
+
expect(callArg.pageContext).toEqual({ pageId: 'ai_assistant.playground' })
|
|
224
|
+
expect(callArg.authContext.userId).toBe('user-1')
|
|
225
|
+
expect(callArg.authContext.tenantId).toBe('tenant-1')
|
|
226
|
+
expect(callArg.authContext.organizationId).toBe('org-1')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('returns 422 when the helper resolves to stream mode', async () => {
|
|
230
|
+
seedAgentRegistryForTests([
|
|
231
|
+
makeAgent({
|
|
232
|
+
id: 'catalog.extract',
|
|
233
|
+
moduleId: 'catalog',
|
|
234
|
+
executionMode: 'object',
|
|
235
|
+
output: { schemaName: 'Extract', schema: z.object({ title: z.string() }) },
|
|
236
|
+
}),
|
|
237
|
+
])
|
|
238
|
+
runAiAgentObjectMock.mockResolvedValueOnce({
|
|
239
|
+
mode: 'stream',
|
|
240
|
+
object: Promise.resolve({ title: 'hello' }),
|
|
241
|
+
partialObjectStream: (async function* () {})(),
|
|
242
|
+
textStream: (async function* () {})(),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const response = await POST(
|
|
246
|
+
buildRequest({
|
|
247
|
+
agent: 'catalog.extract',
|
|
248
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
249
|
+
}) as any,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
expect(response.status).toBe(422)
|
|
253
|
+
const json = await response.json()
|
|
254
|
+
expect(json.code).toBe('execution_mode_not_supported')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('maps AgentPolicyError thrown by the runtime to the canonical HTTP status', async () => {
|
|
258
|
+
const { AgentPolicyError } = await import('../../../../lib/agent-tools')
|
|
259
|
+
seedAgentRegistryForTests([
|
|
260
|
+
makeAgent({
|
|
261
|
+
id: 'catalog.extract',
|
|
262
|
+
moduleId: 'catalog',
|
|
263
|
+
executionMode: 'object',
|
|
264
|
+
output: { schemaName: 'Extract', schema: z.object({ title: z.string() }) },
|
|
265
|
+
}),
|
|
266
|
+
])
|
|
267
|
+
runAiAgentObjectMock.mockRejectedValueOnce(
|
|
268
|
+
new AgentPolicyError('tool_not_whitelisted', 'Tool not whitelisted'),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const response = await POST(
|
|
272
|
+
buildRequest({
|
|
273
|
+
agent: 'catalog.extract',
|
|
274
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
275
|
+
}) as any,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(response.status).toBe(409)
|
|
279
|
+
const json = await response.json()
|
|
280
|
+
expect(json.code).toBe('tool_not_whitelisted')
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
2
|
+
import type { UIMessage } from 'ai'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { loadAgentRegistry } from '../../../lib/agent-registry'
|
|
9
|
+
import { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'
|
|
10
|
+
import { runAiAgentObject } from '../../../lib/agent-runtime'
|
|
11
|
+
import { AgentPolicyError } from '../../../lib/agent-tools'
|
|
12
|
+
|
|
13
|
+
const MAX_MESSAGES = 100
|
|
14
|
+
|
|
15
|
+
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/
|
|
16
|
+
|
|
17
|
+
const agentIdSchema = z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)')
|
|
20
|
+
|
|
21
|
+
const chatMessageSchema = z.object({
|
|
22
|
+
role: z.enum(['user', 'assistant', 'system']),
|
|
23
|
+
content: z.string(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const pageContextSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
pageId: z.string().optional(),
|
|
29
|
+
entityType: z.string().optional(),
|
|
30
|
+
recordId: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.passthrough()
|
|
33
|
+
|
|
34
|
+
const runObjectRequestSchema = z.object({
|
|
35
|
+
agent: agentIdSchema,
|
|
36
|
+
messages: z
|
|
37
|
+
.array(chatMessageSchema)
|
|
38
|
+
.min(1, 'messages must contain at least one message')
|
|
39
|
+
.max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),
|
|
40
|
+
attachmentIds: z.array(z.string()).optional(),
|
|
41
|
+
pageContext: pageContextSchema.optional(),
|
|
42
|
+
modelOverride: z.string().optional(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export type AiRunObjectRequest = z.infer<typeof runObjectRequestSchema>
|
|
46
|
+
|
|
47
|
+
export const openApi: OpenApiRouteDoc = {
|
|
48
|
+
tag: 'AI Assistant',
|
|
49
|
+
summary: 'Run an AI agent in structured-output (object) mode',
|
|
50
|
+
methods: {
|
|
51
|
+
POST: {
|
|
52
|
+
operationId: 'aiAssistantRunObject',
|
|
53
|
+
summary: 'Run an object-mode AI agent and return the generated object',
|
|
54
|
+
description:
|
|
55
|
+
'Invokes `runAiAgentObject` server-side for the registered AI agent identified by `agent` ' +
|
|
56
|
+
'(matching "<module>.<agent>"). Enforces the same `requiredFeatures`, tool whitelisting, ' +
|
|
57
|
+
'mutationPolicy, and attachment media-type policy as the chat dispatcher, but additionally ' +
|
|
58
|
+
'requires the agent to declare `executionMode: "object"`. Returns the generated object in ' +
|
|
59
|
+
'a single JSON response (no streaming).',
|
|
60
|
+
requestBody: {
|
|
61
|
+
contentType: 'application/json',
|
|
62
|
+
description:
|
|
63
|
+
'Object-mode dispatch payload. `agent` and `messages` are required; `attachmentIds`, `pageContext`, and `modelOverride` are optional.',
|
|
64
|
+
schema: runObjectRequestSchema,
|
|
65
|
+
},
|
|
66
|
+
responses: [
|
|
67
|
+
{
|
|
68
|
+
status: 200,
|
|
69
|
+
description: 'Object-mode run completed; response body contains `{ object, usage?, finishReason? }`.',
|
|
70
|
+
mediaType: 'application/json',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
errors: [
|
|
74
|
+
{ status: 400, description: 'Malformed payload or message cap exceeded.' },
|
|
75
|
+
{ status: 401, description: 'Unauthenticated caller.' },
|
|
76
|
+
{ status: 403, description: 'Caller lacks agent-level or tool-level required features.' },
|
|
77
|
+
{ status: 404, description: 'Unknown agent id.' },
|
|
78
|
+
{ status: 409, description: 'Agent/tool/execution-mode policy violation.' },
|
|
79
|
+
{ status: 422, description: 'Agent does not support object-mode execution.' },
|
|
80
|
+
{ status: 500, description: 'Internal runtime failure.' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const metadata = {
|
|
87
|
+
POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function jsonError(
|
|
91
|
+
status: number,
|
|
92
|
+
message: string,
|
|
93
|
+
code: string,
|
|
94
|
+
extra?: Record<string, unknown>,
|
|
95
|
+
): NextResponse {
|
|
96
|
+
return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function statusForDenyCode(code: AgentPolicyDenyCode): number {
|
|
100
|
+
switch (code) {
|
|
101
|
+
case 'agent_unknown':
|
|
102
|
+
return 404
|
|
103
|
+
case 'agent_features_denied':
|
|
104
|
+
case 'tool_features_denied':
|
|
105
|
+
return 403
|
|
106
|
+
case 'execution_mode_not_supported':
|
|
107
|
+
return 422
|
|
108
|
+
case 'tool_not_whitelisted':
|
|
109
|
+
case 'tool_unknown':
|
|
110
|
+
case 'mutation_blocked_by_readonly':
|
|
111
|
+
case 'mutation_blocked_by_policy':
|
|
112
|
+
return 409
|
|
113
|
+
case 'attachment_type_not_accepted':
|
|
114
|
+
return 400
|
|
115
|
+
default:
|
|
116
|
+
return 409
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
121
|
+
const auth = await getAuthFromRequest(req)
|
|
122
|
+
if (!auth) {
|
|
123
|
+
return jsonError(401, 'Unauthorized', 'unauthenticated')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let parsedBody: unknown
|
|
127
|
+
try {
|
|
128
|
+
parsedBody = await req.json()
|
|
129
|
+
} catch {
|
|
130
|
+
return jsonError(400, 'Request body must be valid JSON.', 'validation_error')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bodyResult = runObjectRequestSchema.safeParse(parsedBody)
|
|
134
|
+
if (!bodyResult.success) {
|
|
135
|
+
return jsonError(400, 'Invalid request body.', 'validation_error', {
|
|
136
|
+
issues: bodyResult.error.issues,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await loadAgentRegistry()
|
|
142
|
+
|
|
143
|
+
const container = await createRequestContainer()
|
|
144
|
+
const rbacService = container.resolve<RbacService>('rbacService')
|
|
145
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
146
|
+
tenantId: auth.tenantId,
|
|
147
|
+
organizationId: auth.orgId,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const decision = checkAgentPolicy({
|
|
151
|
+
agentId: bodyResult.data.agent,
|
|
152
|
+
authContext: {
|
|
153
|
+
userFeatures: acl.features,
|
|
154
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
155
|
+
},
|
|
156
|
+
requestedExecutionMode: 'object',
|
|
157
|
+
attachmentMediaTypes: undefined,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (!decision.ok) {
|
|
161
|
+
return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await runAiAgentObject({
|
|
165
|
+
agentId: bodyResult.data.agent,
|
|
166
|
+
input: bodyResult.data.messages as unknown as UIMessage[],
|
|
167
|
+
attachmentIds: bodyResult.data.attachmentIds,
|
|
168
|
+
pageContext: bodyResult.data.pageContext,
|
|
169
|
+
modelOverride: bodyResult.data.modelOverride,
|
|
170
|
+
authContext: {
|
|
171
|
+
tenantId: auth.tenantId ?? null,
|
|
172
|
+
organizationId: auth.orgId ?? null,
|
|
173
|
+
userId: auth.sub,
|
|
174
|
+
features: acl.features,
|
|
175
|
+
isSuperAdmin: acl.isSuperAdmin,
|
|
176
|
+
},
|
|
177
|
+
container,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (result.mode !== 'generate') {
|
|
181
|
+
return jsonError(
|
|
182
|
+
422,
|
|
183
|
+
'Streaming object-mode is not supported by the run-object HTTP route; agent must use generate mode.',
|
|
184
|
+
'execution_mode_not_supported',
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return NextResponse.json({
|
|
189
|
+
object: result.object,
|
|
190
|
+
finishReason: result.finishReason,
|
|
191
|
+
usage: result.usage,
|
|
192
|
+
})
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error instanceof AgentPolicyError) {
|
|
195
|
+
return jsonError(statusForDenyCode(error.code), error.message, error.code)
|
|
196
|
+
}
|
|
197
|
+
console.error('[AI Run Object] Dispatch failure:', error)
|
|
198
|
+
return jsonError(
|
|
199
|
+
500,
|
|
200
|
+
error instanceof Error ? error.message : 'Agent object dispatch failed.',
|
|
201
|
+
'internal_error',
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|