@open-mercato/ai-assistant 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6471.1.9ab5bdd79a
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/dist/frontend/hooks/useCommandPalette.js +16 -14
- package/dist/frontend/hooks/useCommandPalette.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +6 -7
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/run-object/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/chat/route.js +23 -14
- package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/health/route.js +3 -1
- package/dist/modules/ai_assistant/api/health/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/mcp-key/route.js +3 -1
- package/dist/modules/ai_assistant/api/mcp-key/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +11 -9
- package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/session-key/route.js +3 -1
- package/dist/modules/ai_assistant/api/session-key/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +4 -2
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +6 -4
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/execute/route.js +3 -1
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/route.js +3 -1
- package/dist/modules/ai_assistant/api/tools/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +2 -2
- package/dist/modules/ai_assistant/cli.js +3 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-policy.js +7 -5
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-registry.js +11 -13
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +21 -44
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-tools.js +5 -10
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-overrides.js +5 -9
- package/dist/modules/ai_assistant/lib/ai-overrides.js.map +2 -2
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +22 -22
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
- package/dist/modules/ai_assistant/lib/attachment-parts.js +10 -26
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +2 -2
- package/dist/modules/ai_assistant/lib/auth.js +3 -1
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +15 -17
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/entity-index.js +7 -5
- package/dist/modules/ai_assistant/lib/entity-index.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +3 -4
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +45 -48
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js +5 -12
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +24 -19
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +4 -2
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/opencode-client.js +5 -3
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +30 -12
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +3 -1
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +3 -1
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +4 -6
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
- package/dist/modules/ai_assistant/lib/schema-utils.js +3 -1
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +4 -8
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js +18 -25
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-registry.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +2 -2
- package/dist/modules/ai_assistant/setup.js +4 -8
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +3 -1
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +7 -19
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +5 -11
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +2 -2
- package/package.json +6 -6
- package/src/frontend/hooks/useCommandPalette.ts +17 -16
- package/src/modules/ai_assistant/__tests__/events.test.ts +23 -5
- package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +21 -3
- package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +22 -3
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +26 -5
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/chat/route.ts +7 -7
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/conversations/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/run-object/route.ts +4 -1
- package/src/modules/ai_assistant/api/chat/route.ts +24 -15
- package/src/modules/ai_assistant/api/health/route.ts +4 -1
- package/src/modules/ai_assistant/api/mcp-key/route.ts +4 -1
- package/src/modules/ai_assistant/api/route/route.ts +12 -9
- package/src/modules/ai_assistant/api/session-key/route.ts +4 -1
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +5 -2
- package/src/modules/ai_assistant/api/settings/route.ts +7 -4
- package/src/modules/ai_assistant/api/tools/execute/route.ts +4 -1
- package/src/modules/ai_assistant/api/tools/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/daily/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +4 -1
- package/src/modules/ai_assistant/cli.ts +5 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +20 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +25 -5
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +21 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +23 -3
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +20 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +22 -3
- package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +23 -3
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +25 -7
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +25 -5
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +19 -1
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +22 -3
- package/src/modules/ai_assistant/lib/agent-policy.ts +8 -5
- package/src/modules/ai_assistant/lib/agent-registry.ts +12 -15
- package/src/modules/ai_assistant/lib/agent-runtime.ts +23 -46
- package/src/modules/ai_assistant/lib/agent-tools.ts +7 -11
- package/src/modules/ai_assistant/lib/ai-overrides.ts +6 -9
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +23 -22
- package/src/modules/ai_assistant/lib/attachment-parts.ts +14 -29
- package/src/modules/ai_assistant/lib/auth.ts +4 -1
- package/src/modules/ai_assistant/lib/codemode-tools.ts +17 -17
- package/src/modules/ai_assistant/lib/entity-index.ts +8 -5
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +4 -4
- package/src/modules/ai_assistant/lib/http-server.ts +46 -48
- package/src/modules/ai_assistant/lib/llm-bootstrap.ts +7 -13
- package/src/modules/ai_assistant/lib/mcp-client.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-server.ts +27 -19
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +4 -1
- package/src/modules/ai_assistant/lib/model-factory.ts +6 -3
- package/src/modules/ai_assistant/lib/opencode-client.ts +6 -3
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +32 -12
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +4 -1
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +4 -1
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +5 -6
- package/src/modules/ai_assistant/lib/schema-utils.ts +4 -1
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +5 -8
- package/src/modules/ai_assistant/lib/tool-executor.ts +4 -1
- package/src/modules/ai_assistant/lib/tool-loader.ts +19 -25
- package/src/modules/ai_assistant/lib/tool-registry.ts +4 -1
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +4 -1
- package/src/modules/ai_assistant/setup.ts +5 -8
- package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +4 -1
- package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +8 -19
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +6 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/agent-tools.ts"],
|
|
4
|
-
"sourcesContent": ["import { dynamicTool, type Tool } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport type { ZodType } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport type { AiChatRequestContext, AiUiPart } from './attachment-bridge-types'\nimport type { AiToolDefinition, McpToolContext } from './types'\nimport { loadAgentRegistry } from './agent-registry'\nimport {\n checkAgentPolicy,\n resolveEffectiveMutationPolicy,\n type AgentPolicyDenyCode,\n} from './agent-policy'\nimport { toolRegistry } from './tool-registry'\nimport { toSafeZodSchema } from './schema-utils'\nimport { prepareMutation } from './prepare-mutation'\n\n/**\n * Error thrown by `resolveAiAgentTools` (and downstream `runAiAgentText`) when\n * the agent-level policy check denies a request. Carries the structured deny\n * code so HTTP dispatchers can map it to a stable status and JSON body.\n */\nexport class AgentPolicyError extends Error {\n readonly code: AgentPolicyDenyCode\n\n constructor(code: AgentPolicyDenyCode, message: string) {\n super(message)\n this.name = 'AgentPolicyError'\n this.code = code\n }\n}\n\nexport interface ResolveAiAgentToolsInput {\n agentId: string\n authContext: AiChatRequestContext\n pageContext?: Record<string, unknown>\n attachmentIds?: string[]\n /**\n * Execution mode the caller intends to run the agent in. Defaults to\n * `'chat'` to preserve the existing chat dispatcher contract. Object-mode\n * callers (see `runAiAgentObject`) MUST pass `'object'` so the policy gate\n * can reject chat-only agents early with `execution_mode_not_supported`.\n */\n requestedExecutionMode?: 'chat' | 'object'\n /**\n * Optional tenant-scoped mutation-policy DOWNGRADE. When supplied, the\n * effective policy evaluated by `checkAgentPolicy` is the most restrictive\n * of `{ agent.mutationPolicy, mutationPolicyOverride }`. Escalation is\n * rejected at the route layer; this helper trusts callers to pass only\n * values produced by the override repository.\n */\n mutationPolicyOverride?: AiAgentMutationPolicy | null\n /**\n * DI container used by the `prepareMutation` tool-call wrapper (Step 5.6).\n * When present AND the agent's effective mutation policy is non-read-only,\n * `isMutation: true` tools are intercepted: the runtime creates an\n * `AiPendingAction` row and enqueues a `mutation-preview-card` UI part in\n * the returned {@link ResolvedAgentTools.uiPartQueue} instead of running the\n * tool's handler. When absent, mutation tools degrade-gracefully to the\n * pre-5.6 pass-through adapter \u2014 existing read-only agents are unaffected.\n */\n container?: AwilixContainer\n /**\n * Optional chat-turn correlation id used when hashing the\n * `AiPendingAction.idempotencyKey` so retries of the same mutation collapse\n * to a single row. The chat dispatcher supplies the OpenCode / AI SDK turn\n * id here; when omitted the hash falls back to `null` which still preserves\n * per-tenant/org uniqueness within the TTL window.\n */\n conversationId?: string | null\n}\n\n/**\n * Queue of UI parts the mutation-preview wrapper accumulates during a turn.\n * The chat/object dispatcher flushes these on the next emission boundary\n * (spec \u00A79 allows either direct streaming or this queue pattern \u2014 we ship the\n * queue in Step 5.6 and the chat dispatcher will drain it in Step 5.10 when\n * the `mutation-preview-card` component registers). BC: callers that ignore\n * the field are unaffected.\n */\nexport interface AiUiPartQueue {\n /** Pushed by the mutation wrapper; drained by the dispatcher in order. */\n enqueue: (part: AiUiPart) => void\n drain: () => AiUiPart[]\n size: () => number\n}\n\nfunction createAiUiPartQueue(): AiUiPartQueue {\n const buffer: AiUiPart[] = []\n return {\n enqueue: (part) => {\n buffer.push(part)\n },\n drain: () => {\n const snapshot = buffer.slice()\n buffer.length = 0\n return snapshot\n },\n size: () => buffer.length,\n }\n}\n\nexport interface ResolvedAgentTools {\n agent: AiAgentDefinition\n tools: Record<string, Tool<unknown, unknown>>\n /**\n * Per-request UI-part queue the chat dispatcher drains between streamText\n * chunks (Step 5.10 contract). Always present; empty when no mutation-tool\n * calls fire during the turn.\n */\n uiPartQueue: AiUiPartQueue\n}\n\nfunction toPolicyAuthContext(ctx: AiChatRequestContext): {\n userFeatures: string[]\n isSuperAdmin: boolean\n} {\n return {\n userFeatures: ctx.features,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n}\n\n/**\n * Sanitize a dotted tool name (e.g. `search.hybrid_search`) into a format\n * accepted by all major LLM providers. OpenAI requires tool names to match\n * `^[a-zA-Z0-9_-]+$`; dots are replaced with double underscores (`__`).\n * Anthropic and Google accept both formats, so this is safe across providers.\n */\nexport function sanitizeToolNameForModel(name: string): string {\n return name.replace(/\\./g, '__')\n}\n\nexport function desanitizeToolNameForDisplay(name: string): string {\n return name.replace(/__/g, '.')\n}\n\nfunction formatToolResult(result: unknown): string {\n if (result === null || result === undefined) return 'No result returned'\n if (typeof result === 'string') return result\n if (typeof result === 'number' || typeof result === 'boolean') return String(result)\n try {\n return JSON.stringify(result, null, 2)\n } catch {\n return String(result)\n }\n}\n\nfunction buildToolHandlerContext(\n ctx: AiChatRequestContext,\n container?: AwilixContainer,\n tool?: AiToolDefinition,\n): McpToolContext {\n return {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n container: (container ?? undefined) as unknown as McpToolContext['container'],\n userFeatures: ctx.features,\n isSuperAdmin: ctx.isSuperAdmin,\n ...(tool ? { tool } : {}),\n }\n}\n\ninterface MutationInterceptorOptions {\n agent: AiAgentDefinition\n tool: AiToolDefinition\n container: AwilixContainer\n ctx: AiChatRequestContext\n mutationPolicyOverride: AiAgentMutationPolicy | null\n conversationId: string | null\n uiPartQueue: AiUiPartQueue\n}\n\nfunction formatPendingActionToolResult(\n agent: AiAgentDefinition,\n tool: AiToolDefinition,\n pendingActionId: string,\n expiresAt: Date,\n): string {\n return formatToolResult({\n status: 'pending-confirmation',\n agentId: agent.id,\n toolName: tool.name,\n pendingActionId,\n expiresAt: expiresAt.toISOString(),\n message: `Awaiting user confirmation for mutation \"${tool.name}\". The action will NOT run until the user approves it.`,\n })\n}\n\n/**\n * Per-call gating decision for the destructive-confirm-required policy.\n * Honors a static boolean OR a predicate evaluated against the actual\n * tool-call args, so a multi-operation tool (e.g.\n * `customers.manage_deal_comment` with `operation: 'create' | 'update'\n * | 'delete'`) can gate only its destructive branches without being\n * split into multiple tools.\n */\nfunction resolveIsDestructive(\n isDestructive: boolean | ((input: unknown) => boolean) | undefined,\n args: unknown,\n): boolean {\n if (typeof isDestructive === 'function') {\n try {\n return isDestructive(args) === true\n } catch {\n // If the predicate throws (e.g. unexpected input shape), default to\n // gating \u2014 fail-safe for destructive policy.\n return true\n }\n }\n return isDestructive === true\n}\n\nfunction adaptToolToAiSdk(\n tool: AiToolDefinition,\n ctx: AiChatRequestContext,\n mutation: MutationInterceptorOptions | null,\n container?: AwilixContainer,\n effectiveMutationPolicy?: 'read-only' | 'confirm-required' | 'destructive-confirm-required',\n): Tool<unknown, unknown> {\n const safeSchema = toSafeZodSchema(tool.inputSchema as ZodType)\n const handlerContext = buildToolHandlerContext(ctx, container, tool)\n return dynamicTool({\n description: tool.description,\n inputSchema: safeSchema,\n execute: async (args: unknown) => {\n // Per-call gating decision. Under `destructive-confirm-required`,\n // tools with a predicate `isDestructive(args)` may flip per call \u2014\n // e.g. comment delete gates while comment create runs direct.\n const requiresApprovalNow = (() => {\n if (!mutation) return false\n if (effectiveMutationPolicy === 'confirm-required') return true\n if (effectiveMutationPolicy === 'destructive-confirm-required') {\n return resolveIsDestructive(mutation.tool.isDestructive, args)\n }\n return false\n })()\n if (mutation && requiresApprovalNow) {\n try {\n const toolCallArgs =\n args && typeof args === 'object' && !Array.isArray(args)\n ? { ...(args as Record<string, unknown>) }\n : {}\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent: mutation.agent,\n tool: mutation.tool,\n toolCallArgs,\n conversationId: mutation.conversationId,\n mutationPolicyOverride: mutation.mutationPolicyOverride,\n },\n {\n tenantId: mutation.ctx.tenantId,\n organizationId: mutation.ctx.organizationId,\n userId: mutation.ctx.userId,\n features: mutation.ctx.features,\n isSuperAdmin: mutation.ctx.isSuperAdmin,\n container: mutation.container,\n },\n )\n mutation.uiPartQueue.enqueue(uiPart)\n return formatPendingActionToolResult(\n mutation.agent,\n mutation.tool,\n pendingAction.id,\n pendingAction.expiresAt,\n )\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `Tool \"${tool.name}\" could not be prepared for confirmation: ${message}`,\n )\n }\n }\n try {\n // Create a fresh container per tool call so the EM is never stale.\n const freshContainer = await createRequestContainer()\n const freshContext: McpToolContext = {\n ...handlerContext,\n container: freshContainer as unknown as McpToolContext['container'],\n }\n const { executeTool } = await import('./tool-executor')\n const execResult = await executeTool(tool.name, args, freshContext)\n if (!execResult.success) {\n throw new Error(execResult.error || 'Tool execution failed')\n }\n return formatToolResult(execResult.result)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`Tool \"${tool.name}\" failed: ${message}`)\n }\n },\n })\n}\n\n/**\n * Resolves the agent's whitelisted tools into an AI SDK `tools` map, enforcing\n * the same policy gate as the HTTP dispatcher. Throws {@link AgentPolicyError}\n * on agent-level deny; tool-level denies are skipped with a `console.warn`\n * because the agent author whitelisted a tool the caller is not currently\n * permitted to execute (deterministic non-failure \u2014 the remaining tools still\n * reach the model).\n */\nexport async function resolveAiAgentTools(\n input: ResolveAiAgentToolsInput,\n): Promise<ResolvedAgentTools> {\n await loadAgentRegistry()\n\n const policyAuth = toPolicyAuthContext(input.authContext)\n const mutationPolicyOverride = input.mutationPolicyOverride ?? null\n const agentDecision = checkAgentPolicy({\n agentId: input.agentId,\n authContext: policyAuth,\n requestedExecutionMode: input.requestedExecutionMode ?? 'chat',\n mutationPolicyOverride,\n })\n if (!agentDecision.ok) {\n throw new AgentPolicyError(agentDecision.code, agentDecision.message)\n }\n\n const { agent } = agentDecision\n const tools: Record<string, Tool<unknown, unknown>> = {}\n const uiPartQueue = createAiUiPartQueue()\n const effectiveMutationPolicy = resolveEffectiveMutationPolicy(\n agent.mutationPolicy,\n mutationPolicyOverride,\n agent.id,\n )\n const canInterceptMutations =\n effectiveMutationPolicy !== 'read-only' && typeof input.container !== 'undefined'\n\n for (const toolName of agent.allowedTools) {\n const toolDecision = checkAgentPolicy({\n agentId: input.agentId,\n authContext: policyAuth,\n toolName,\n mutationPolicyOverride,\n })\n if (!toolDecision.ok) {\n console.warn(\n `[AI Agents] Skipping tool \"${toolName}\" for agent \"${agent.id}\": ${toolDecision.message}`,\n )\n continue\n }\n\n const record = (toolDecision.tool ?? toolRegistry.getTool(toolName)) as\n | AiToolDefinition\n | undefined\n if (!record) {\n console.warn(\n `[AI Agents] Tool \"${toolName}\" vanished from registry between policy checks; skipping.`,\n )\n continue\n }\n\n try {\n // For any mutation tool whose policy isn't read-only, prepare the\n // interceptor options once. The actual gate-vs-run-direct decision\n // happens at call time inside `adaptToolToAiSdk` because under\n // `destructive-confirm-required` `isDestructive` may be a predicate\n // evaluated against the per-call args (e.g. a multi-operation\n // tool with `operation: 'create' | 'update' | 'delete'` gates only\n // the destructive branch).\n const mutationOptions: MutationInterceptorOptions | null =\n record.isMutation === true && canInterceptMutations && input.container\n ? {\n agent,\n tool: record,\n container: input.container,\n ctx: input.authContext,\n mutationPolicyOverride,\n conversationId: input.conversationId ?? null,\n uiPartQueue,\n }\n : null\n // Sanitize tool name for model API compatibility: OpenAI requires\n // names matching ^[a-zA-Z0-9_-]+$ (no dots). Replace dots with\n // double underscores so `search.hybrid_search` becomes\n // `search__hybrid_search`. The AI SDK uses the record key as the\n // tool name sent to the model; the original dotted name stays on\n // the `tool` object for logging and prepareMutation hashing.\n const modelSafeToolName = sanitizeToolNameForModel(toolName)\n tools[modelSafeToolName] = adaptToolToAiSdk(\n record,\n input.authContext,\n mutationOptions,\n input.container,\n effectiveMutationPolicy,\n )\n } catch (error) {\n console.error(\n `[AI Agents] Failed to adapt tool \"${toolName}\" for agent \"${agent.id}\":`,\n error,\n )\n }\n }\n\n return { agent, tools, uiPartQueue }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,mBAA8B;AAGvC,SAAS,8BAA8B;AAIvC,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { dynamicTool, type Tool } from 'ai'\nimport type { AwilixContainer } from 'awilix'\nimport type { ZodType } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { AiAgentDefinition, AiAgentMutationPolicy } from './ai-agent-definition'\nimport type { AiChatRequestContext, AiUiPart } from './attachment-bridge-types'\nimport type { AiToolDefinition, McpToolContext } from './types'\nimport { loadAgentRegistry } from './agent-registry'\nimport {\n checkAgentPolicy,\n resolveEffectiveMutationPolicy,\n type AgentPolicyDenyCode,\n} from './agent-policy'\nimport { toolRegistry } from './tool-registry'\nimport { toSafeZodSchema } from './schema-utils'\nimport { prepareMutation } from './prepare-mutation'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * Error thrown by `resolveAiAgentTools` (and downstream `runAiAgentText`) when\n * the agent-level policy check denies a request. Carries the structured deny\n * code so HTTP dispatchers can map it to a stable status and JSON body.\n */\nexport class AgentPolicyError extends Error {\n readonly code: AgentPolicyDenyCode\n\n constructor(code: AgentPolicyDenyCode, message: string) {\n super(message)\n this.name = 'AgentPolicyError'\n this.code = code\n }\n}\n\nexport interface ResolveAiAgentToolsInput {\n agentId: string\n authContext: AiChatRequestContext\n pageContext?: Record<string, unknown>\n attachmentIds?: string[]\n /**\n * Execution mode the caller intends to run the agent in. Defaults to\n * `'chat'` to preserve the existing chat dispatcher contract. Object-mode\n * callers (see `runAiAgentObject`) MUST pass `'object'` so the policy gate\n * can reject chat-only agents early with `execution_mode_not_supported`.\n */\n requestedExecutionMode?: 'chat' | 'object'\n /**\n * Optional tenant-scoped mutation-policy DOWNGRADE. When supplied, the\n * effective policy evaluated by `checkAgentPolicy` is the most restrictive\n * of `{ agent.mutationPolicy, mutationPolicyOverride }`. Escalation is\n * rejected at the route layer; this helper trusts callers to pass only\n * values produced by the override repository.\n */\n mutationPolicyOverride?: AiAgentMutationPolicy | null\n /**\n * DI container used by the `prepareMutation` tool-call wrapper (Step 5.6).\n * When present AND the agent's effective mutation policy is non-read-only,\n * `isMutation: true` tools are intercepted: the runtime creates an\n * `AiPendingAction` row and enqueues a `mutation-preview-card` UI part in\n * the returned {@link ResolvedAgentTools.uiPartQueue} instead of running the\n * tool's handler. When absent, mutation tools degrade-gracefully to the\n * pre-5.6 pass-through adapter \u2014 existing read-only agents are unaffected.\n */\n container?: AwilixContainer\n /**\n * Optional chat-turn correlation id used when hashing the\n * `AiPendingAction.idempotencyKey` so retries of the same mutation collapse\n * to a single row. The chat dispatcher supplies the OpenCode / AI SDK turn\n * id here; when omitted the hash falls back to `null` which still preserves\n * per-tenant/org uniqueness within the TTL window.\n */\n conversationId?: string | null\n}\n\n/**\n * Queue of UI parts the mutation-preview wrapper accumulates during a turn.\n * The chat/object dispatcher flushes these on the next emission boundary\n * (spec \u00A79 allows either direct streaming or this queue pattern \u2014 we ship the\n * queue in Step 5.6 and the chat dispatcher will drain it in Step 5.10 when\n * the `mutation-preview-card` component registers). BC: callers that ignore\n * the field are unaffected.\n */\nexport interface AiUiPartQueue {\n /** Pushed by the mutation wrapper; drained by the dispatcher in order. */\n enqueue: (part: AiUiPart) => void\n drain: () => AiUiPart[]\n size: () => number\n}\n\nfunction createAiUiPartQueue(): AiUiPartQueue {\n const buffer: AiUiPart[] = []\n return {\n enqueue: (part) => {\n buffer.push(part)\n },\n drain: () => {\n const snapshot = buffer.slice()\n buffer.length = 0\n return snapshot\n },\n size: () => buffer.length,\n }\n}\n\nexport interface ResolvedAgentTools {\n agent: AiAgentDefinition\n tools: Record<string, Tool<unknown, unknown>>\n /**\n * Per-request UI-part queue the chat dispatcher drains between streamText\n * chunks (Step 5.10 contract). Always present; empty when no mutation-tool\n * calls fire during the turn.\n */\n uiPartQueue: AiUiPartQueue\n}\n\nfunction toPolicyAuthContext(ctx: AiChatRequestContext): {\n userFeatures: string[]\n isSuperAdmin: boolean\n} {\n return {\n userFeatures: ctx.features,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n}\n\n/**\n * Sanitize a dotted tool name (e.g. `search.hybrid_search`) into a format\n * accepted by all major LLM providers. OpenAI requires tool names to match\n * `^[a-zA-Z0-9_-]+$`; dots are replaced with double underscores (`__`).\n * Anthropic and Google accept both formats, so this is safe across providers.\n */\nexport function sanitizeToolNameForModel(name: string): string {\n return name.replace(/\\./g, '__')\n}\n\nexport function desanitizeToolNameForDisplay(name: string): string {\n return name.replace(/__/g, '.')\n}\n\nfunction formatToolResult(result: unknown): string {\n if (result === null || result === undefined) return 'No result returned'\n if (typeof result === 'string') return result\n if (typeof result === 'number' || typeof result === 'boolean') return String(result)\n try {\n return JSON.stringify(result, null, 2)\n } catch {\n return String(result)\n }\n}\n\nfunction buildToolHandlerContext(\n ctx: AiChatRequestContext,\n container?: AwilixContainer,\n tool?: AiToolDefinition,\n): McpToolContext {\n return {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n container: (container ?? undefined) as unknown as McpToolContext['container'],\n userFeatures: ctx.features,\n isSuperAdmin: ctx.isSuperAdmin,\n ...(tool ? { tool } : {}),\n }\n}\n\ninterface MutationInterceptorOptions {\n agent: AiAgentDefinition\n tool: AiToolDefinition\n container: AwilixContainer\n ctx: AiChatRequestContext\n mutationPolicyOverride: AiAgentMutationPolicy | null\n conversationId: string | null\n uiPartQueue: AiUiPartQueue\n}\n\nfunction formatPendingActionToolResult(\n agent: AiAgentDefinition,\n tool: AiToolDefinition,\n pendingActionId: string,\n expiresAt: Date,\n): string {\n return formatToolResult({\n status: 'pending-confirmation',\n agentId: agent.id,\n toolName: tool.name,\n pendingActionId,\n expiresAt: expiresAt.toISOString(),\n message: `Awaiting user confirmation for mutation \"${tool.name}\". The action will NOT run until the user approves it.`,\n })\n}\n\n/**\n * Per-call gating decision for the destructive-confirm-required policy.\n * Honors a static boolean OR a predicate evaluated against the actual\n * tool-call args, so a multi-operation tool (e.g.\n * `customers.manage_deal_comment` with `operation: 'create' | 'update'\n * | 'delete'`) can gate only its destructive branches without being\n * split into multiple tools.\n */\nfunction resolveIsDestructive(\n isDestructive: boolean | ((input: unknown) => boolean) | undefined,\n args: unknown,\n): boolean {\n if (typeof isDestructive === 'function') {\n try {\n return isDestructive(args) === true\n } catch {\n // If the predicate throws (e.g. unexpected input shape), default to\n // gating \u2014 fail-safe for destructive policy.\n return true\n }\n }\n return isDestructive === true\n}\n\nfunction adaptToolToAiSdk(\n tool: AiToolDefinition,\n ctx: AiChatRequestContext,\n mutation: MutationInterceptorOptions | null,\n container?: AwilixContainer,\n effectiveMutationPolicy?: 'read-only' | 'confirm-required' | 'destructive-confirm-required',\n): Tool<unknown, unknown> {\n const safeSchema = toSafeZodSchema(tool.inputSchema as ZodType)\n const handlerContext = buildToolHandlerContext(ctx, container, tool)\n return dynamicTool({\n description: tool.description,\n inputSchema: safeSchema,\n execute: async (args: unknown) => {\n // Per-call gating decision. Under `destructive-confirm-required`,\n // tools with a predicate `isDestructive(args)` may flip per call \u2014\n // e.g. comment delete gates while comment create runs direct.\n const requiresApprovalNow = (() => {\n if (!mutation) return false\n if (effectiveMutationPolicy === 'confirm-required') return true\n if (effectiveMutationPolicy === 'destructive-confirm-required') {\n return resolveIsDestructive(mutation.tool.isDestructive, args)\n }\n return false\n })()\n if (mutation && requiresApprovalNow) {\n try {\n const toolCallArgs =\n args && typeof args === 'object' && !Array.isArray(args)\n ? { ...(args as Record<string, unknown>) }\n : {}\n const { uiPart, pendingAction } = await prepareMutation(\n {\n agent: mutation.agent,\n tool: mutation.tool,\n toolCallArgs,\n conversationId: mutation.conversationId,\n mutationPolicyOverride: mutation.mutationPolicyOverride,\n },\n {\n tenantId: mutation.ctx.tenantId,\n organizationId: mutation.ctx.organizationId,\n userId: mutation.ctx.userId,\n features: mutation.ctx.features,\n isSuperAdmin: mutation.ctx.isSuperAdmin,\n container: mutation.container,\n },\n )\n mutation.uiPartQueue.enqueue(uiPart)\n return formatPendingActionToolResult(\n mutation.agent,\n mutation.tool,\n pendingAction.id,\n pendingAction.expiresAt,\n )\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `Tool \"${tool.name}\" could not be prepared for confirmation: ${message}`,\n )\n }\n }\n try {\n // Create a fresh container per tool call so the EM is never stale.\n const freshContainer = await createRequestContainer()\n const freshContext: McpToolContext = {\n ...handlerContext,\n container: freshContainer as unknown as McpToolContext['container'],\n }\n const { executeTool } = await import('./tool-executor')\n const execResult = await executeTool(tool.name, args, freshContext)\n if (!execResult.success) {\n throw new Error(execResult.error || 'Tool execution failed')\n }\n return formatToolResult(execResult.result)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(`Tool \"${tool.name}\" failed: ${message}`)\n }\n },\n })\n}\n\n/**\n * Resolves the agent's whitelisted tools into an AI SDK `tools` map, enforcing\n * the same policy gate as the HTTP dispatcher. Throws {@link AgentPolicyError}\n * on agent-level deny; tool-level denies are skipped with a logger warning\n * because the agent author whitelisted a tool the caller is not currently\n * permitted to execute (deterministic non-failure \u2014 the remaining tools still\n * reach the model).\n */\nexport async function resolveAiAgentTools(\n input: ResolveAiAgentToolsInput,\n): Promise<ResolvedAgentTools> {\n await loadAgentRegistry()\n\n const policyAuth = toPolicyAuthContext(input.authContext)\n const mutationPolicyOverride = input.mutationPolicyOverride ?? null\n const agentDecision = checkAgentPolicy({\n agentId: input.agentId,\n authContext: policyAuth,\n requestedExecutionMode: input.requestedExecutionMode ?? 'chat',\n mutationPolicyOverride,\n })\n if (!agentDecision.ok) {\n throw new AgentPolicyError(agentDecision.code, agentDecision.message)\n }\n\n const { agent } = agentDecision\n const tools: Record<string, Tool<unknown, unknown>> = {}\n const uiPartQueue = createAiUiPartQueue()\n const effectiveMutationPolicy = resolveEffectiveMutationPolicy(\n agent.mutationPolicy,\n mutationPolicyOverride,\n agent.id,\n )\n const canInterceptMutations =\n effectiveMutationPolicy !== 'read-only' && typeof input.container !== 'undefined'\n\n for (const toolName of agent.allowedTools) {\n const toolDecision = checkAgentPolicy({\n agentId: input.agentId,\n authContext: policyAuth,\n toolName,\n mutationPolicyOverride,\n })\n if (!toolDecision.ok) {\n logger.warn('Skipping tool for agent', { toolName, agentId: agent.id, reason: toolDecision.message })\n continue\n }\n\n const record = (toolDecision.tool ?? toolRegistry.getTool(toolName)) as\n | AiToolDefinition\n | undefined\n if (!record) {\n logger.warn('Tool vanished from registry between policy checks; skipping', { toolName })\n continue\n }\n\n try {\n // For any mutation tool whose policy isn't read-only, prepare the\n // interceptor options once. The actual gate-vs-run-direct decision\n // happens at call time inside `adaptToolToAiSdk` because under\n // `destructive-confirm-required` `isDestructive` may be a predicate\n // evaluated against the per-call args (e.g. a multi-operation\n // tool with `operation: 'create' | 'update' | 'delete'` gates only\n // the destructive branch).\n const mutationOptions: MutationInterceptorOptions | null =\n record.isMutation === true && canInterceptMutations && input.container\n ? {\n agent,\n tool: record,\n container: input.container,\n ctx: input.authContext,\n mutationPolicyOverride,\n conversationId: input.conversationId ?? null,\n uiPartQueue,\n }\n : null\n // Sanitize tool name for model API compatibility: OpenAI requires\n // names matching ^[a-zA-Z0-9_-]+$ (no dots). Replace dots with\n // double underscores so `search.hybrid_search` becomes\n // `search__hybrid_search`. The AI SDK uses the record key as the\n // tool name sent to the model; the original dotted name stays on\n // the `tool` object for logging and prepareMutation hashing.\n const modelSafeToolName = sanitizeToolNameForModel(toolName)\n tools[modelSafeToolName] = adaptToolToAiSdk(\n record,\n input.authContext,\n mutationOptions,\n input.container,\n effectiveMutationPolicy,\n )\n } catch (error) {\n logger.error('Failed to adapt tool for agent', { toolName, agentId: agent.id, err: error })\n }\n }\n\n return { agent, tools, uiPartQueue }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import {
|
|
2
3
|
registerModuleOverrideApplier
|
|
3
4
|
} from "@open-mercato/shared/modules/overrides";
|
|
5
|
+
const logger = createLogger("ai_assistant");
|
|
4
6
|
const programmaticAgentOverrides = {};
|
|
5
7
|
const programmaticToolOverrides = {};
|
|
6
8
|
const programmaticAgentExtensions = [];
|
|
@@ -125,18 +127,14 @@ function applyAgentOverrideMap(base, overrides) {
|
|
|
125
127
|
}
|
|
126
128
|
for (const [id, value] of Object.entries(overrides)) {
|
|
127
129
|
if (!byId.has(id) && value !== null) {
|
|
128
|
-
|
|
129
|
-
`[AI Overrides] Override registers a new agent "${id}" \u2014 no base entry to replace.`
|
|
130
|
-
);
|
|
130
|
+
logger.warn("Override registers a new agent \u2014 no base entry to replace", { agentId: id });
|
|
131
131
|
}
|
|
132
132
|
if (value === null) {
|
|
133
133
|
byId.delete(id);
|
|
134
134
|
continue;
|
|
135
135
|
}
|
|
136
136
|
if (!value || typeof value.id !== "string" || value.id !== id) {
|
|
137
|
-
|
|
138
|
-
`[AI Overrides] Skipping malformed agent override for id "${id}" \u2014 id mismatch or missing fields.`
|
|
139
|
-
);
|
|
137
|
+
logger.warn("Skipping malformed agent override \u2014 id mismatch or missing fields", { agentId: id });
|
|
140
138
|
continue;
|
|
141
139
|
}
|
|
142
140
|
byId.set(id, value);
|
|
@@ -153,9 +151,7 @@ function applyToolOverrideMap(base, overrides) {
|
|
|
153
151
|
}
|
|
154
152
|
if (value === void 0) continue;
|
|
155
153
|
if (!value || typeof value.name !== "string" || value.name !== name) {
|
|
156
|
-
|
|
157
|
-
`[AI Overrides] Skipping malformed tool override for name "${name}" \u2014 name mismatch or missing fields.`
|
|
158
|
-
);
|
|
154
|
+
logger.warn("Skipping malformed tool override \u2014 name mismatch or missing fields", { toolName: name });
|
|
159
155
|
continue;
|
|
160
156
|
}
|
|
161
157
|
out.set(name, value);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/ai-overrides.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n registerModuleOverrideApplier,\n type ModuleOverrideEntry,\n} from '@open-mercato/shared/modules/overrides'\n\n/**\n * Module-to-module override pipeline for AI agents and AI tools.\n *\n * Modules contribute overrides through two surfaces:\n *\n * 1. Per-module: declare additional `aiAgentOverrides` / `aiToolOverrides`\n * exports in the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts`\n * files (no separate `ai-overrides.ts` file). The generator picks the\n * exports up alongside `aiAgents` / `aiTools` and emits override entries\n * inside `apps/<app>/.mercato/generated/ai-agents.generated.ts`\n * (`aiAgentOverrideEntries`) and `ai-tools.generated.ts`\n * (`aiToolOverrideEntries`).\n *\n * 2. App-level: declare `aiAgentOverrides` / `aiToolOverrides` directly on a\n * `ModuleEntry` inside the app's `src/modules.ts`. {@link\n * applyAiOverridesFromEnabledModules} feeds these into the runtime; they\n * sit one tier higher than the per-module file-based entries but below\n * explicit programmatic calls.\n *\n * Tests and bootstrap code can also override imperatively via\n * {@link applyAiAgentOverrides} and {@link applyAiToolOverrides}, which\n * supersede every other tier and persist for the process lifetime.\n *\n * `null` always means \"remove from the registry\"; a definition replaces.\n *\n * @see ../../../../../../.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md\n */\nimport type { AiAgentDefinition, AiAgentExtension } from './ai-agent-definition'\nimport type { AiToolDefinition } from './types'\n\n/** Override for a single agent: replace with a definition or remove with `null`. */\nexport type AiAgentOverride = AiAgentDefinition | null\n\n/** Override for a single tool: replace with a definition or remove with `null`. */\nexport type AiToolOverride = AiToolDefinition | null\n\n/** Map of agent id \u2192 override. Used in the per-module `ai-agents.ts` file. */\nexport type AiAgentOverridesMap = Record<string, AiAgentOverride>\n\n/** Map of tool name \u2192 override. Used in the per-module `ai-tools.ts` file. */\nexport type AiToolOverridesMap = Record<string, AiToolOverride>\n\n/**\n * Per-entry shape produced by the agent generator. Mirrors the per-module\n * record format used elsewhere in the registry generators so the file\n * stays grep-friendly.\n */\nexport interface AiAgentOverrideConfigEntry {\n moduleId: string\n overrides: AiAgentOverridesMap\n}\n\nexport interface AiAgentExtensionConfigEntry {\n moduleId: string\n extensions: AiAgentExtension[]\n}\n\n/**\n * Per-entry shape produced by the tool generator. Same record format as\n * {@link AiAgentOverrideConfigEntry}, but with tool definitions.\n */\nexport interface AiToolOverrideConfigEntry {\n moduleId: string\n overrides: AiToolOverridesMap\n}\n\n/** Shape of the `entry.overrides.ai` sub-tree on a `modules.ts` entry. */\nexport interface AiModuleOverridesShape {\n agents?: AiAgentOverridesMap\n tools?: AiToolOverridesMap\n extensions?: AiAgentExtension[]\n}\n\n/** Shape of a `modules.ts` `ModuleEntry` with the umbrella `overrides.ai`. */\nexport interface EnabledModuleAiOverrides {\n id: string\n overrides?: { ai?: AiModuleOverridesShape }\n}\n\nconst programmaticAgentOverrides: AiAgentOverridesMap = {}\nconst programmaticToolOverrides: AiToolOverridesMap = {}\nconst programmaticAgentExtensions: AiAgentExtension[] = []\n\nconst modulesConfigAgentOverrides: AiAgentOverridesMap = {}\nconst modulesConfigToolOverrides: AiToolOverridesMap = {}\nconst modulesConfigAgentExtensions: AiAgentExtension[] = []\n\n/**\n * Apply programmatic agent overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same id.\n *\n * @example\n * ```ts\n * applyAiAgentOverrides({\n * 'catalog.catalog_assistant': null, // disable\n * 'catalog.merchandising_assistant': customAgent, // replace\n * })\n * ```\n */\nexport function applyAiAgentOverrides(overrides: AiAgentOverridesMap): void {\n for (const [id, value] of Object.entries(overrides)) {\n programmaticAgentOverrides[id] = value\n }\n}\n\n/**\n * Apply programmatic tool overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same name.\n */\nexport function applyAiToolOverrides(overrides: AiToolOverridesMap): void {\n for (const [name, value] of Object.entries(overrides)) {\n programmaticToolOverrides[name] = value\n }\n}\n\n/**\n * Apply programmatic additive agent extensions. Extensions append to an\n * already-registered agent after replacements/disable overrides resolve.\n */\nexport function applyAiAgentExtensions(extensions: readonly AiAgentExtension[]): void {\n programmaticAgentExtensions.push(...extensions)\n}\n\n/**\n * Walk a list of `enabledModules` entries (the `apps/<app>/src/modules.ts`\n * shape) and register their `overrides.ai.agents` / `overrides.ai.tools`\n * into the `modules.ts` tier. Tier precedence (highest first):\n *\n * 1. {@link applyAiAgentOverrides} / {@link applyAiToolOverrides}\n * 2. `modules.ts` inline (this function)\n * 3. `<module>/ai-agents.ts` `aiAgentOverrides` / `<module>/ai-tools.ts`\n * `aiToolOverrides`\n * 4. base `aiAgents` / `aiTools`\n *\n * Calling this multiple times is additive: later calls overlay on the\n * existing tier (last wins per id). Use only at boot time \u2014 re-entering\n * mid-request blurs the resolution order.\n *\n * In practice this is invoked from `applyModuleOverridesFromEnabledModules`\n * (the umbrella dispatcher in `@open-mercato/shared/modules/overrides`)\n * via the registered `'ai'` applier; downstream apps call the dispatcher\n * once from `bootstrap.ts`. The standalone signature is kept for tests\n * and ad-hoc use.\n */\nexport function applyAiOverridesFromEnabledModules(\n modules: ReadonlyArray<EnabledModuleAiOverrides>,\n): void {\n for (const entry of modules) {\n if (!entry || typeof entry.id !== 'string' || !entry.id) continue\n const ai = entry.overrides?.ai\n if (!ai || typeof ai !== 'object') continue\n if (ai.agents && typeof ai.agents === 'object') {\n for (const [id, value] of Object.entries(ai.agents)) {\n modulesConfigAgentOverrides[id] = value as AiAgentOverride\n }\n }\n if (ai.tools && typeof ai.tools === 'object') {\n for (const [name, value] of Object.entries(ai.tools)) {\n modulesConfigToolOverrides[name] = value as AiToolOverride\n }\n }\n if (Array.isArray(ai.extensions)) {\n modulesConfigAgentExtensions.push(...ai.extensions)\n }\n }\n}\n\n/**\n * Bucketed entry shape passed to the umbrella dispatcher's per-domain\n * applier. Each entry carries one module's `overrides.ai` sub-tree.\n */\ntype AiOverrideEntryFromDispatcher = {\n moduleId: string\n overrides: AiModuleOverridesShape\n}\n\n/**\n * Applier registered with `@open-mercato/shared/modules/overrides` for\n * the `'ai'` domain. The dispatcher hands us per-module entries already\n * scoped to `overrides.ai`; we re-shape into `EnabledModuleAiOverrides`\n * and reuse {@link applyAiOverridesFromEnabledModules} so the AI tier\n * has exactly one code path.\n */\nexport function applyAiOverridesDispatcherEntries(\n entries: ReadonlyArray<AiOverrideEntryFromDispatcher>,\n): void {\n applyAiOverridesFromEnabledModules(\n entries.map((entry) => ({\n id: entry.moduleId,\n overrides: { ai: entry.overrides },\n })),\n )\n}\n\n// Side-effect: register the `'ai'` applier on first module load so the\n// umbrella dispatcher in `@open-mercato/shared/modules/overrides` can\n// route `entry.overrides.ai` here. Any consumer that imports\n// `@open-mercato/ai-assistant` (which apps do via `bootstrap.ts`) gets\n// the registration for free \u2014 no second import needed.\nregisterModuleOverrideApplier<AiModuleOverridesShape>(\n 'ai',\n (entries: ReadonlyArray<ModuleOverrideEntry<AiModuleOverridesShape>>) => {\n applyAiOverridesDispatcherEntries(entries)\n },\n)\n\n/** @__internal Test-only hook \u2014 reset programmatic + modules.ts override state. */\nexport function resetProgrammaticOverridesForTests(): void {\n for (const key of Object.keys(programmaticAgentOverrides)) {\n delete programmaticAgentOverrides[key]\n }\n for (const key of Object.keys(programmaticToolOverrides)) {\n delete programmaticToolOverrides[key]\n }\n for (const key of Object.keys(modulesConfigAgentOverrides)) {\n delete modulesConfigAgentOverrides[key]\n }\n for (const key of Object.keys(modulesConfigToolOverrides)) {\n delete modulesConfigToolOverrides[key]\n }\n programmaticAgentExtensions.length = 0\n modulesConfigAgentExtensions.length = 0\n}\n\n/**\n * Resolve the final agent override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n *\n * Resolution order (lowest precedence \u2192 highest):\n * 1. file entries in module load order\n * 2. modules.ts entries\n * 3. programmatic\n */\nexport function composeAgentOverrideMap(\n fileEntries: readonly AiAgentOverrideConfigEntry[],\n): AiAgentOverridesMap {\n const out: AiAgentOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [id, value] of Object.entries(overrides)) {\n if (typeof id !== 'string' || !id) continue\n out[id] = value as AiAgentOverride\n }\n }\n for (const [id, value] of Object.entries(modulesConfigAgentOverrides)) {\n out[id] = value\n }\n for (const [id, value] of Object.entries(programmaticAgentOverrides)) {\n out[id] = value\n }\n return out\n}\n\n/**\n * Resolve the final tool override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n */\nexport function composeToolOverrideMap(\n fileEntries: readonly AiToolOverrideConfigEntry[],\n): AiToolOverridesMap {\n const out: AiToolOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [name, value] of Object.entries(overrides)) {\n if (typeof name !== 'string' || !name) continue\n out[name] = value as AiToolOverride\n }\n }\n for (const [name, value] of Object.entries(modulesConfigToolOverrides)) {\n out[name] = value\n }\n for (const [name, value] of Object.entries(programmaticToolOverrides)) {\n out[name] = value\n }\n return out\n}\n\nexport function composeAgentExtensionEntries(\n fileEntries: readonly AiAgentExtensionConfigEntry[],\n): AiAgentExtension[] {\n const out: AiAgentExtension[] = []\n for (const entry of fileEntries) {\n if (Array.isArray(entry?.extensions)) out.push(...entry.extensions)\n }\n out.push(...modulesConfigAgentExtensions)\n out.push(...programmaticAgentExtensions)\n return out\n}\n\n/**\n * Apply an agent override map to a base list. Returns a new array.\n * `null` removes the entry; a non-null override replaces it. Override\n * entries naming an id that is not in `base` log a structured warning so\n * an operator can spot stale override files.\n */\nexport function applyAgentOverrideMap(\n base: readonly AiAgentDefinition[],\n overrides: AiAgentOverridesMap,\n): AiAgentDefinition[] {\n if (!overrides || Object.keys(overrides).length === 0) return base.slice()\n const byId = new Map<string, AiAgentDefinition>()\n for (const agent of base) {\n if (agent && typeof agent.id === 'string' && agent.id) {\n byId.set(agent.id, agent)\n }\n }\n for (const [id, value] of Object.entries(overrides)) {\n if (!byId.has(id) && value !== null) {\n // Allow registering a brand-new agent through the override surface\n // \u2014 useful for app-level \"synthetic\" agents without authoring a\n // module file. Warn at the structured logger so the operator\n // notices a stale id slipping through.\n console.warn(\n `[AI Overrides] Override registers a new agent \"${id}\" \u2014 no base entry to replace.`,\n )\n }\n if (value === null) {\n byId.delete(id)\n continue\n }\n if (!value || typeof value.id !== 'string' || value.id !== id) {\n console.warn(\n `[AI Overrides] Skipping malformed agent override for id \"${id}\" \u2014 id mismatch or missing fields.`,\n )\n continue\n }\n byId.set(id, value)\n }\n return Array.from(byId.values())\n}\n\n/**\n * Apply a tool override map to a base map. Returns a new Map.\n * `null` removes the entry; a non-null override replaces it.\n */\nexport function applyToolOverrideMap<TTool extends { name: string }>(\n base: ReadonlyMap<string, TTool>,\n overrides: Record<string, TTool | null | undefined>,\n): Map<string, TTool> {\n const out = new Map<string, TTool>(base)\n if (!overrides) return out\n for (const [name, value] of Object.entries(overrides)) {\n if (value === null) {\n out.delete(name)\n continue\n }\n if (value === undefined) continue\n if (!value || typeof (value as TTool).name !== 'string' || (value as TTool).name !== name) {\n console.warn(\n `[AI Overrides] Skipping malformed tool override for name \"${name}\" \u2014 name mismatch or missing fields.`,\n )\n continue\n }\n out.set(name, value as TTool)\n }\n return out\n}\n\n/**\n * @__internal \u2014 read the snapshot of programmatic + modules.ts overrides.\n * Used by tests and by the diagnostic helper that reports which overrides\n * are in effect.\n */\nexport function snapshotProgrammaticOverrides(): {\n agents: Readonly<AiAgentOverridesMap>\n tools: Readonly<AiToolOverridesMap>\n modulesConfigAgents: Readonly<AiAgentOverridesMap>\n modulesConfigTools: Readonly<AiToolOverridesMap>\n agentExtensions: readonly AiAgentExtension[]\n modulesConfigAgentExtensions: readonly AiAgentExtension[]\n} {\n return {\n agents: { ...programmaticAgentOverrides },\n tools: { ...programmaticToolOverrides },\n modulesConfigAgents: { ...modulesConfigAgentOverrides },\n modulesConfigTools: { ...modulesConfigToolOverrides },\n agentExtensions: programmaticAgentExtensions.slice(),\n modulesConfigAgentExtensions: modulesConfigAgentExtensions.slice(),\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,EACE;AAAA,OAEK;
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n registerModuleOverrideApplier,\n type ModuleOverrideEntry,\n} from '@open-mercato/shared/modules/overrides'\n\n/**\n * Module-to-module override pipeline for AI agents and AI tools.\n *\n * Modules contribute overrides through two surfaces:\n *\n * 1. Per-module: declare additional `aiAgentOverrides` / `aiToolOverrides`\n * exports in the existing `<module>/ai-agents.ts` / `<module>/ai-tools.ts`\n * files (no separate `ai-overrides.ts` file). The generator picks the\n * exports up alongside `aiAgents` / `aiTools` and emits override entries\n * inside `apps/<app>/.mercato/generated/ai-agents.generated.ts`\n * (`aiAgentOverrideEntries`) and `ai-tools.generated.ts`\n * (`aiToolOverrideEntries`).\n *\n * 2. App-level: declare `aiAgentOverrides` / `aiToolOverrides` directly on a\n * `ModuleEntry` inside the app's `src/modules.ts`. {@link\n * applyAiOverridesFromEnabledModules} feeds these into the runtime; they\n * sit one tier higher than the per-module file-based entries but below\n * explicit programmatic calls.\n *\n * Tests and bootstrap code can also override imperatively via\n * {@link applyAiAgentOverrides} and {@link applyAiToolOverrides}, which\n * supersede every other tier and persist for the process lifetime.\n *\n * `null` always means \"remove from the registry\"; a definition replaces.\n *\n * @see ../../../../../../.ai/specs/implemented/2026-04-30-ai-overrides-and-module-disable.md\n */\nimport type { AiAgentDefinition, AiAgentExtension } from './ai-agent-definition'\nimport type { AiToolDefinition } from './types'\n\nconst logger = createLogger('ai_assistant')\n\n/** Override for a single agent: replace with a definition or remove with `null`. */\nexport type AiAgentOverride = AiAgentDefinition | null\n\n/** Override for a single tool: replace with a definition or remove with `null`. */\nexport type AiToolOverride = AiToolDefinition | null\n\n/** Map of agent id \u2192 override. Used in the per-module `ai-agents.ts` file. */\nexport type AiAgentOverridesMap = Record<string, AiAgentOverride>\n\n/** Map of tool name \u2192 override. Used in the per-module `ai-tools.ts` file. */\nexport type AiToolOverridesMap = Record<string, AiToolOverride>\n\n/**\n * Per-entry shape produced by the agent generator. Mirrors the per-module\n * record format used elsewhere in the registry generators so the file\n * stays grep-friendly.\n */\nexport interface AiAgentOverrideConfigEntry {\n moduleId: string\n overrides: AiAgentOverridesMap\n}\n\nexport interface AiAgentExtensionConfigEntry {\n moduleId: string\n extensions: AiAgentExtension[]\n}\n\n/**\n * Per-entry shape produced by the tool generator. Same record format as\n * {@link AiAgentOverrideConfigEntry}, but with tool definitions.\n */\nexport interface AiToolOverrideConfigEntry {\n moduleId: string\n overrides: AiToolOverridesMap\n}\n\n/** Shape of the `entry.overrides.ai` sub-tree on a `modules.ts` entry. */\nexport interface AiModuleOverridesShape {\n agents?: AiAgentOverridesMap\n tools?: AiToolOverridesMap\n extensions?: AiAgentExtension[]\n}\n\n/** Shape of a `modules.ts` `ModuleEntry` with the umbrella `overrides.ai`. */\nexport interface EnabledModuleAiOverrides {\n id: string\n overrides?: { ai?: AiModuleOverridesShape }\n}\n\nconst programmaticAgentOverrides: AiAgentOverridesMap = {}\nconst programmaticToolOverrides: AiToolOverridesMap = {}\nconst programmaticAgentExtensions: AiAgentExtension[] = []\n\nconst modulesConfigAgentOverrides: AiAgentOverridesMap = {}\nconst modulesConfigToolOverrides: AiToolOverridesMap = {}\nconst modulesConfigAgentExtensions: AiAgentExtension[] = []\n\n/**\n * Apply programmatic agent overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same id.\n *\n * @example\n * ```ts\n * applyAiAgentOverrides({\n * 'catalog.catalog_assistant': null, // disable\n * 'catalog.merchandising_assistant': customAgent, // replace\n * })\n * ```\n */\nexport function applyAiAgentOverrides(overrides: AiAgentOverridesMap): void {\n for (const [id, value] of Object.entries(overrides)) {\n programmaticAgentOverrides[id] = value\n }\n}\n\n/**\n * Apply programmatic tool overrides \u2014 survive after the registries load\n * and take precedence over both file-based and `modules.ts`-tier overrides\n * for the same name.\n */\nexport function applyAiToolOverrides(overrides: AiToolOverridesMap): void {\n for (const [name, value] of Object.entries(overrides)) {\n programmaticToolOverrides[name] = value\n }\n}\n\n/**\n * Apply programmatic additive agent extensions. Extensions append to an\n * already-registered agent after replacements/disable overrides resolve.\n */\nexport function applyAiAgentExtensions(extensions: readonly AiAgentExtension[]): void {\n programmaticAgentExtensions.push(...extensions)\n}\n\n/**\n * Walk a list of `enabledModules` entries (the `apps/<app>/src/modules.ts`\n * shape) and register their `overrides.ai.agents` / `overrides.ai.tools`\n * into the `modules.ts` tier. Tier precedence (highest first):\n *\n * 1. {@link applyAiAgentOverrides} / {@link applyAiToolOverrides}\n * 2. `modules.ts` inline (this function)\n * 3. `<module>/ai-agents.ts` `aiAgentOverrides` / `<module>/ai-tools.ts`\n * `aiToolOverrides`\n * 4. base `aiAgents` / `aiTools`\n *\n * Calling this multiple times is additive: later calls overlay on the\n * existing tier (last wins per id). Use only at boot time \u2014 re-entering\n * mid-request blurs the resolution order.\n *\n * In practice this is invoked from `applyModuleOverridesFromEnabledModules`\n * (the umbrella dispatcher in `@open-mercato/shared/modules/overrides`)\n * via the registered `'ai'` applier; downstream apps call the dispatcher\n * once from `bootstrap.ts`. The standalone signature is kept for tests\n * and ad-hoc use.\n */\nexport function applyAiOverridesFromEnabledModules(\n modules: ReadonlyArray<EnabledModuleAiOverrides>,\n): void {\n for (const entry of modules) {\n if (!entry || typeof entry.id !== 'string' || !entry.id) continue\n const ai = entry.overrides?.ai\n if (!ai || typeof ai !== 'object') continue\n if (ai.agents && typeof ai.agents === 'object') {\n for (const [id, value] of Object.entries(ai.agents)) {\n modulesConfigAgentOverrides[id] = value as AiAgentOverride\n }\n }\n if (ai.tools && typeof ai.tools === 'object') {\n for (const [name, value] of Object.entries(ai.tools)) {\n modulesConfigToolOverrides[name] = value as AiToolOverride\n }\n }\n if (Array.isArray(ai.extensions)) {\n modulesConfigAgentExtensions.push(...ai.extensions)\n }\n }\n}\n\n/**\n * Bucketed entry shape passed to the umbrella dispatcher's per-domain\n * applier. Each entry carries one module's `overrides.ai` sub-tree.\n */\ntype AiOverrideEntryFromDispatcher = {\n moduleId: string\n overrides: AiModuleOverridesShape\n}\n\n/**\n * Applier registered with `@open-mercato/shared/modules/overrides` for\n * the `'ai'` domain. The dispatcher hands us per-module entries already\n * scoped to `overrides.ai`; we re-shape into `EnabledModuleAiOverrides`\n * and reuse {@link applyAiOverridesFromEnabledModules} so the AI tier\n * has exactly one code path.\n */\nexport function applyAiOverridesDispatcherEntries(\n entries: ReadonlyArray<AiOverrideEntryFromDispatcher>,\n): void {\n applyAiOverridesFromEnabledModules(\n entries.map((entry) => ({\n id: entry.moduleId,\n overrides: { ai: entry.overrides },\n })),\n )\n}\n\n// Side-effect: register the `'ai'` applier on first module load so the\n// umbrella dispatcher in `@open-mercato/shared/modules/overrides` can\n// route `entry.overrides.ai` here. Any consumer that imports\n// `@open-mercato/ai-assistant` (which apps do via `bootstrap.ts`) gets\n// the registration for free \u2014 no second import needed.\nregisterModuleOverrideApplier<AiModuleOverridesShape>(\n 'ai',\n (entries: ReadonlyArray<ModuleOverrideEntry<AiModuleOverridesShape>>) => {\n applyAiOverridesDispatcherEntries(entries)\n },\n)\n\n/** @__internal Test-only hook \u2014 reset programmatic + modules.ts override state. */\nexport function resetProgrammaticOverridesForTests(): void {\n for (const key of Object.keys(programmaticAgentOverrides)) {\n delete programmaticAgentOverrides[key]\n }\n for (const key of Object.keys(programmaticToolOverrides)) {\n delete programmaticToolOverrides[key]\n }\n for (const key of Object.keys(modulesConfigAgentOverrides)) {\n delete modulesConfigAgentOverrides[key]\n }\n for (const key of Object.keys(modulesConfigToolOverrides)) {\n delete modulesConfigToolOverrides[key]\n }\n programmaticAgentExtensions.length = 0\n modulesConfigAgentExtensions.length = 0\n}\n\n/**\n * Resolve the final agent override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n *\n * Resolution order (lowest precedence \u2192 highest):\n * 1. file entries in module load order\n * 2. modules.ts entries\n * 3. programmatic\n */\nexport function composeAgentOverrideMap(\n fileEntries: readonly AiAgentOverrideConfigEntry[],\n): AiAgentOverridesMap {\n const out: AiAgentOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [id, value] of Object.entries(overrides)) {\n if (typeof id !== 'string' || !id) continue\n out[id] = value as AiAgentOverride\n }\n }\n for (const [id, value] of Object.entries(modulesConfigAgentOverrides)) {\n out[id] = value\n }\n for (const [id, value] of Object.entries(programmaticAgentOverrides)) {\n out[id] = value\n }\n return out\n}\n\n/**\n * Resolve the final tool override map from a list of file-based entries\n * plus the `modules.ts` and programmatic state.\n */\nexport function composeToolOverrideMap(\n fileEntries: readonly AiToolOverrideConfigEntry[],\n): AiToolOverridesMap {\n const out: AiToolOverridesMap = {}\n for (const entry of fileEntries) {\n const overrides = entry?.overrides\n if (!overrides || typeof overrides !== 'object') continue\n for (const [name, value] of Object.entries(overrides)) {\n if (typeof name !== 'string' || !name) continue\n out[name] = value as AiToolOverride\n }\n }\n for (const [name, value] of Object.entries(modulesConfigToolOverrides)) {\n out[name] = value\n }\n for (const [name, value] of Object.entries(programmaticToolOverrides)) {\n out[name] = value\n }\n return out\n}\n\nexport function composeAgentExtensionEntries(\n fileEntries: readonly AiAgentExtensionConfigEntry[],\n): AiAgentExtension[] {\n const out: AiAgentExtension[] = []\n for (const entry of fileEntries) {\n if (Array.isArray(entry?.extensions)) out.push(...entry.extensions)\n }\n out.push(...modulesConfigAgentExtensions)\n out.push(...programmaticAgentExtensions)\n return out\n}\n\n/**\n * Apply an agent override map to a base list. Returns a new array.\n * `null` removes the entry; a non-null override replaces it. Override\n * entries naming an id that is not in `base` log a structured warning so\n * an operator can spot stale override files.\n */\nexport function applyAgentOverrideMap(\n base: readonly AiAgentDefinition[],\n overrides: AiAgentOverridesMap,\n): AiAgentDefinition[] {\n if (!overrides || Object.keys(overrides).length === 0) return base.slice()\n const byId = new Map<string, AiAgentDefinition>()\n for (const agent of base) {\n if (agent && typeof agent.id === 'string' && agent.id) {\n byId.set(agent.id, agent)\n }\n }\n for (const [id, value] of Object.entries(overrides)) {\n if (!byId.has(id) && value !== null) {\n // Allow registering a brand-new agent through the override surface\n // \u2014 useful for app-level \"synthetic\" agents without authoring a\n // module file. Warn at the structured logger so the operator\n // notices a stale id slipping through.\n logger.warn('Override registers a new agent \u2014 no base entry to replace', { agentId: id })\n }\n if (value === null) {\n byId.delete(id)\n continue\n }\n if (!value || typeof value.id !== 'string' || value.id !== id) {\n logger.warn('Skipping malformed agent override \u2014 id mismatch or missing fields', { agentId: id })\n continue\n }\n byId.set(id, value)\n }\n return Array.from(byId.values())\n}\n\n/**\n * Apply a tool override map to a base map. Returns a new Map.\n * `null` removes the entry; a non-null override replaces it.\n */\nexport function applyToolOverrideMap<TTool extends { name: string }>(\n base: ReadonlyMap<string, TTool>,\n overrides: Record<string, TTool | null | undefined>,\n): Map<string, TTool> {\n const out = new Map<string, TTool>(base)\n if (!overrides) return out\n for (const [name, value] of Object.entries(overrides)) {\n if (value === null) {\n out.delete(name)\n continue\n }\n if (value === undefined) continue\n if (!value || typeof (value as TTool).name !== 'string' || (value as TTool).name !== name) {\n logger.warn('Skipping malformed tool override \u2014 name mismatch or missing fields', { toolName: name })\n continue\n }\n out.set(name, value as TTool)\n }\n return out\n}\n\n/**\n * @__internal \u2014 read the snapshot of programmatic + modules.ts overrides.\n * Used by tests and by the diagnostic helper that reports which overrides\n * are in effect.\n */\nexport function snapshotProgrammaticOverrides(): {\n agents: Readonly<AiAgentOverridesMap>\n tools: Readonly<AiToolOverridesMap>\n modulesConfigAgents: Readonly<AiAgentOverridesMap>\n modulesConfigTools: Readonly<AiToolOverridesMap>\n agentExtensions: readonly AiAgentExtension[]\n modulesConfigAgentExtensions: readonly AiAgentExtension[]\n} {\n return {\n agents: { ...programmaticAgentOverrides },\n tools: { ...programmaticToolOverrides },\n modulesConfigAgents: { ...modulesConfigAgentOverrides },\n modulesConfigTools: { ...modulesConfigToolOverrides },\n agentExtensions: programmaticAgentExtensions.slice(),\n modulesConfigAgentExtensions: modulesConfigAgentExtensions.slice(),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { buildOpenApiDocument } from "@open-mercato/shared/lib/openapi";
|
|
2
3
|
import { fetchWithTimeout, resolveTimeoutMs } from "@open-mercato/shared/lib/http/fetchWithTimeout";
|
|
4
|
+
const logger = createLogger("ai_assistant").child({ component: "api-index" });
|
|
3
5
|
const DEFAULT_OPENAPI_FETCH_TIMEOUT_MS = 1e4;
|
|
4
6
|
function resolveOpenapiFetchTimeoutMs() {
|
|
5
7
|
const raw = process.env.AI_OPENAPI_FETCH_TIMEOUT_MS;
|
|
@@ -47,7 +49,7 @@ async function loadRichOpenApiSpec() {
|
|
|
47
49
|
if (!doc.paths || Object.keys(doc.paths).length === 0) {
|
|
48
50
|
return null;
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
logger.debug("Rich OpenAPI spec built from module registry", { modules: modulesWithApis.length, tier: 2 });
|
|
51
53
|
rawSpecCache = doc;
|
|
52
54
|
return doc;
|
|
53
55
|
}
|
|
@@ -80,12 +82,12 @@ async function loadRawOpenApiSpec() {
|
|
|
80
82
|
const jsonPath = path.join(appRoot.generatedDir, "openapi.generated.json");
|
|
81
83
|
if (fs.existsSync(jsonPath)) {
|
|
82
84
|
const doc = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
83
|
-
|
|
85
|
+
logger.debug("Raw OpenAPI spec loaded from generated JSON", { jsonPath });
|
|
84
86
|
return doc;
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
} catch (error) {
|
|
88
|
-
|
|
90
|
+
logger.warn("Raw spec from generated JSON failed", { err: error });
|
|
89
91
|
}
|
|
90
92
|
try {
|
|
91
93
|
const { getModules } = await import("@open-mercato/shared/lib/modules/registry");
|
|
@@ -97,7 +99,7 @@ async function loadRawOpenApiSpec() {
|
|
|
97
99
|
version: "1.0.0",
|
|
98
100
|
servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" }]
|
|
99
101
|
});
|
|
100
|
-
|
|
102
|
+
logger.debug("Raw OpenAPI spec built from module registry", { modules: modulesWithApis.length });
|
|
101
103
|
return doc;
|
|
102
104
|
}
|
|
103
105
|
} catch {
|
|
@@ -109,11 +111,11 @@ async function loadRawOpenApiSpec() {
|
|
|
109
111
|
});
|
|
110
112
|
if (response.ok) {
|
|
111
113
|
const doc = await response.json();
|
|
112
|
-
|
|
114
|
+
logger.debug("Raw OpenAPI spec fetched via HTTP");
|
|
113
115
|
return doc;
|
|
114
116
|
}
|
|
115
117
|
} catch (error) {
|
|
116
|
-
|
|
118
|
+
logger.warn("Raw spec HTTP fetch failed", { err: error });
|
|
117
119
|
}
|
|
118
120
|
return null;
|
|
119
121
|
}
|
|
@@ -138,19 +140,19 @@ async function parseApiEndpointsFromGeneratedJson() {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
if (!appRoot) {
|
|
141
|
-
|
|
143
|
+
logger.warn("Could not find app root");
|
|
142
144
|
return [];
|
|
143
145
|
}
|
|
144
146
|
const jsonPath = path.join(appRoot.generatedDir, "openapi.generated.json");
|
|
145
147
|
if (!fs.existsSync(jsonPath)) {
|
|
146
|
-
|
|
148
|
+
logger.warn("openapi.generated.json not found - run yarn generate");
|
|
147
149
|
return [];
|
|
148
150
|
}
|
|
149
151
|
const doc = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
150
|
-
|
|
152
|
+
logger.debug("Loaded OpenAPI from generated JSON", { jsonPath });
|
|
151
153
|
return extractEndpoints(doc);
|
|
152
154
|
} catch (error) {
|
|
153
|
-
|
|
155
|
+
logger.error("Error reading generated OpenAPI JSON", { err: error });
|
|
154
156
|
return [];
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -160,9 +162,7 @@ async function parseApiEndpointsFromModules() {
|
|
|
160
162
|
const modules = getModules();
|
|
161
163
|
const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0);
|
|
162
164
|
if (modulesWithApis.length > 0) {
|
|
163
|
-
|
|
164
|
-
`[API Index] Found ${modules.length} modules, ${modulesWithApis.length} with APIs`
|
|
165
|
-
);
|
|
165
|
+
logger.debug("Modules discovered for OpenAPI generation", { modules: modules.length, withApis: modulesWithApis.length });
|
|
166
166
|
const doc = buildOpenApiDocument(modules, {
|
|
167
167
|
title: "Open Mercato API",
|
|
168
168
|
version: "1.0.0",
|
|
@@ -181,35 +181,35 @@ async function parseApiEndpointsFromHttp() {
|
|
|
181
181
|
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || "http://localhost:3000";
|
|
182
182
|
const openApiUrl = `${baseUrl}/api/docs/openapi`;
|
|
183
183
|
try {
|
|
184
|
-
|
|
184
|
+
logger.debug("Fetching OpenAPI spec", { openApiUrl });
|
|
185
185
|
const response = await fetchWithTimeout(openApiUrl, {
|
|
186
186
|
timeoutMs: resolveOpenapiFetchTimeoutMs()
|
|
187
187
|
});
|
|
188
188
|
if (!response.ok) {
|
|
189
|
-
|
|
189
|
+
logger.warn("Failed to fetch OpenAPI spec", { status: response.status, statusText: response.statusText });
|
|
190
190
|
return [];
|
|
191
191
|
}
|
|
192
192
|
const doc = await response.json();
|
|
193
|
-
|
|
193
|
+
logger.debug("Successfully fetched OpenAPI spec");
|
|
194
194
|
return extractEndpoints(doc);
|
|
195
195
|
} catch (error) {
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
logger.warn("Could not fetch OpenAPI spec", { err: error });
|
|
197
|
+
logger.warn("OpenAPI fetch fallback requires the app to be running", { baseUrl });
|
|
198
198
|
return [];
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
async function parseApiEndpoints() {
|
|
202
202
|
const fromJson = await parseApiEndpointsFromGeneratedJson();
|
|
203
203
|
if (fromJson.length > 0) {
|
|
204
|
-
|
|
204
|
+
logger.debug("Loaded endpoints from generated JSON", { count: fromJson.length });
|
|
205
205
|
return fromJson;
|
|
206
206
|
}
|
|
207
207
|
const fromModules = await parseApiEndpointsFromModules();
|
|
208
208
|
if (fromModules.length > 0) {
|
|
209
|
-
|
|
209
|
+
logger.debug("Loaded endpoints from modules registry", { count: fromModules.length });
|
|
210
210
|
return fromModules;
|
|
211
211
|
}
|
|
212
|
-
|
|
212
|
+
logger.debug("Generated JSON and modules not available, falling back to HTTP fetch");
|
|
213
213
|
return parseApiEndpointsFromHttp();
|
|
214
214
|
}
|
|
215
215
|
function extractEndpoints(doc) {
|
|
@@ -241,7 +241,7 @@ function extractEndpoints(doc) {
|
|
|
241
241
|
endpoints.push(endpoint);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
logger.debug("Parsed endpoints from OpenAPI spec", { count: endpoints.length });
|
|
245
245
|
return endpoints;
|
|
246
246
|
}
|
|
247
247
|
function generateOperationId(path, method) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/api-endpoint-index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * API Endpoint Index\n *\n * Parses the OpenAPI spec into a cached, in-memory list of endpoints and\n * exposes the raw OpenAPI document to Code Mode's `search` tool. The Code\n * Mode rewrite (2026-02-22) made this the only consumer \u2014 the legacy\n * `find_api` / `call_api` / `discover_schema` tools and their search-index\n * fan-out have been removed.\n */\n\nimport type { OpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport { buildOpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport type { Module } from '@open-mercato/shared/modules/registry'\nimport { fetchWithTimeout, resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\n\nconst DEFAULT_OPENAPI_FETCH_TIMEOUT_MS = 10_000\n\nfunction resolveOpenapiFetchTimeoutMs(): number {\n const raw = process.env.AI_OPENAPI_FETCH_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_OPENAPI_FETCH_TIMEOUT_MS)\n}\n\n/**\n * Indexed API endpoint structure\n */\nexport interface ApiEndpoint {\n id: string\n operationId: string\n method: string\n path: string\n summary: string\n description: string\n tags: string[]\n requiredFeatures: string[]\n parameters: ApiParameter[]\n requestBodySchema: Record<string, unknown> | null\n deprecated: boolean\n}\n\nexport interface ApiParameter {\n name: string\n in: 'path' | 'query' | 'header'\n required: boolean\n type: string\n description: string\n}\n\n/**\n * In-memory cache of parsed endpoints (avoid re-parsing on each request)\n */\nlet endpointsCache: ApiEndpoint[] | null = null\nlet endpointsByOperationId: Map<string, ApiEndpoint> | null = null\n\n/**\n * In-memory cache of the raw OpenAPI spec document (for Code Mode search tool)\n */\nlet rawSpecCache: OpenApiDocument | null = null\n\n/**\n * Get all parsed API endpoints (cached)\n */\nexport async function getApiEndpoints(): Promise<ApiEndpoint[]> {\n if (endpointsCache) {\n return endpointsCache\n }\n\n endpointsCache = await parseApiEndpoints()\n endpointsByOperationId = new Map(endpointsCache.map((e) => [e.operationId, e]))\n\n return endpointsCache\n}\n\n/**\n * Get endpoint by operationId\n */\nexport async function getEndpointByOperationId(operationId: string): Promise<ApiEndpoint | null> {\n await getApiEndpoints() // Ensure cache is populated\n return endpointsByOperationId?.get(operationId) ?? null\n}\n\n/**\n * Get the raw OpenAPI spec document (cached).\n * Uses the same 3-tier loading strategy as parseApiEndpoints():\n * generated JSON \u2192 module registry \u2192 HTTP fetch.\n */\nexport async function getRawOpenApiSpec(): Promise<OpenApiDocument | null> {\n if (rawSpecCache) return rawSpecCache\n rawSpecCache = await loadRawOpenApiSpec()\n return rawSpecCache\n}\n\n/**\n * Set the raw OpenAPI spec cache directly.\n * Used by servers that want to inject a pre-built spec.\n */\nexport function setRawSpecCache(doc: OpenApiDocument): void {\n rawSpecCache = doc\n}\n\n/**\n * Clear the raw OpenAPI spec cache.\n */\nexport function clearRawSpecCache(): void {\n rawSpecCache = null\n}\n\n/**\n * Load the rich OpenAPI spec, skipping Tier 1 (static JSON) which lacks requestBody schemas.\n * Prefers Tier 2 (runtime module registry) which has full Zod-converted schemas.\n * Falls back to Tier 1 then Tier 3 if needed.\n */\nexport async function loadRichOpenApiSpec(): Promise<OpenApiDocument | null> {\n if (rawSpecCache) return rawSpecCache\n\n // Tier 2 first: Module registry (has full Zod-converted schemas)\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n if (!doc.paths || Object.keys(doc.paths).length === 0) {\n return null\n }\n console.error(`[API Index] Rich OpenAPI spec built from ${modulesWithApis.length} modules (Tier 2)`)\n rawSpecCache = doc\n return doc\n }\n } catch {\n // Registry not available \u2014 fall through\n }\n\n // Fall back to standard 3-tier loading (Tier 1 \u2192 Tier 3)\n rawSpecCache = await loadRawOpenApiSpec()\n return rawSpecCache\n}\n\n/**\n * Load raw OpenAPI spec using the 3-tier strategy.\n */\nasync function loadRawOpenApiSpec(): Promise<OpenApiDocument | null> {\n // Tier 1: Generated JSON file\n try {\n const fs = await import('node:fs')\n const path = await import('node:path')\n const { findAppRoot, findAllApps } = await import('@open-mercato/shared/lib/bootstrap/appResolver')\n\n let appRoot = findAppRoot()\n if (!appRoot) {\n let current = process.cwd()\n while (current !== path.dirname(current)) {\n const appsDir = path.join(current, 'apps')\n if (fs.existsSync(appsDir)) {\n const apps = findAllApps(current)\n if (apps.length > 0) {\n appRoot = apps[0]\n break\n }\n }\n current = path.dirname(current)\n }\n }\n\n if (appRoot) {\n const jsonPath = path.join(appRoot.generatedDir, 'openapi.generated.json')\n if (fs.existsSync(jsonPath)) {\n const doc = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as OpenApiDocument\n console.error(`[API Index] Raw OpenAPI spec loaded from ${jsonPath}`)\n return doc\n }\n }\n } catch (error) {\n console.error('[API Index] Raw spec from JSON failed:', error instanceof Error ? error.message : error)\n }\n\n // Tier 2: Module registry\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n console.error(`[API Index] Raw OpenAPI spec built from ${modulesWithApis.length} modules`)\n return doc\n }\n } catch {\n // Registry not available\n }\n\n // Tier 3: HTTP fetch\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n try {\n const response = await fetchWithTimeout(`${baseUrl}/api/docs/openapi`, {\n timeoutMs: resolveOpenapiFetchTimeoutMs(),\n })\n if (response.ok) {\n const doc = (await response.json()) as OpenApiDocument\n console.error('[API Index] Raw OpenAPI spec fetched via HTTP')\n return doc\n }\n } catch (error) {\n console.error('[API Index] Raw spec HTTP fetch failed:', error instanceof Error ? error.message : error)\n }\n\n return null\n}\n\n/**\n * Parse endpoints from generated OpenAPI JSON file (for CLI context).\n * This is generated by `yarn generate`.\n */\nasync function parseApiEndpointsFromGeneratedJson(): Promise<ApiEndpoint[]> {\n try {\n const fs = await import('node:fs')\n const path = await import('node:path')\n const { findAppRoot, findAllApps } = await import('@open-mercato/shared/lib/bootstrap/appResolver')\n\n let appRoot = findAppRoot()\n\n // Try monorepo structure if not found - walk up to find monorepo root\n if (!appRoot) {\n let current = process.cwd()\n // Walk up until we find a directory containing 'apps' folder\n while (current !== path.dirname(current)) {\n const appsDir = path.join(current, 'apps')\n if (fs.existsSync(appsDir)) {\n const apps = findAllApps(current)\n if (apps.length > 0) {\n appRoot = apps[0]\n break\n }\n }\n current = path.dirname(current)\n }\n }\n\n if (!appRoot) {\n console.error('[API Index] Could not find app root')\n return []\n }\n\n const jsonPath = path.join(appRoot.generatedDir, 'openapi.generated.json')\n if (!fs.existsSync(jsonPath)) {\n console.error('[API Index] openapi.generated.json not found - run yarn generate')\n return []\n }\n\n const doc = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as OpenApiDocument\n console.error(`[API Index] Loaded OpenAPI from ${jsonPath}`)\n return extractEndpoints(doc)\n } catch (error) {\n console.error('[API Index] Error reading generated JSON:', error instanceof Error ? error.message : error)\n return []\n }\n}\n\n/**\n * Parse endpoints from registered modules (works in Next.js context).\n */\nasync function parseApiEndpointsFromModules(): Promise<ApiEndpoint[]> {\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n\n // Count how many modules have APIs defined\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n console.error(\n `[API Index] Found ${modules.length} modules, ${modulesWithApis.length} with APIs`\n )\n\n // Generate OpenAPI spec from modules\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n if (!doc.paths || Object.keys(doc.paths).length === 0) {\n return []\n }\n\n return extractEndpoints(doc)\n }\n } catch {\n // Registry not available\n }\n\n return []\n}\n\n/**\n * Parse OpenAPI spec via HTTP fetch.\n * Fetches the OpenAPI spec from the running app's /api/docs/openapi endpoint.\n */\nasync function parseApiEndpointsFromHttp(): Promise<ApiEndpoint[]> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n const openApiUrl = `${baseUrl}/api/docs/openapi`\n\n try {\n console.error(`[API Index] Fetching OpenAPI spec from ${openApiUrl}...`)\n const response = await fetchWithTimeout(openApiUrl, {\n timeoutMs: resolveOpenapiFetchTimeoutMs(),\n })\n\n if (!response.ok) {\n console.error(`[API Index] Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`)\n return []\n }\n\n const doc = (await response.json()) as OpenApiDocument\n console.error(`[API Index] Successfully fetched OpenAPI spec`)\n return extractEndpoints(doc)\n } catch (error) {\n console.error('[API Index] Could not fetch OpenAPI spec:', error instanceof Error ? error.message : error)\n console.error('[API Index] Make sure the app is running at', baseUrl)\n return []\n }\n}\n\n/**\n * Parse API endpoints - tries generated JSON first (CLI), then modules (Next.js), then HTTP.\n */\nasync function parseApiEndpoints(): Promise<ApiEndpoint[]> {\n // Try generated JSON first (works in CLI context without Next.js)\n const fromJson = await parseApiEndpointsFromGeneratedJson()\n if (fromJson.length > 0) {\n console.error(`[API Index] Loaded ${fromJson.length} endpoints from generated JSON`)\n return fromJson\n }\n\n // Try loading from module registry (works in Next.js context)\n const fromModules = await parseApiEndpointsFromModules()\n if (fromModules.length > 0) {\n console.error(`[API Index] Loaded ${fromModules.length} endpoints from modules registry`)\n return fromModules\n }\n\n // Fall back to HTTP fetch (requires running Next.js app)\n console.error('[API Index] Generated JSON and modules not available, falling back to HTTP fetch...')\n return parseApiEndpointsFromHttp()\n}\n\n/**\n * Extract endpoints from OpenAPI document\n */\nfunction extractEndpoints(doc: OpenApiDocument): ApiEndpoint[] {\n const endpoints: ApiEndpoint[] = []\n const validMethods = ['get', 'post', 'put', 'patch', 'delete']\n\n if (!doc.paths) {\n return endpoints\n }\n\n for (const [path, pathItem] of Object.entries(doc.paths)) {\n if (!pathItem || typeof pathItem !== 'object') continue\n\n for (const [method, operation] of Object.entries(pathItem)) {\n if (!validMethods.includes(method.toLowerCase())) continue\n if (!operation || typeof operation !== 'object') continue\n\n const op = operation as any\n\n // Generate operationId if not present\n const operationId = op.operationId || generateOperationId(path, method)\n\n const endpoint: ApiEndpoint = {\n id: operationId,\n operationId,\n method: method.toUpperCase(),\n path,\n summary: op.summary || '',\n description: op.description || op.summary || `${method.toUpperCase()} ${path}`,\n tags: op.tags || [],\n requiredFeatures: op['x-require-features'] || [],\n deprecated: op.deprecated || false,\n parameters: extractParameters(op.parameters || []),\n requestBodySchema: extractRequestBodySchema(op.requestBody, doc.components?.schemas),\n }\n\n endpoints.push(endpoint)\n }\n }\n\n console.error(`[API Index] Parsed ${endpoints.length} endpoints from OpenAPI spec`)\n return endpoints\n}\n\n/**\n * Generate operationId from path and method\n */\nfunction generateOperationId(path: string, method: string): string {\n const pathParts = path\n .replace(/^\\//, '')\n .replace(/\\{([^}]+)\\}/g, 'by_$1')\n .split('/')\n .filter(Boolean)\n .join('_')\n\n return `${method.toLowerCase()}_${pathParts}`\n}\n\n/**\n * Extract parameter info\n */\nfunction extractParameters(params: any[]): ApiParameter[] {\n return params\n .filter((p) => p.in === 'path' || p.in === 'query')\n .map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required ?? false,\n type: p.schema?.type || 'string',\n description: p.description || '',\n }))\n}\n\n/**\n * Extract request body schema (simplified)\n */\nfunction extractRequestBodySchema(\n requestBody: any,\n schemas?: Record<string, any>\n): Record<string, unknown> | null {\n if (!requestBody?.content?.['application/json']?.schema) {\n return null\n }\n\n const schema = requestBody.content['application/json'].schema\n\n // Resolve $ref if present\n if (schema.$ref && schemas) {\n const refPath = schema.$ref.replace('#/components/schemas/', '')\n return schemas[refPath] || schema\n }\n\n return schema\n}\n\n/**\n * Clear endpoint cache (for testing)\n */\nexport function clearEndpointCache(): void {\n endpointsCache = null\n endpointsByOperationId = null\n rawSpecCache = null\n}\n\n/**\n * Extract simplified request body schema for LLM consumption.\n * Returns required fields and basic property info without deep nesting.\n */\nexport function simplifyRequestBodySchema(\n schema: Record<string, unknown> | null\n): { required: string[]; properties: Record<string, { type: string; format?: string; enum?: string[] }> } | null {\n if (!schema) return null\n\n const properties: Record<string, { type: string; format?: string; enum?: string[] }> = {}\n const required: string[] = (schema.required as string[]) || []\n\n const schemaProps = (schema.properties || schema) as Record<string, unknown>\n\n for (const [key, value] of Object.entries(schemaProps)) {\n if (typeof value !== 'object' || value === null) continue\n const propSchema = value as Record<string, unknown>\n\n const prop: { type: string; format?: string; enum?: string[] } = {\n type: (propSchema.type as string) || 'unknown',\n }\n\n if (propSchema.format) prop.format = propSchema.format as string\n if (propSchema.enum && Array.isArray(propSchema.enum)) {\n prop.enum = propSchema.enum.slice(0, 10) as string[] // Limit enum values\n }\n\n properties[key] = prop\n }\n\n return { required, properties }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * API Endpoint Index\n *\n * Parses the OpenAPI spec into a cached, in-memory list of endpoints and\n * exposes the raw OpenAPI document to Code Mode's `search` tool. The Code\n * Mode rewrite (2026-02-22) made this the only consumer \u2014 the legacy\n * `find_api` / `call_api` / `discover_schema` tools and their search-index\n * fan-out have been removed.\n */\n\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { OpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport { buildOpenApiDocument } from '@open-mercato/shared/lib/openapi'\nimport type { Module } from '@open-mercato/shared/modules/registry'\nimport { fetchWithTimeout, resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\n\nconst logger = createLogger('ai_assistant').child({ component: 'api-index' })\n\nconst DEFAULT_OPENAPI_FETCH_TIMEOUT_MS = 10_000\n\nfunction resolveOpenapiFetchTimeoutMs(): number {\n const raw = process.env.AI_OPENAPI_FETCH_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_OPENAPI_FETCH_TIMEOUT_MS)\n}\n\n/**\n * Indexed API endpoint structure\n */\nexport interface ApiEndpoint {\n id: string\n operationId: string\n method: string\n path: string\n summary: string\n description: string\n tags: string[]\n requiredFeatures: string[]\n parameters: ApiParameter[]\n requestBodySchema: Record<string, unknown> | null\n deprecated: boolean\n}\n\nexport interface ApiParameter {\n name: string\n in: 'path' | 'query' | 'header'\n required: boolean\n type: string\n description: string\n}\n\n/**\n * In-memory cache of parsed endpoints (avoid re-parsing on each request)\n */\nlet endpointsCache: ApiEndpoint[] | null = null\nlet endpointsByOperationId: Map<string, ApiEndpoint> | null = null\n\n/**\n * In-memory cache of the raw OpenAPI spec document (for Code Mode search tool)\n */\nlet rawSpecCache: OpenApiDocument | null = null\n\n/**\n * Get all parsed API endpoints (cached)\n */\nexport async function getApiEndpoints(): Promise<ApiEndpoint[]> {\n if (endpointsCache) {\n return endpointsCache\n }\n\n endpointsCache = await parseApiEndpoints()\n endpointsByOperationId = new Map(endpointsCache.map((e) => [e.operationId, e]))\n\n return endpointsCache\n}\n\n/**\n * Get endpoint by operationId\n */\nexport async function getEndpointByOperationId(operationId: string): Promise<ApiEndpoint | null> {\n await getApiEndpoints() // Ensure cache is populated\n return endpointsByOperationId?.get(operationId) ?? null\n}\n\n/**\n * Get the raw OpenAPI spec document (cached).\n * Uses the same 3-tier loading strategy as parseApiEndpoints():\n * generated JSON \u2192 module registry \u2192 HTTP fetch.\n */\nexport async function getRawOpenApiSpec(): Promise<OpenApiDocument | null> {\n if (rawSpecCache) return rawSpecCache\n rawSpecCache = await loadRawOpenApiSpec()\n return rawSpecCache\n}\n\n/**\n * Set the raw OpenAPI spec cache directly.\n * Used by servers that want to inject a pre-built spec.\n */\nexport function setRawSpecCache(doc: OpenApiDocument): void {\n rawSpecCache = doc\n}\n\n/**\n * Clear the raw OpenAPI spec cache.\n */\nexport function clearRawSpecCache(): void {\n rawSpecCache = null\n}\n\n/**\n * Load the rich OpenAPI spec, skipping Tier 1 (static JSON) which lacks requestBody schemas.\n * Prefers Tier 2 (runtime module registry) which has full Zod-converted schemas.\n * Falls back to Tier 1 then Tier 3 if needed.\n */\nexport async function loadRichOpenApiSpec(): Promise<OpenApiDocument | null> {\n if (rawSpecCache) return rawSpecCache\n\n // Tier 2 first: Module registry (has full Zod-converted schemas)\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n if (!doc.paths || Object.keys(doc.paths).length === 0) {\n return null\n }\n logger.debug('Rich OpenAPI spec built from module registry', { modules: modulesWithApis.length, tier: 2 })\n rawSpecCache = doc\n return doc\n }\n } catch {\n // Registry not available \u2014 fall through\n }\n\n // Fall back to standard 3-tier loading (Tier 1 \u2192 Tier 3)\n rawSpecCache = await loadRawOpenApiSpec()\n return rawSpecCache\n}\n\n/**\n * Load raw OpenAPI spec using the 3-tier strategy.\n */\nasync function loadRawOpenApiSpec(): Promise<OpenApiDocument | null> {\n // Tier 1: Generated JSON file\n try {\n const fs = await import('node:fs')\n const path = await import('node:path')\n const { findAppRoot, findAllApps } = await import('@open-mercato/shared/lib/bootstrap/appResolver')\n\n let appRoot = findAppRoot()\n if (!appRoot) {\n let current = process.cwd()\n while (current !== path.dirname(current)) {\n const appsDir = path.join(current, 'apps')\n if (fs.existsSync(appsDir)) {\n const apps = findAllApps(current)\n if (apps.length > 0) {\n appRoot = apps[0]\n break\n }\n }\n current = path.dirname(current)\n }\n }\n\n if (appRoot) {\n const jsonPath = path.join(appRoot.generatedDir, 'openapi.generated.json')\n if (fs.existsSync(jsonPath)) {\n const doc = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as OpenApiDocument\n logger.debug('Raw OpenAPI spec loaded from generated JSON', { jsonPath })\n return doc\n }\n }\n } catch (error) {\n logger.warn('Raw spec from generated JSON failed', { err: error })\n }\n\n // Tier 2: Module registry\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n logger.debug('Raw OpenAPI spec built from module registry', { modules: modulesWithApis.length })\n return doc\n }\n } catch {\n // Registry not available\n }\n\n // Tier 3: HTTP fetch\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n try {\n const response = await fetchWithTimeout(`${baseUrl}/api/docs/openapi`, {\n timeoutMs: resolveOpenapiFetchTimeoutMs(),\n })\n if (response.ok) {\n const doc = (await response.json()) as OpenApiDocument\n logger.debug('Raw OpenAPI spec fetched via HTTP')\n return doc\n }\n } catch (error) {\n logger.warn('Raw spec HTTP fetch failed', { err: error })\n }\n\n return null\n}\n\n/**\n * Parse endpoints from generated OpenAPI JSON file (for CLI context).\n * This is generated by `yarn generate`.\n */\nasync function parseApiEndpointsFromGeneratedJson(): Promise<ApiEndpoint[]> {\n try {\n const fs = await import('node:fs')\n const path = await import('node:path')\n const { findAppRoot, findAllApps } = await import('@open-mercato/shared/lib/bootstrap/appResolver')\n\n let appRoot = findAppRoot()\n\n // Try monorepo structure if not found - walk up to find monorepo root\n if (!appRoot) {\n let current = process.cwd()\n // Walk up until we find a directory containing 'apps' folder\n while (current !== path.dirname(current)) {\n const appsDir = path.join(current, 'apps')\n if (fs.existsSync(appsDir)) {\n const apps = findAllApps(current)\n if (apps.length > 0) {\n appRoot = apps[0]\n break\n }\n }\n current = path.dirname(current)\n }\n }\n\n if (!appRoot) {\n logger.warn('Could not find app root')\n return []\n }\n\n const jsonPath = path.join(appRoot.generatedDir, 'openapi.generated.json')\n if (!fs.existsSync(jsonPath)) {\n logger.warn('openapi.generated.json not found - run yarn generate')\n return []\n }\n\n const doc = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as OpenApiDocument\n logger.debug('Loaded OpenAPI from generated JSON', { jsonPath })\n return extractEndpoints(doc)\n } catch (error) {\n logger.error('Error reading generated OpenAPI JSON', { err: error })\n return []\n }\n}\n\n/**\n * Parse endpoints from registered modules (works in Next.js context).\n */\nasync function parseApiEndpointsFromModules(): Promise<ApiEndpoint[]> {\n try {\n const { getModules } = await import('@open-mercato/shared/lib/modules/registry')\n const modules: Module[] = getModules()\n\n // Count how many modules have APIs defined\n const modulesWithApis = modules.filter((m) => m.apis && m.apis.length > 0)\n\n if (modulesWithApis.length > 0) {\n logger.debug('Modules discovered for OpenAPI generation', { modules: modules.length, withApis: modulesWithApis.length })\n\n // Generate OpenAPI spec from modules\n const doc = buildOpenApiDocument(modules, {\n title: 'Open Mercato API',\n version: '1.0.0',\n servers: [{ url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }],\n })\n if (!doc.paths || Object.keys(doc.paths).length === 0) {\n return []\n }\n\n return extractEndpoints(doc)\n }\n } catch {\n // Registry not available\n }\n\n return []\n}\n\n/**\n * Parse OpenAPI spec via HTTP fetch.\n * Fetches the OpenAPI spec from the running app's /api/docs/openapi endpoint.\n */\nasync function parseApiEndpointsFromHttp(): Promise<ApiEndpoint[]> {\n const baseUrl =\n process.env.NEXT_PUBLIC_API_BASE_URL ||\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n 'http://localhost:3000'\n\n const openApiUrl = `${baseUrl}/api/docs/openapi`\n\n try {\n logger.debug('Fetching OpenAPI spec', { openApiUrl })\n const response = await fetchWithTimeout(openApiUrl, {\n timeoutMs: resolveOpenapiFetchTimeoutMs(),\n })\n\n if (!response.ok) {\n logger.warn('Failed to fetch OpenAPI spec', { status: response.status, statusText: response.statusText })\n return []\n }\n\n const doc = (await response.json()) as OpenApiDocument\n logger.debug('Successfully fetched OpenAPI spec')\n return extractEndpoints(doc)\n } catch (error) {\n logger.warn('Could not fetch OpenAPI spec', { err: error })\n logger.warn('OpenAPI fetch fallback requires the app to be running', { baseUrl })\n return []\n }\n}\n\n/**\n * Parse API endpoints - tries generated JSON first (CLI), then modules (Next.js), then HTTP.\n */\nasync function parseApiEndpoints(): Promise<ApiEndpoint[]> {\n // Try generated JSON first (works in CLI context without Next.js)\n const fromJson = await parseApiEndpointsFromGeneratedJson()\n if (fromJson.length > 0) {\n logger.debug('Loaded endpoints from generated JSON', { count: fromJson.length })\n return fromJson\n }\n\n // Try loading from module registry (works in Next.js context)\n const fromModules = await parseApiEndpointsFromModules()\n if (fromModules.length > 0) {\n logger.debug('Loaded endpoints from modules registry', { count: fromModules.length })\n return fromModules\n }\n\n // Fall back to HTTP fetch (requires running Next.js app)\n logger.debug('Generated JSON and modules not available, falling back to HTTP fetch')\n return parseApiEndpointsFromHttp()\n}\n\n/**\n * Extract endpoints from OpenAPI document\n */\nfunction extractEndpoints(doc: OpenApiDocument): ApiEndpoint[] {\n const endpoints: ApiEndpoint[] = []\n const validMethods = ['get', 'post', 'put', 'patch', 'delete']\n\n if (!doc.paths) {\n return endpoints\n }\n\n for (const [path, pathItem] of Object.entries(doc.paths)) {\n if (!pathItem || typeof pathItem !== 'object') continue\n\n for (const [method, operation] of Object.entries(pathItem)) {\n if (!validMethods.includes(method.toLowerCase())) continue\n if (!operation || typeof operation !== 'object') continue\n\n const op = operation as any\n\n // Generate operationId if not present\n const operationId = op.operationId || generateOperationId(path, method)\n\n const endpoint: ApiEndpoint = {\n id: operationId,\n operationId,\n method: method.toUpperCase(),\n path,\n summary: op.summary || '',\n description: op.description || op.summary || `${method.toUpperCase()} ${path}`,\n tags: op.tags || [],\n requiredFeatures: op['x-require-features'] || [],\n deprecated: op.deprecated || false,\n parameters: extractParameters(op.parameters || []),\n requestBodySchema: extractRequestBodySchema(op.requestBody, doc.components?.schemas),\n }\n\n endpoints.push(endpoint)\n }\n }\n\n logger.debug('Parsed endpoints from OpenAPI spec', { count: endpoints.length })\n return endpoints\n}\n\n/**\n * Generate operationId from path and method\n */\nfunction generateOperationId(path: string, method: string): string {\n const pathParts = path\n .replace(/^\\//, '')\n .replace(/\\{([^}]+)\\}/g, 'by_$1')\n .split('/')\n .filter(Boolean)\n .join('_')\n\n return `${method.toLowerCase()}_${pathParts}`\n}\n\n/**\n * Extract parameter info\n */\nfunction extractParameters(params: any[]): ApiParameter[] {\n return params\n .filter((p) => p.in === 'path' || p.in === 'query')\n .map((p) => ({\n name: p.name,\n in: p.in,\n required: p.required ?? false,\n type: p.schema?.type || 'string',\n description: p.description || '',\n }))\n}\n\n/**\n * Extract request body schema (simplified)\n */\nfunction extractRequestBodySchema(\n requestBody: any,\n schemas?: Record<string, any>\n): Record<string, unknown> | null {\n if (!requestBody?.content?.['application/json']?.schema) {\n return null\n }\n\n const schema = requestBody.content['application/json'].schema\n\n // Resolve $ref if present\n if (schema.$ref && schemas) {\n const refPath = schema.$ref.replace('#/components/schemas/', '')\n return schemas[refPath] || schema\n }\n\n return schema\n}\n\n/**\n * Clear endpoint cache (for testing)\n */\nexport function clearEndpointCache(): void {\n endpointsCache = null\n endpointsByOperationId = null\n rawSpecCache = null\n}\n\n/**\n * Extract simplified request body schema for LLM consumption.\n * Returns required fields and basic property info without deep nesting.\n */\nexport function simplifyRequestBodySchema(\n schema: Record<string, unknown> | null\n): { required: string[]; properties: Record<string, { type: string; format?: string; enum?: string[] }> } | null {\n if (!schema) return null\n\n const properties: Record<string, { type: string; format?: string; enum?: string[] }> = {}\n const required: string[] = (schema.required as string[]) || []\n\n const schemaProps = (schema.properties || schema) as Record<string, unknown>\n\n for (const [key, value] of Object.entries(schemaProps)) {\n if (typeof value !== 'object' || value === null) continue\n const propSchema = value as Record<string, unknown>\n\n const prop: { type: string; format?: string; enum?: string[] } = {\n type: (propSchema.type as string) || 'unknown',\n }\n\n if (propSchema.format) prop.format = propSchema.format as string\n if (propSchema.enum && Array.isArray(propSchema.enum)) {\n prop.enum = propSchema.enum.slice(0, 10) as string[] // Limit enum values\n }\n\n properties[key] = prop\n }\n\n return { required, properties }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { promises as fs } from "fs";
|
|
2
3
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
|
+
const logger = createLogger("ai_assistant");
|
|
3
5
|
const DEFAULT_MAX_INLINE_BYTES = 4 * 1024 * 1024;
|
|
4
6
|
const DEFAULT_MAX_TEXT_CHARS = 64 * 1024;
|
|
5
7
|
function classifyMediaType(mimeType) {
|
|
@@ -96,10 +98,7 @@ async function readAttachmentBytes(row) {
|
|
|
96
98
|
const buffer = await fs.readFile(absolutePath);
|
|
97
99
|
return new Uint8Array(buffer);
|
|
98
100
|
} catch (error) {
|
|
99
|
-
|
|
100
|
-
`[AI Agents] Failed to read attachment ${row.id} from storage; falling back to metadata-only:`,
|
|
101
|
-
error
|
|
102
|
-
);
|
|
101
|
+
logger.warn("Failed to read attachment from storage; falling back to metadata-only", { attachmentId: row.id, err: error });
|
|
103
102
|
return null;
|
|
104
103
|
}
|
|
105
104
|
}
|
|
@@ -144,10 +143,7 @@ async function classifyAndBuildPart(row, mediaClass, maxInlineBytes, maxTextChar
|
|
|
144
143
|
};
|
|
145
144
|
}
|
|
146
145
|
} catch (error) {
|
|
147
|
-
|
|
148
|
-
`[AI Agents] attachmentSigner failed for ${row.id}; falling back to metadata-only:`,
|
|
149
|
-
error
|
|
150
|
-
);
|
|
146
|
+
logger.warn("attachmentSigner failed; falling back to metadata-only", { attachmentId: row.id, err: error });
|
|
151
147
|
}
|
|
152
148
|
}
|
|
153
149
|
return { ...base, source: "metadata-only" };
|
|
@@ -159,9 +155,7 @@ async function resolveAttachmentParts(input) {
|
|
|
159
155
|
if (ids.length === 0) return [];
|
|
160
156
|
const em = resolveEm(input.container);
|
|
161
157
|
if (!em) {
|
|
162
|
-
|
|
163
|
-
"[AI Agents] resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution."
|
|
164
|
-
);
|
|
158
|
+
logger.warn("resolveAttachmentParts called without a DI container exposing `em`; skipping attachment resolution");
|
|
165
159
|
return [];
|
|
166
160
|
}
|
|
167
161
|
const maxInlineBytes = input.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
|
|
@@ -175,27 +169,20 @@ async function resolveAttachmentParts(input) {
|
|
|
175
169
|
try {
|
|
176
170
|
row = await loadAttachmentRow(em, id, input.authContext);
|
|
177
171
|
} catch (error) {
|
|
178
|
-
|
|
179
|
-
`[AI Agents] Failed to load attachment ${id}; skipping:`,
|
|
180
|
-
error
|
|
181
|
-
);
|
|
172
|
+
logger.warn("Failed to load attachment; skipping", { attachmentId: id, err: error });
|
|
182
173
|
continue;
|
|
183
174
|
}
|
|
184
175
|
if (!row) {
|
|
185
|
-
|
|
176
|
+
logger.warn("Attachment not found; skipping", { attachmentId: id });
|
|
186
177
|
continue;
|
|
187
178
|
}
|
|
188
179
|
if (!rowBelongsToCaller(row, input.authContext)) {
|
|
189
|
-
|
|
190
|
-
`[AI Agents] Attachment ${id} is out of scope for caller (tenant=${input.authContext.tenantId}, org=${input.authContext.organizationId}); skipping.`
|
|
191
|
-
);
|
|
180
|
+
logger.warn("Attachment is out of scope for caller; skipping", { attachmentId: id, tenantId: input.authContext.tenantId, organizationId: input.authContext.organizationId });
|
|
192
181
|
continue;
|
|
193
182
|
}
|
|
194
183
|
const mediaClass = classifyMediaType(row.mimeType);
|
|
195
184
|
if (acceptedSet && !acceptedSet.has(mediaClass)) {
|
|
196
|
-
|
|
197
|
-
`[AI Agents] Attachment ${id} (${row.mimeType}) is not in agent acceptedMediaTypes=${[...acceptedSet].join(",")}; skipping.`
|
|
198
|
-
);
|
|
185
|
+
logger.warn("Attachment is not in agent acceptedMediaTypes; skipping", { attachmentId: id, mimeType: row.mimeType, acceptedMediaTypes: [...acceptedSet].join(",") });
|
|
199
186
|
continue;
|
|
200
187
|
}
|
|
201
188
|
try {
|
|
@@ -209,10 +196,7 @@ async function resolveAttachmentParts(input) {
|
|
|
209
196
|
);
|
|
210
197
|
parts.push(part);
|
|
211
198
|
} catch (error) {
|
|
212
|
-
|
|
213
|
-
`[AI Agents] Failed to build attachment part for ${id}; skipping:`,
|
|
214
|
-
error
|
|
215
|
-
);
|
|
199
|
+
logger.warn("Failed to build attachment part; skipping", { attachmentId: id, err: error });
|
|
216
200
|
}
|
|
217
201
|
}
|
|
218
202
|
return parts;
|