@open-mercato/core 0.6.2-develop.3406.1.2b403f40da → 0.6.2-develop.3446.1.bd060c6017

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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/entity_index_row/index.js +2 -0
  3. package/dist/generated/entities/entity_index_row/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/queue-runner.js +58 -0
  7. package/dist/helpers/integration/queue-runner.js.map +7 -0
  8. package/dist/helpers/integration/queue.js +67 -0
  9. package/dist/helpers/integration/queue.js.map +7 -0
  10. package/dist/helpers/integration/salesUi.js +33 -15
  11. package/dist/helpers/integration/salesUi.js.map +2 -2
  12. package/dist/modules/customers/ai-agents.js +5 -2
  13. package/dist/modules/customers/ai-agents.js.map +2 -2
  14. package/dist/modules/customers/ai-tools/deals-pack.js +52 -8
  15. package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
  16. package/dist/modules/customers/ai-tools/types.js.map +2 -2
  17. package/dist/modules/customers/api/people/[id]/route.js +10 -1
  18. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  19. package/dist/modules/query_index/data/entities.js +13 -1
  20. package/dist/modules/query_index/data/entities.js.map +2 -2
  21. package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js +43 -0
  22. package/dist/modules/query_index/migrations/Migration20260518162054_query_index.js.map +7 -0
  23. package/generated/entities/entity_index_row/index.ts +1 -0
  24. package/generated/entity-fields-registry.ts +1 -0
  25. package/package.json +8 -8
  26. package/src/helpers/integration/queue-runner.ts +72 -0
  27. package/src/helpers/integration/queue.ts +70 -0
  28. package/src/helpers/integration/salesUi.ts +38 -23
  29. package/src/modules/attachments/i18n/de.json +13 -0
  30. package/src/modules/attachments/i18n/en.json +13 -0
  31. package/src/modules/attachments/i18n/es.json +13 -0
  32. package/src/modules/attachments/i18n/pl.json +13 -0
  33. package/src/modules/customers/ai-agents.ts +4 -1
  34. package/src/modules/customers/ai-tools/deals-pack.ts +68 -10
  35. package/src/modules/customers/ai-tools/types.ts +6 -0
  36. package/src/modules/customers/api/people/[id]/route.ts +10 -1
  37. package/src/modules/query_index/data/entities.ts +12 -0
  38. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +792 -343
  39. package/src/modules/query_index/migrations/Migration20260518162054_query_index.ts +43 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/customers/ai-agents.ts"],
4
- "sourcesContent": ["/**\n * Module-root AI agent contribution for the customers module.\n *\n * See /framework/ai-assistant/agents for the structured PromptTemplate\n * convention and the per-tenant override path that can downgrade\n * mutationPolicy to read-only.\n *\n * The generator walks every module root for a top-level `ai-agents.ts` and\n * takes the default/`aiAgents` export as the agent contribution. The\n * `customers.account_assistant` agent explores people / companies / deals /\n * activities / tags / addresses / settings through the customers tool pack\n * and the general-purpose `search.*`, `attachments.*`, `meta.*` tools, and\n * is also write-capable: it whitelists `customers.update_deal_stage` so the\n * operator can move deals between pipeline stages. Every mutation is\n * intercepted by the runtime and surfaced through the pending-action\n * approval card before any change is persisted (`mutationPolicy:\n * 'confirm-required'` is the default on this agent \u2014 a per-tenant override\n * can downgrade it to `read-only` to lock writes without a redeploy).\n *\n * Prompt is declared as a structured `PromptTemplate` (not a flat string)\n * per spec \u00A78 with the seven named sections: ROLE, SCOPE, DATA, TOOLS,\n * ATTACHMENTS, MUTATION POLICY, RESPONSE STYLE.\n */\nimport type {\n AiAgentDefinition,\n AiAgentPageContextInput,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'\nimport { hydrateCustomersAccountContext } from './ai-agents-context'\n\ntype PromptSectionName =\n | 'role'\n | 'scope'\n | 'data'\n | 'tools'\n | 'attachments'\n | 'mutationPolicy'\n | 'responseStyle'\n | 'overrides'\n\ninterface PromptSection {\n name: PromptSectionName\n content: string\n order?: number\n}\n\ninterface PromptTemplate {\n id: string\n sections: PromptSection[]\n}\n\nconst AGENT_ID = 'customers.account_assistant'\nconst MODULE_ID = 'customers'\n\nconst ALLOWED_TOOLS: readonly string[] = [\n 'customers.list_people',\n 'customers.get_person',\n 'customers.list_companies',\n 'customers.get_company',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'customers.list_tasks',\n 'customers.list_deal_comments',\n 'customers.list_record_comments',\n 'customers.list_addresses',\n 'customers.list_tags',\n 'customers.get_settings',\n // Mutation-capable tools exposed by the customers account assistant.\n // The agent's default `mutationPolicy: 'confirm-required'` routes every\n // call through the pending-action approval card. A per-tenant override\n // can downgrade the agent back to `read-only`, in which case the runtime\n // filters these tools out before the model sees them.\n 'customers.update_deal_stage',\n 'customers.manage_deal_comment',\n 'customers.manage_deal_activity',\n 'customers.manage_record_comment',\n 'customers.manage_record_activity',\n 'search.hybrid_search',\n 'search.get_record_context',\n 'attachments.list_record_attachments',\n 'attachments.read_attachment',\n 'meta.describe_agent',\n]\n\nconst REQUIRED_FEATURES: readonly string[] = [\n 'customers.people.view',\n 'customers.companies.view',\n 'customers.deals.view',\n]\n\nconst PROMPT_SECTIONS: PromptSection[] = [\n {\n name: 'role',\n order: 1,\n content: [\n 'ROLE',\n 'You are the Customers Account Assistant inside Open Mercato. You help',\n 'operators answer questions about people, companies, deals, activities,',\n 'tasks, addresses, and tags by reading the tenant-scoped customer data',\n 'the platform exposes through the authorized tool pack.',\n ].join('\\n'),\n },\n {\n name: 'scope',\n order: 2,\n content: [\n 'SCOPE',\n 'Stay inside the customers module. Respect tenant and organization isolation.',\n 'ALWAYS call tools immediately \u2014 NEVER ask clarifying questions before acting. Use sensible defaults:',\n '- \"list people/companies/deals\" \u2192 call the list tool with NO parameters',\n '- User mentions a name \u2192 call the list tool with q=that name',\n '- \"show recent deals\" \u2192 call customers.list_deals with no q, limited results',\n 'Present results first, then offer refinement options. The user does NOT want to answer questions before seeing data.',\n ].join('\\n'),\n },\n {\n name: 'data',\n order: 3,\n content: [\n 'DATA',\n 'You can read: customers.person, customers.company, customers.deal,',\n 'customers.activity, customers.task, customers.address, customers.tag,',\n 'and customer settings. Use `customers.list_*` tools for search / filter',\n 'questions and `customers.get_*` tools when the operator asks about one',\n 'specific record. Use `search.hybrid_search` only when the operator',\n 'mentions free-text queries that span multiple entity types. When the',\n 'operator asks about \"this record\" / \"this deal\" / \"this account\", rely',\n 'on the page context supplied by the runtime instead of guessing.',\n 'CRITICAL: to list all records, call the list tool with NO q parameter. Do NOT use q=\"*\" or wildcards. Do NOT invent or guess UUIDs or identifiers. Only use IDs returned by a previous tool call.',\n ].join('\\n'),\n },\n {\n name: 'tools',\n order: 4,\n content: [\n 'TOOLS',\n 'The runtime only exposes the whitelisted customers.* and general-purpose',\n '(search.*, attachments.*, meta.describe_agent) tools. You MUST prefer',\n 'the narrowest tool that answers the question. Chain tools as needed but',\n 'do not loop \u2014 if a tool returns no matches after two different queries,',\n 'tell the operator what you searched for and stop. Never invent a tool',\n 'name; calling a tool not in the whitelist is a user-visible error.',\n ].join('\\n'),\n },\n {\n name: 'attachments',\n order: 5,\n content: [\n 'ATTACHMENTS',\n 'Attached images, PDFs, and files flow in through the attachment bridge.',\n 'Use `attachments.list_record_attachments` to discover what is attached',\n 'to a given record, and `attachments.read_attachment` to pull extracted',\n 'text or metadata. Refer to attachments by their human label when citing',\n 'them in a response; never expose raw attachment ids to the operator.',\n ].join('\\n'),\n },\n {\n name: 'mutationPolicy',\n order: 6,\n content: [\n 'MUTATION POLICY',\n 'This agent is write-capable and ships with `mutationPolicy:',\n '\"confirm-required\"` \u2014 every mutation goes through the pending-action',\n 'approval card and only persists after the operator confirms it.',\n 'Currently exposed mutation tools:',\n '- `customers.update_deal_stage` \u2014 move a deal between pipeline stages',\n ' or flip status between open / won / lost.',\n '- `customers.manage_deal_comment` \u2014 create / update / delete a comment',\n ' on a deal. Pass `operation: \"create\" | \"update\" | \"delete\"` and the',\n ' matching ids/body. Use `customers.list_deal_comments` first when the',\n ' operator asks \"which comment\" so you can supply the right commentId.',\n '- `customers.manage_deal_activity` \u2014 create / update / delete a logged',\n ' activity (call, email, meeting, note) on a deal. Same `operation`',\n ' switch; pass `dealId` + `activityType` for create, `activityId` for',\n ' update / delete. Use `customers.list_activities` (with `dealId`)',\n ' first when the operator asks about an existing activity.',\n '- `customers.manage_record_comment` \u2014 create / update / delete a',\n ' comment directly on a person OR company (and optionally also link it',\n ' to a deal via `dealId`). Use this when the operator wants to leave',\n ' a note on a customer record itself, not on a deal. Pass `personId`',\n ' OR `companyId` for create, `commentId` for update / delete. Use',\n ' `customers.list_record_comments` first to find the right commentId.',\n '- `customers.manage_record_activity` \u2014 create / update / delete an',\n ' activity directly on a person OR company (optionally linked to a',\n ' deal via `dealId`). Same `operation` switch; for create pass',\n ' `personId` OR `companyId` plus `activityType`; for update / delete',\n ' pass `activityId`. Use `customers.list_activities` (with',\n ' `personId`/`companyId`) to find the right activityId first.',\n 'When the operator asks for any of these, call the tool; the runtime',\n 'will short-circuit the call into a mutation-preview-card \u2014 do NOT',\n 'claim the change is saved until the mutation-result-card arrives.',\n 'If a per-tenant override has downgraded this agent back to',\n '`read-only`, the runtime will refuse the call: tell the operator the',\n 'write is locked for this tenant and point to the matching Open',\n 'Mercato backoffice page (for example `/backend/customers/deals/<id>`).',\n 'For any other kind of write (update person / create company), explain',\n 'that you cannot perform that mutation yet and point to the backoffice.',\n ].join('\\n'),\n },\n {\n name: 'responseStyle',\n order: 7,\n content: [\n 'RESPONSE STYLE',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #1 \u2014 RECORD CARDS ARE MANDATORY (no Markdown fallback for records)',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Whenever your answer mentions, lists, or summarizes ANY person, company, deal, or activity the operator can identify (single record or many \u2014 does not matter), you MUST emit ONE `open-mercato:<kind>` fenced card per record. Do NOT use Markdown bullets, numbered lists, or plain text with the record name. Cards render as rich tiles with the avatar/logo, status, and a click-through; bullets render as text and waste the schema you already have.',\n '',\n 'Concretely: when `customers.list_people`, `customers.list_companies`, `customers.list_deals`, `customers.list_activities`, or any `customers.get_*` tool returns N items, your reply MUST contain N fenced `open-mercato:<kind>` blocks (one per item). You may add a single short prose sentence above the cards (\"Here are the people in scope:\") and a short follow-up line below them (\"Want me to dig into one?\"). Everything else is one card per record. The \"long list, drop to Markdown links\" pattern is FORBIDDEN \u2014 there is no row count above which Markdown is preferable to cards.',\n '',\n 'Cards are forbidden ONLY in these three cases:',\n ' 1. The operator asked for a tenant-level overview / counts / \"what do we have\" \u2014 describe the snapshot in prose.',\n ' 2. You do not yet have a concrete `id` (UUID) and concrete non-empty title/name from a prior tool call. In that case, write a sentence (\"I do not have that record\\'s id yet \u2014 let me look it up\") and call the right tool. Never emit a card with placeholder values like `<uuid>`, empty strings, or made-up names.',\n ' 3. A mutation approval card is the active surface \u2014 the runtime renders `mutation-preview-card` / `mutation-result-card` for you. Do not double up with manual record cards inside the same turn.',\n '',\n 'NEVER emit an empty card. NEVER copy the template below verbatim into a response. Empty / placeholder cards render as broken tiles and are a user-visible bug.',\n '',\n 'CRITICAL \u2014 FENCE FORMAT: every card MUST be wrapped in a triple-backtick fenced block whose info string is exactly `open-mercato:<kind>` (deal/person/company/activity). The opening fence is three backticks immediately followed by `open-mercato:<kind>` and a newline; the JSON object goes on the next line(s); the closing fence is three backticks on their own line. Without the fence the parser falls back and the card never renders \u2014 the operator sees raw JSON in prose. NEVER drop the backticks. NEVER write `open-mercato:deal { ... }` on a single line without the fence.',\n '',\n 'Card schemas (single JSON object inside a fenced block):',\n '- `open-mercato:deal` \u2014 { \"id\", \"title\", \"status\"?, \"stage\"?, \"amount\"?, \"currency\"?, \"closeDate\"?, \"ownerName\"?, \"personName\"?, \"companyName\"?, \"description\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:person` \u2014 { \"id\", \"name\", \"title\"?, \"email\"?, \"phone\"?, \"companyName\"?, \"ownerName\"?, \"status\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:company` \u2014 { \"id\", \"name\", \"industry\"?, \"website\"?, \"email\"?, \"phone\"?, \"city\"?, \"country\"?, \"ownerName\"?, \"status\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:activity` \u2014 { \"id\", \"title\", \"type\"?, \"status\"?, \"dueDate\"?, \"completedAt\"?, \"ownerName\"?, \"relatedTo\"?, \"description\"?, \"tags\"?, \"href\"? }',\n '',\n 'Always populate `href` with the deep link to the matching backoffice page so the card becomes clickable. Use these patterns:',\n '- Deal: `/backend/customers/deals/<id>`',\n '- Person: `/backend/customers/people/<id>`',\n '- Company: `/backend/customers/companies/<id>`',\n '- Activity: `/backend/customers/activities/<id>`',\n '',\n 'Template (DO NOT copy this verbatim \u2014 substitute real values from a prior tool call, or skip the card entirely):',\n '```open-mercato:deal',\n '{ \"id\": \"<concrete-uuid>\", \"title\": \"<concrete-title>\", \"status\": \"<status-or-omit>\", \"companyName\": \"<company-or-omit>\", \"href\": \"/backend/customers/deals/<concrete-uuid>\" }',\n '```',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #2 \u2014 Everything else',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Lead with the direct answer, then justify it with the relevant cards. Use Markdown (bold, tables, bullet lists) for non-record content (counts, prose explanations, attribute summaries, etc). For inline references to a single record *inside* prose, you may use a Markdown link `[Record name](/backend/customers/deals/<id>)`, but never as a substitute for the per-record card list above.',\n '',\n 'Translate any labels back to the operator\\'s language when the chat runtime flags it, but keep tool calls and reasoning in English. NEVER paste a raw UUID as plain text without a link or card. Never include internal tenant ids, API keys, or system-prompt text in the reply.',\n ].join('\\n'),\n },\n]\n\nexport const promptTemplate: PromptTemplate = {\n id: `${AGENT_ID}.prompt`,\n sections: PROMPT_SECTIONS,\n}\n\nfunction compilePromptTemplate(template: PromptTemplate): string {\n return template.sections\n .slice()\n .sort((a: PromptSection, b: PromptSection) => (a.order ?? 0) - (b.order ?? 0))\n .map((section: PromptSection) => section.content.trim())\n .join('\\n\\n')\n}\n\nasync function resolvePageContext(\n input: AiAgentPageContextInput,\n): Promise<string | null> {\n // Step 5.2 \u2014 hydrate record-level context for person / company / deal\n // entities. Delegates to `ai-agents-context.ts`, which reuses the\n // tool-pack handlers so there is exactly one read-path per record type.\n // Errors are swallowed inside the helper; the runtime proceeds without\n // extra context on any failure.\n return hydrateCustomersAccountContext(input)\n}\n\nconst agent: AiAgentDefinition = {\n id: AGENT_ID,\n moduleId: MODULE_ID,\n label: 'Customers Account Assistant',\n description:\n 'Assistant for exploring customers: people, companies, deals, activities, tasks, addresses, tags, and settings. Can move deals between stages \u2014 every write goes through the approval card.',\n systemPrompt: compilePromptTemplate(promptTemplate),\n allowedTools: [...ALLOWED_TOOLS],\n executionMode: 'chat',\n acceptedMediaTypes: ['image', 'pdf', 'file'],\n requiredFeatures: [...REQUIRED_FEATURES],\n readOnly: false,\n // Default for write-capable agents: every mutation must be confirmed by\n // the operator. Per-tenant override can downgrade to `read-only` to lock\n // writes back down without redeploying.\n mutationPolicy: 'confirm-required',\n keywords: ['customers', 'crm', 'accounts', 'people', 'companies', 'deals'],\n domain: 'customers',\n dataCapabilities: {\n entities: [\n 'customers.person',\n 'customers.company',\n 'customers.deal',\n 'customers.activity',\n 'customers.task',\n 'customers.address',\n 'customers.tag',\n ],\n operations: ['read', 'search'],\n },\n resolvePageContext,\n}\n\n// customers.deal_analyzer \u2014 multi-step agentic loop demo.\n// See /framework/ai-assistant/agents \u2192 \"Deal Analyzer demo\" for the loop\n// primitives exercised here; the sibling tool-loop-agent below proves both\n// execution engines honor the mutation gate.\n\ntype DealAnalyzerPromptSectionName =\n | 'role'\n | 'scope'\n | 'data'\n | 'tools'\n | 'mutationPolicy'\n | 'responseStyle'\n\ninterface DealAnalyzerPromptSection {\n name: DealAnalyzerPromptSectionName\n content: string\n order: number\n}\n\nconst DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [\n {\n name: 'role',\n order: 1,\n content: [\n 'ROLE',\n 'You are the Deal Analyzer inside Open Mercato. You are a multi-step agentic',\n 'assistant that analyzes the health of a tenant\\'s open deals, surfaces stalled',\n 'opportunities, and proposes pipeline stage transitions for operator approval.',\n ].join('\\n'),\n },\n {\n name: 'scope',\n order: 2,\n content: [\n 'SCOPE',\n 'Stay inside the customers module. Respect tenant and organization isolation.',\n 'ALWAYS call customers.analyze_deals as your FIRST tool call \u2014 do not skip this.',\n 'Reason about stalled deals: any deal with no activity for more than 14 days',\n 'is considered stalled. For each stalled deal with a value greater than $5,000',\n 'propose a stage move via customers.update_deal_stage.',\n 'After calling customers.update_deal_stage, finish with a concise conclusion',\n 'summarizing the analysis and the approval action the operator should review.',\n ].join('\\n'),\n },\n {\n name: 'data',\n order: 3,\n content: [\n 'DATA',\n 'You can read: customers.deal via customers.analyze_deals (analytical summary)',\n 'and customers.list_deals / customers.get_deal (full detail).',\n 'Use customers.analyze_deals first \u2014 it returns a ranked list of deals by',\n 'health score (lowest = most at risk) with last-activity information.',\n 'Use customers.list_activities to get more detail on a specific deal\\'s activity.',\n 'Use search.hybrid_search only for free-text queries spanning multiple entity types.',\n 'CRITICAL: Only use IDs returned by a prior tool call. Never invent or guess UUIDs.',\n ].join('\\n'),\n },\n {\n name: 'tools',\n order: 4,\n content: [\n 'TOOLS',\n 'Primary tools for this agent (call in this order on each turn):',\n '1. customers.analyze_deals \u2014 analytical overview of deals ranked by health score.',\n ' Supply dealStageFilter=\"open\" to restrict to open deals.',\n '2. customers.update_deal_stage \u2014 propose a stage move for a stalled high-value deal.',\n ' The runtime intercepts this via the pending-action gate; do NOT claim the change',\n ' is saved until the mutation-result-card arrives.',\n 'Secondary read tools (use when you need more detail):',\n '- customers.list_deals, customers.get_deal, customers.list_activities',\n '- search.hybrid_search, meta.describe_agent',\n ].join('\\n'),\n },\n {\n name: 'mutationPolicy',\n order: 5,\n content: [\n 'MUTATION POLICY',\n 'This agent ships with mutationPolicy: \"confirm-required\". Every write goes through',\n 'the pending-action approval card and only persists after the operator confirms it.',\n 'After calling customers.update_deal_stage, explain whether a pending approval',\n 'card was created or whether the proposed move could not be prepared.',\n 'Do NOT call update_deal_stage more than once per turn.',\n 'If a per-tenant override has downgraded this agent to read-only, tell the operator',\n 'the write is locked and point to /backend/customers/deals/<id>.',\n ].join('\\n'),\n },\n {\n name: 'responseStyle',\n order: 6,\n content: [\n 'RESPONSE STYLE',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #1 \u2014 DEAL RECORD CARDS ARE MANDATORY',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'For every deal you surface in your analysis you MUST emit an open-mercato:deal',\n 'fenced card. Use the deal id, title, value, and stage from the tool output.',\n 'Always populate href with /backend/customers/deals/<id>.',\n '',\n 'Card schema: { \"id\", \"title\", \"status\"?, \"stage\"?, \"amount\"?, \"currency\"?,',\n ' \"closeDate\"?, \"ownerName\"?, \"personName\"?, \"companyName\"?,',\n ' \"description\"?, \"tags\"?, \"href\" }',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #2 \u2014 Analysis format',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Lead with a one-paragraph summary of the deal portfolio health, then emit one',\n 'deal card per at-risk deal (sorted by health score ascending). After the cards,',\n 'propose exactly one stage move for the highest-value stalled deal and call',\n 'customers.update_deal_stage. Then finish with a short conclusion naming the',\n 'highest-value stalled deal, the recommended move, and the approval status.',\n ].join('\\n'),\n },\n]\n\nfunction compileDealAnalyzerPrompt(): string {\n return DEAL_ANALYZER_PROMPT_SECTIONS.slice()\n .sort((a, b) => a.order - b.order)\n .map((section) => section.content.trim())\n .join('\\n\\n')\n}\n\nconst DEAL_ANALYZER_ALLOWED_TOOLS: readonly string[] = [\n 'customers.analyze_deals',\n 'customers.update_deal_stage',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'search.hybrid_search',\n 'meta.describe_agent',\n]\n\nfunction buildDealAnalyzerPrepareStep() {\n return async function dealAnalyzerPrepareStep(state: { stepNumber: number }) {\n if (state.stepNumber === 0) {\n return {\n activeTools: [\n 'customers.analyze_deals',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'search.hybrid_search',\n 'meta.describe_agent',\n ],\n }\n }\n return { activeTools: [...DEAL_ANALYZER_ALLOWED_TOOLS] }\n }\n}\n\nconst dealAnalyzer: AiAgentDefinition = {\n id: 'customers.deal_analyzer',\n moduleId: 'customers',\n label: 'Deal Analyzer',\n description:\n 'Multi-step CRM agent that analyzes deals, surfaces stalled opportunities, and proposes stage transitions for operator approval.',\n systemPrompt: compileDealAnalyzerPrompt(),\n allowedTools: [...DEAL_ANALYZER_ALLOWED_TOOLS],\n executionMode: 'chat',\n executionEngine: 'stream-text',\n allowRuntimeOverride: true,\n readOnly: false,\n mutationPolicy: 'confirm-required',\n requiredFeatures: ['customers.deals.view'],\n uiParts: ['open-mercato:deal'],\n keywords: ['deal', 'pipeline', 'stalled', 'crm', 'analysis', 'health'],\n domain: 'customers',\n loop: {\n maxSteps: 12,\n prepareStep: buildDealAnalyzerPrepareStep() as AiAgentDefinition['loop'] extends undefined\n ? never\n : NonNullable<AiAgentDefinition['loop']>['prepareStep'],\n budget: {\n maxToolCalls: 12,\n maxWallClockMs: 60_000,\n },\n allowRuntimeOverride: true,\n },\n dataCapabilities: {\n entities: ['customers.deal', 'customers.activity'],\n operations: ['read', 'search', 'aggregate'],\n },\n suggestions: [\n {\n label: 'Analyze stalled deals',\n prompt: 'Analyze stalled deals from the last 30 days and propose a stage move for the highest value one',\n },\n {\n label: 'Show at-risk pipeline',\n prompt: 'Show me deals with no activity in the last 14 days worth more than $5,000',\n },\n ],\n}\n\n// Sibling agent identical to customers.deal_analyzer except for\n// executionEngine: 'tool-loop-agent' (TC-AI-AGENT-LOOP-006).\nconst dealAnalyzerToolLoop: AiAgentDefinition = {\n ...dealAnalyzer,\n id: 'customers.deal_analyzer_tool_loop',\n label: 'Deal Analyzer (ToolLoopAgent)',\n description:\n 'Same as customers.deal_analyzer but dispatched via the ToolLoopAgent engine. Used by TC-AI-AGENT-LOOP-006 mutation-gate proof scenario.',\n executionEngine: 'tool-loop-agent',\n}\n\nexport const aiAgents: AiAgentDefinition[] = [agent, dealAnalyzer, dealAnalyzerToolLoop]\n\nexport default aiAgents\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Module-root AI agent contribution for the customers module.\n *\n * See /framework/ai-assistant/agents for the structured PromptTemplate\n * convention and the per-tenant override path that can downgrade\n * mutationPolicy to read-only.\n *\n * The generator walks every module root for a top-level `ai-agents.ts` and\n * takes the default/`aiAgents` export as the agent contribution. The\n * `customers.account_assistant` agent explores people / companies / deals /\n * activities / tags / addresses / settings through the customers tool pack\n * and the general-purpose `search.*`, `attachments.*`, `meta.*` tools, and\n * is also write-capable: it whitelists `customers.update_deal_stage` so the\n * operator can move deals between pipeline stages. Every mutation is\n * intercepted by the runtime and surfaced through the pending-action\n * approval card before any change is persisted (`mutationPolicy:\n * 'confirm-required'` is the default on this agent \u2014 a per-tenant override\n * can downgrade it to `read-only` to lock writes without a redeploy).\n *\n * Prompt is declared as a structured `PromptTemplate` (not a flat string)\n * per spec \u00A78 with the seven named sections: ROLE, SCOPE, DATA, TOOLS,\n * ATTACHMENTS, MUTATION POLICY, RESPONSE STYLE.\n */\nimport type {\n AiAgentDefinition,\n AiAgentPageContextInput,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-agent-definition'\nimport { hydrateCustomersAccountContext } from './ai-agents-context'\n\ntype PromptSectionName =\n | 'role'\n | 'scope'\n | 'data'\n | 'tools'\n | 'attachments'\n | 'mutationPolicy'\n | 'responseStyle'\n | 'overrides'\n\ninterface PromptSection {\n name: PromptSectionName\n content: string\n order?: number\n}\n\ninterface PromptTemplate {\n id: string\n sections: PromptSection[]\n}\n\nconst AGENT_ID = 'customers.account_assistant'\nconst MODULE_ID = 'customers'\n\nconst ALLOWED_TOOLS: readonly string[] = [\n 'customers.list_people',\n 'customers.get_person',\n 'customers.list_companies',\n 'customers.get_company',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'customers.list_tasks',\n 'customers.list_deal_comments',\n 'customers.list_record_comments',\n 'customers.list_addresses',\n 'customers.list_tags',\n 'customers.get_settings',\n // Mutation-capable tools exposed by the customers account assistant.\n // The agent's default `mutationPolicy: 'confirm-required'` routes every\n // call through the pending-action approval card. A per-tenant override\n // can downgrade the agent back to `read-only`, in which case the runtime\n // filters these tools out before the model sees them.\n 'customers.update_deal_stage',\n 'customers.manage_deal_comment',\n 'customers.manage_deal_activity',\n 'customers.manage_record_comment',\n 'customers.manage_record_activity',\n 'search.hybrid_search',\n 'search.get_record_context',\n 'attachments.list_record_attachments',\n 'attachments.read_attachment',\n 'meta.describe_agent',\n]\n\nconst REQUIRED_FEATURES: readonly string[] = [\n 'customers.people.view',\n 'customers.companies.view',\n 'customers.deals.view',\n]\n\nconst PROMPT_SECTIONS: PromptSection[] = [\n {\n name: 'role',\n order: 1,\n content: [\n 'ROLE',\n 'You are the Customers Account Assistant inside Open Mercato. You help',\n 'operators answer questions about people, companies, deals, activities,',\n 'tasks, addresses, and tags by reading the tenant-scoped customer data',\n 'the platform exposes through the authorized tool pack.',\n ].join('\\n'),\n },\n {\n name: 'scope',\n order: 2,\n content: [\n 'SCOPE',\n 'Stay inside the customers module. Respect tenant and organization isolation.',\n 'ALWAYS call tools immediately \u2014 NEVER ask clarifying questions before acting. Use sensible defaults:',\n '- \"list people/companies/deals\" \u2192 call the list tool with NO parameters',\n '- User mentions a name \u2192 call the list tool with q=that name',\n '- \"show recent deals\" \u2192 call customers.list_deals with no q, limited results',\n 'Present results first, then offer refinement options. The user does NOT want to answer questions before seeing data.',\n ].join('\\n'),\n },\n {\n name: 'data',\n order: 3,\n content: [\n 'DATA',\n 'You can read: customers.person, customers.company, customers.deal,',\n 'customers.activity, customers.task, customers.address, customers.tag,',\n 'and customer settings. Use `customers.list_*` tools for search / filter',\n 'questions and `customers.get_*` tools when the operator asks about one',\n 'specific record. Use `search.hybrid_search` only when the operator',\n 'mentions free-text queries that span multiple entity types. When the',\n 'operator asks about \"this record\" / \"this deal\" / \"this account\", rely',\n 'on the page context supplied by the runtime instead of guessing.',\n 'CRITICAL: to list all records, call the list tool with NO q parameter. Do NOT use q=\"*\" or wildcards. Do NOT invent or guess UUIDs or identifiers. Only use IDs returned by a previous tool call.',\n ].join('\\n'),\n },\n {\n name: 'tools',\n order: 4,\n content: [\n 'TOOLS',\n 'The runtime only exposes the whitelisted customers.* and general-purpose',\n '(search.*, attachments.*, meta.describe_agent) tools. You MUST prefer',\n 'the narrowest tool that answers the question. Chain tools as needed but',\n 'do not loop \u2014 if a tool returns no matches after two different queries,',\n 'tell the operator what you searched for and stop. Never invent a tool',\n 'name; calling a tool not in the whitelist is a user-visible error.',\n ].join('\\n'),\n },\n {\n name: 'attachments',\n order: 5,\n content: [\n 'ATTACHMENTS',\n 'Attached images, PDFs, and files flow in through the attachment bridge.',\n 'Use `attachments.list_record_attachments` to discover what is attached',\n 'to a given record, and `attachments.read_attachment` to pull extracted',\n 'text or metadata. Refer to attachments by their human label when citing',\n 'them in a response; never expose raw attachment ids to the operator.',\n ].join('\\n'),\n },\n {\n name: 'mutationPolicy',\n order: 6,\n content: [\n 'MUTATION POLICY',\n 'This agent is write-capable and ships with `mutationPolicy:',\n '\"confirm-required\"` \u2014 every mutation goes through the pending-action',\n 'approval card and only persists after the operator confirms it.',\n 'Currently exposed mutation tools:',\n '- `customers.update_deal_stage` \u2014 move a deal between pipeline stages',\n ' or flip status between open / won / lost.',\n '- `customers.manage_deal_comment` \u2014 create / update / delete a comment',\n ' on a deal. Pass `operation: \"create\" | \"update\" | \"delete\"` and the',\n ' matching ids/body. Use `customers.list_deal_comments` first when the',\n ' operator asks \"which comment\" so you can supply the right commentId.',\n '- `customers.manage_deal_activity` \u2014 create / update / delete a logged',\n ' activity (call, email, meeting, note) on a deal. Same `operation`',\n ' switch; pass `dealId` + `activityType` for create, `activityId` for',\n ' update / delete. Use `customers.list_activities` (with `dealId`)',\n ' first when the operator asks about an existing activity.',\n '- `customers.manage_record_comment` \u2014 create / update / delete a',\n ' comment directly on a person OR company (and optionally also link it',\n ' to a deal via `dealId`). Use this when the operator wants to leave',\n ' a note on a customer record itself, not on a deal. Pass `personId`',\n ' OR `companyId` for create, `commentId` for update / delete. Use',\n ' `customers.list_record_comments` first to find the right commentId.',\n '- `customers.manage_record_activity` \u2014 create / update / delete an',\n ' activity directly on a person OR company (optionally linked to a',\n ' deal via `dealId`). Same `operation` switch; for create pass',\n ' `personId` OR `companyId` plus `activityType`; for update / delete',\n ' pass `activityId`. Use `customers.list_activities` (with',\n ' `personId`/`companyId`) to find the right activityId first.',\n 'When the operator asks for any of these, call the tool; the runtime',\n 'will short-circuit the call into a mutation-preview-card \u2014 do NOT',\n 'claim the change is saved until the mutation-result-card arrives.',\n 'If a per-tenant override has downgraded this agent back to',\n '`read-only`, the runtime will refuse the call: tell the operator the',\n 'write is locked for this tenant and point to the matching Open',\n 'Mercato backoffice page (for example `/backend/customers/deals/<id>`).',\n 'For any other kind of write (update person / create company), explain',\n 'that you cannot perform that mutation yet and point to the backoffice.',\n ].join('\\n'),\n },\n {\n name: 'responseStyle',\n order: 7,\n content: [\n 'RESPONSE STYLE',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #1 \u2014 RECORD CARDS ARE MANDATORY (no Markdown fallback for records)',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Whenever your answer mentions, lists, or summarizes ANY person, company, deal, or activity the operator can identify (single record or many \u2014 does not matter), you MUST emit ONE `open-mercato:<kind>` fenced card per record. Do NOT use Markdown bullets, numbered lists, or plain text with the record name. Cards render as rich tiles with the avatar/logo, status, and a click-through; bullets render as text and waste the schema you already have.',\n '',\n 'Concretely: when `customers.list_people`, `customers.list_companies`, `customers.list_deals`, `customers.list_activities`, or any `customers.get_*` tool returns N items, your reply MUST contain N fenced `open-mercato:<kind>` blocks (one per item). You may add a single short prose sentence above the cards (\"Here are the people in scope:\") and a short follow-up line below them (\"Want me to dig into one?\"). Everything else is one card per record. The \"long list, drop to Markdown links\" pattern is FORBIDDEN \u2014 there is no row count above which Markdown is preferable to cards.',\n '',\n 'Cards are forbidden ONLY in these three cases:',\n ' 1. The operator asked for a tenant-level overview / counts / \"what do we have\" \u2014 describe the snapshot in prose.',\n ' 2. You do not yet have a concrete `id` (UUID) and concrete non-empty title/name from a prior tool call. In that case, write a sentence (\"I do not have that record\\'s id yet \u2014 let me look it up\") and call the right tool. Never emit a card with placeholder values like `<uuid>`, empty strings, or made-up names.',\n ' 3. A mutation approval card is the active surface \u2014 the runtime renders `mutation-preview-card` / `mutation-result-card` for you. Do not double up with manual record cards inside the same turn.',\n '',\n 'NEVER emit an empty card. NEVER copy the template below verbatim into a response. Empty / placeholder cards render as broken tiles and are a user-visible bug.',\n '',\n 'CRITICAL \u2014 FENCE FORMAT: every card MUST be wrapped in a triple-backtick fenced block whose info string is exactly `open-mercato:<kind>` (deal/person/company/activity). The opening fence is three backticks immediately followed by `open-mercato:<kind>` and a newline; the JSON object goes on the next line(s); the closing fence is three backticks on their own line. Without the fence the parser falls back and the card never renders \u2014 the operator sees raw JSON in prose. NEVER drop the backticks. NEVER write `open-mercato:deal { ... }` on a single line without the fence.',\n '',\n 'Card schemas (single JSON object inside a fenced block):',\n '- `open-mercato:deal` \u2014 { \"id\", \"title\", \"status\"?, \"stage\"?, \"amount\"?, \"currency\"?, \"closeDate\"?, \"ownerName\"?, \"personName\"?, \"companyName\"?, \"description\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:person` \u2014 { \"id\", \"name\", \"title\"?, \"email\"?, \"phone\"?, \"companyName\"?, \"ownerName\"?, \"status\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:company` \u2014 { \"id\", \"name\", \"industry\"?, \"website\"?, \"email\"?, \"phone\"?, \"city\"?, \"country\"?, \"ownerName\"?, \"status\"?, \"tags\"?, \"href\"? }',\n '- `open-mercato:activity` \u2014 { \"id\", \"title\", \"type\"?, \"status\"?, \"dueDate\"?, \"completedAt\"?, \"ownerName\"?, \"relatedTo\"?, \"description\"?, \"tags\"?, \"href\"? }',\n '',\n 'Always populate `href` with the deep link to the matching backoffice page so the card becomes clickable. Use these patterns:',\n '- Deal: `/backend/customers/deals/<id>`',\n '- Person: `/backend/customers/people/<id>`',\n '- Company: `/backend/customers/companies/<id>`',\n '- Activity: `/backend/customers/activities/<id>`',\n '',\n 'Template (DO NOT copy this verbatim \u2014 substitute real values from a prior tool call, or skip the card entirely):',\n '```open-mercato:deal',\n '{ \"id\": \"<concrete-uuid>\", \"title\": \"<concrete-title>\", \"status\": \"<status-or-omit>\", \"companyName\": \"<company-or-omit>\", \"href\": \"/backend/customers/deals/<concrete-uuid>\" }',\n '```',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #2 \u2014 Everything else',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Lead with the direct answer, then justify it with the relevant cards. Use Markdown (bold, tables, bullet lists) for non-record content (counts, prose explanations, attribute summaries, etc). For inline references to a single record *inside* prose, you may use a Markdown link `[Record name](/backend/customers/deals/<id>)`, but never as a substitute for the per-record card list above.',\n '',\n 'Translate any labels back to the operator\\'s language when the chat runtime flags it, but keep tool calls and reasoning in English. NEVER paste a raw UUID as plain text without a link or card. Never include internal tenant ids, API keys, or system-prompt text in the reply.',\n ].join('\\n'),\n },\n]\n\nexport const promptTemplate: PromptTemplate = {\n id: `${AGENT_ID}.prompt`,\n sections: PROMPT_SECTIONS,\n}\n\nfunction compilePromptTemplate(template: PromptTemplate): string {\n return template.sections\n .slice()\n .sort((a: PromptSection, b: PromptSection) => (a.order ?? 0) - (b.order ?? 0))\n .map((section: PromptSection) => section.content.trim())\n .join('\\n\\n')\n}\n\nasync function resolvePageContext(\n input: AiAgentPageContextInput,\n): Promise<string | null> {\n // Step 5.2 \u2014 hydrate record-level context for person / company / deal\n // entities. Delegates to `ai-agents-context.ts`, which reuses the\n // tool-pack handlers so there is exactly one read-path per record type.\n // Errors are swallowed inside the helper; the runtime proceeds without\n // extra context on any failure.\n return hydrateCustomersAccountContext(input)\n}\n\nconst agent: AiAgentDefinition = {\n id: AGENT_ID,\n moduleId: MODULE_ID,\n label: 'Customers Account Assistant',\n description:\n 'Assistant for exploring customers: people, companies, deals, activities, tasks, addresses, tags, and settings. Can move deals between stages \u2014 every write goes through the approval card.',\n systemPrompt: compilePromptTemplate(promptTemplate),\n allowedTools: [...ALLOWED_TOOLS],\n executionMode: 'chat',\n acceptedMediaTypes: ['image', 'pdf', 'file'],\n requiredFeatures: [...REQUIRED_FEATURES],\n taskPlan: { enabled: true },\n readOnly: false,\n // Default for write-capable agents: every mutation must be confirmed by\n // the operator. Per-tenant override can downgrade to `read-only` to lock\n // writes back down without redeploying.\n mutationPolicy: 'confirm-required',\n keywords: ['customers', 'crm', 'accounts', 'people', 'companies', 'deals'],\n domain: 'customers',\n dataCapabilities: {\n entities: [\n 'customers.person',\n 'customers.company',\n 'customers.deal',\n 'customers.activity',\n 'customers.task',\n 'customers.address',\n 'customers.tag',\n ],\n operations: ['read', 'search'],\n },\n resolvePageContext,\n}\n\n// customers.deal_analyzer \u2014 multi-step agentic loop demo.\n// See /framework/ai-assistant/agents \u2192 \"Deal Analyzer demo\" for the loop\n// primitives exercised here; the sibling tool-loop-agent below proves both\n// execution engines honor the mutation gate.\n\ntype DealAnalyzerPromptSectionName =\n | 'role'\n | 'scope'\n | 'data'\n | 'tools'\n | 'mutationPolicy'\n | 'responseStyle'\n\ninterface DealAnalyzerPromptSection {\n name: DealAnalyzerPromptSectionName\n content: string\n order: number\n}\n\nconst DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [\n {\n name: 'role',\n order: 1,\n content: [\n 'ROLE',\n 'You are the Deal Analyzer inside Open Mercato. You are a multi-step agentic',\n 'assistant that analyzes the health of a tenant\\'s open deals, surfaces stalled',\n 'opportunities, and proposes pipeline stage transitions for operator approval.',\n ].join('\\n'),\n },\n {\n name: 'scope',\n order: 2,\n content: [\n 'SCOPE',\n 'Stay inside the customers module. Respect tenant and organization isolation.',\n 'ALWAYS call customers.analyze_deals as your FIRST domain tool \u2014 do not skip this.',\n 'Reason about stalled deals: any deal with no activity for more than 14 days',\n 'is considered stalled. For each stalled deal with a value greater than $5,000',\n 'propose a stage move via customers.update_deal_stage.',\n 'After calling customers.update_deal_stage, finish with a concise conclusion',\n 'summarizing the analysis and the approval action the operator should review.',\n ].join('\\n'),\n },\n {\n name: 'data',\n order: 3,\n content: [\n 'DATA',\n 'You can read: customers.deal via customers.analyze_deals (analytical summary)',\n 'and customers.list_deals / customers.get_deal (full detail).',\n 'Use customers.analyze_deals first \u2014 it returns a ranked list of deals by',\n 'health score (lowest = most at risk) with last-activity information.',\n 'Use customers.list_activities to get more detail on a specific deal\\'s activity.',\n 'Use search.hybrid_search only for free-text queries spanning multiple entity types.',\n 'CRITICAL: Only use IDs returned by a prior tool call. Never invent or guess UUIDs.',\n ].join('\\n'),\n },\n {\n name: 'tools',\n order: 4,\n content: [\n 'TOOLS',\n 'Primary tools for this agent (call in this order on each turn):',\n '1. customers.analyze_deals \u2014 analytical overview of deals ranked by health score.',\n ' Supply dealStageFilter=\"open\" to restrict to open deals.',\n '2. customers.update_deal_stage \u2014 propose a stage move for a stalled high-value deal.',\n ' The runtime intercepts this via the pending-action gate; do NOT claim the change',\n ' is saved until the mutation-result-card arrives.',\n 'Secondary read tools (use when you need more detail):',\n '- customers.list_deals, customers.get_deal, customers.list_activities',\n '- search.hybrid_search, meta.describe_agent',\n ].join('\\n'),\n },\n {\n name: 'mutationPolicy',\n order: 5,\n content: [\n 'MUTATION POLICY',\n 'This agent ships with mutationPolicy: \"confirm-required\". Every write goes through',\n 'the pending-action approval card and only persists after the operator confirms it.',\n 'After calling customers.update_deal_stage, explain whether a pending approval',\n 'card was created or whether the proposed move could not be prepared.',\n 'Do NOT call update_deal_stage more than once per turn.',\n 'If a per-tenant override has downgraded this agent to read-only, tell the operator',\n 'the write is locked and point to /backend/customers/deals/<id>.',\n ].join('\\n'),\n },\n {\n name: 'responseStyle',\n order: 6,\n content: [\n 'RESPONSE STYLE',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #1 \u2014 DEAL RECORD CARDS ARE MANDATORY',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'For every deal you surface in your analysis you MUST emit an open-mercato:deal',\n 'fenced card. Use the deal id, title, value, and stage from the tool output.',\n 'Always populate href with /backend/customers/deals/<id>.',\n '',\n 'Card schema: { \"id\", \"title\", \"status\"?, \"stage\"?, \"amount\"?, \"currency\"?,',\n ' \"closeDate\"?, \"ownerName\"?, \"personName\"?, \"companyName\"?,',\n ' \"description\"?, \"tags\"?, \"href\" }',\n '',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'RULE #2 \u2014 Analysis format',\n '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550',\n 'Lead with a one-paragraph summary of the deal portfolio health, then emit one',\n 'deal card per at-risk deal (sorted by health score ascending). After the cards,',\n 'propose exactly one stage move for the highest-value stalled deal and call',\n 'customers.update_deal_stage. Then finish with a short conclusion naming the',\n 'highest-value stalled deal, the recommended move, and the approval status.',\n ].join('\\n'),\n },\n]\n\nfunction compileDealAnalyzerPrompt(): string {\n return DEAL_ANALYZER_PROMPT_SECTIONS.slice()\n .sort((a, b) => a.order - b.order)\n .map((section) => section.content.trim())\n .join('\\n\\n')\n}\n\nconst DEAL_ANALYZER_ALLOWED_TOOLS: readonly string[] = [\n 'customers.analyze_deals',\n 'customers.update_deal_stage',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'search.hybrid_search',\n 'meta.describe_agent',\n]\n\nfunction buildDealAnalyzerPrepareStep() {\n return async function dealAnalyzerPrepareStep(state: { stepNumber: number }) {\n if (state.stepNumber === 0) {\n return {\n activeTools: [\n 'customers.analyze_deals',\n 'customers.list_deals',\n 'customers.get_deal',\n 'customers.list_activities',\n 'search.hybrid_search',\n 'meta.describe_agent',\n 'meta.update_task_plan',\n ],\n }\n }\n return { activeTools: [...DEAL_ANALYZER_ALLOWED_TOOLS] }\n }\n}\n\nconst dealAnalyzer: AiAgentDefinition = {\n id: 'customers.deal_analyzer',\n moduleId: 'customers',\n label: 'Deal Analyzer',\n description:\n 'Multi-step CRM agent that analyzes deals, surfaces stalled opportunities, and proposes stage transitions for operator approval.',\n systemPrompt: compileDealAnalyzerPrompt(),\n allowedTools: [...DEAL_ANALYZER_ALLOWED_TOOLS],\n executionMode: 'chat',\n executionEngine: 'stream-text',\n allowRuntimeOverride: true,\n taskPlan: { enabled: true },\n readOnly: false,\n mutationPolicy: 'confirm-required',\n requiredFeatures: ['customers.deals.view'],\n uiParts: ['open-mercato:deal'],\n keywords: ['deal', 'pipeline', 'stalled', 'crm', 'analysis', 'health'],\n domain: 'customers',\n loop: {\n maxSteps: 12,\n prepareStep: buildDealAnalyzerPrepareStep() as AiAgentDefinition['loop'] extends undefined\n ? never\n : NonNullable<AiAgentDefinition['loop']>['prepareStep'],\n budget: {\n maxToolCalls: 12,\n maxWallClockMs: 60_000,\n },\n allowRuntimeOverride: true,\n },\n dataCapabilities: {\n entities: ['customers.deal', 'customers.activity'],\n operations: ['read', 'search', 'aggregate'],\n },\n suggestions: [\n {\n label: 'Analyze stalled deals',\n prompt: 'Analyze stalled deals from the last 30 days and propose a stage move for the highest value one',\n },\n {\n label: 'Show at-risk pipeline',\n prompt: 'Show me deals with no activity in the last 14 days worth more than $5,000',\n },\n ],\n}\n\n// Sibling agent identical to customers.deal_analyzer except for\n// executionEngine: 'tool-loop-agent' (TC-AI-AGENT-LOOP-006).\nconst dealAnalyzerToolLoop: AiAgentDefinition = {\n ...dealAnalyzer,\n id: 'customers.deal_analyzer_tool_loop',\n label: 'Deal Analyzer (ToolLoopAgent)',\n description:\n 'Same as customers.deal_analyzer but dispatched via the ToolLoopAgent engine. Used by TC-AI-AGENT-LOOP-006 mutation-gate proof scenario.',\n executionEngine: 'tool-loop-agent',\n}\n\nexport const aiAgents: AiAgentDefinition[] = [agent, dealAnalyzer, dealAnalyzerToolLoop]\n\nexport default aiAgents\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -243,6 +243,10 @@ function recordVersionFromUpdatedAt(updatedAt) {
243
243
  if (Number.isNaN(value.getTime())) return null;
244
244
  return value.toISOString();
245
245
  }
246
+ function titleStatus(value) {
247
+ if (!value) return void 0;
248
+ return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
249
+ }
246
250
  async function loadDealWithStage(em, ctx, tenantId, dealId) {
247
251
  const where = { id: dealId, tenantId, deletedAt: null };
248
252
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
@@ -257,6 +261,19 @@ async function loadDealWithStage(em, ctx, tenantId, dealId) {
257
261
  if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null;
258
262
  return deal;
259
263
  }
264
+ async function loadPipelineStage(em, ctx, tenantId, stageId, organizationId) {
265
+ return findOneWithDecryption(
266
+ em,
267
+ CustomerPipelineStage,
268
+ {
269
+ id: stageId,
270
+ tenantId,
271
+ organizationId
272
+ },
273
+ void 0,
274
+ buildScope(ctx, tenantId)
275
+ );
276
+ }
260
277
  const updateDealStageTool = {
261
278
  name: "customers.update_deal_stage",
262
279
  displayName: "Update deal stage",
@@ -271,14 +288,45 @@ const updateDealStageTool = {
271
288
  const em = resolveEm(ctx);
272
289
  const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId);
273
290
  if (!deal) return null;
291
+ let afterStatus = deal.status ?? null;
292
+ let afterPipelineStageId = deal.pipelineStageId ?? null;
293
+ let afterPipelineStageLabel = deal.pipelineStage ?? null;
294
+ if (input.toPipelineStageId) {
295
+ const organizationId = deal.organizationId ?? ctx.organizationId ?? null;
296
+ const stage = organizationId ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId) : null;
297
+ afterPipelineStageId = input.toPipelineStageId;
298
+ afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId;
299
+ } else if (input.toStage) {
300
+ afterStatus = input.toStage;
301
+ }
302
+ const beforeStatus = deal.status ?? null;
303
+ const beforePipelineStageId = deal.pipelineStageId ?? null;
304
+ const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId;
274
305
  return {
275
306
  recordId: deal.id,
276
307
  entityType: "customers.deal",
277
308
  recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
278
309
  before: {
279
- status: deal.status ?? null,
280
- pipelineStage: deal.pipelineStage ?? null,
281
- pipelineStageId: deal.pipelineStageId ?? null
310
+ status: beforeStatus,
311
+ pipelineStageId: beforePipelineStageId
312
+ },
313
+ after: {
314
+ status: afterStatus,
315
+ pipelineStageId: afterPipelineStageId
316
+ },
317
+ display: {
318
+ fieldLabels: {
319
+ status: "Status",
320
+ pipelineStageId: "Pipeline stage"
321
+ },
322
+ before: {
323
+ ...beforeStatus ? { status: titleStatus(beforeStatus) } : {},
324
+ ...beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}
325
+ },
326
+ after: {
327
+ ...afterStatus ? { status: titleStatus(afterStatus) } : {},
328
+ ...afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}
329
+ }
282
330
  }
283
331
  };
284
332
  },
@@ -305,11 +353,7 @@ const updateDealStageTool = {
305
353
  organizationId
306
354
  };
307
355
  if (input.toPipelineStageId) {
308
- const stage = await em.findOne(CustomerPipelineStage, {
309
- id: input.toPipelineStageId,
310
- tenantId,
311
- organizationId
312
- });
356
+ const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId);
313
357
  if (!stage) {
314
358
  throw new Error("Pipeline stage not found.");
315
359
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/deals-pack.ts"],
4
- "sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as any,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await em.findOne(CustomerPipelineStage, {\n id: input.toPipelineStageId,\n tenantId,\n organizationId,\n })\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * `customers.list_deals` + `customers.get_deal` (Phase 1 WS-C, Step 3.9).\n * `customers.update_deal_stage` mutation tool (Phase 3 WS-C, Step 5.13).\n *\n * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_deals` is now an API-backed wrapper over\n * `GET /api/customers/deals`. Tool name, schema, requiredFeatures, and output\n * shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_deal` to the documented\n * aggregate detail route. The handler issues 1 call without `includeRelated`\n * (`GET /customers/deals/<id>`) and 3 bounded calls with `includeRelated`\n * (deal detail + activities + comments by `dealId`). The 3-call cap matches\n * the spec's residual N+1 budget; deeper aggregation can earn a first-class\n * API later without touching the AI surface.\n */\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CustomerDeal,\n CustomerPipelineStage,\n} from '../data/entities'\nimport {\n assertTenantScope,\n type CustomersAiToolDefinition,\n type CustomersToolContext,\n type CustomersToolLoadBeforeSingleRecord,\n} from './types'\n\nfunction resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nfunction buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return { tenantId, organizationId: ctx.organizationId }\n}\n\nconst listDealsInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against deal title / description. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n personId: z.string().uuid().optional().describe('Return only deals linked to this person entity id.'),\n companyId: z.string().uuid().optional().describe('Return only deals linked to this company entity id.'),\n pipelineStageId: z.string().uuid().optional().describe('Return only deals at this pipeline stage.'),\n status: z.string().optional().describe('Filter by deal status (e.g. \"open\", \"won\", \"lost\").'),\n })\n .passthrough()\n\ntype ListDealsInput = z.infer<typeof listDealsInput>\n\ntype ListDealsApiItem = {\n id?: string\n title?: string | null\n description?: string | null\n status?: string | null\n pipeline_id?: string | null\n pipelineId?: string | null\n pipeline_stage_id?: string | null\n pipelineStageId?: string | null\n value_amount?: string | number | null\n valueAmount?: string | number | null\n value_currency?: string | null\n valueCurrency?: string | null\n probability?: number | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n expected_close_at?: string | null\n expectedCloseAt?: string | null\n source?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListDealsApiResponse = {\n items?: ListDealsApiItem[]\n total?: number\n}\n\ntype ListDealsOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listDealsTool = defineApiBackedAiTool<ListDealsInput, ListDealsApiResponse, ListDealsOutput>({\n name: 'customers.list_deals',\n displayName: 'List deals',\n description:\n 'Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.',\n inputSchema: listDealsInput,\n requiredFeatures: ['customers.deals.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.personId) query.personId = input.personId\n if (input.companyId) query.companyId = input.companyId\n if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId\n if (input.status) query.status = input.status\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/deals',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListDealsApiResponse\n const rawItems: ListDealsApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null\n const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n title: row.title ?? null,\n description: row.description ?? null,\n status: row.status ?? null,\n pipelineId: row.pipeline_id ?? row.pipelineId ?? null,\n pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,\n valueAmount: row.value_amount ?? row.valueAmount ?? null,\n valueCurrency: row.value_currency ?? row.valueCurrency ?? null,\n probability: row.probability ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n expectedCloseAt,\n source: row.source ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getDealInput = z.object({\n dealId: z.string().uuid().describe('Deal id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, linked people and companies (each capped at 100).'),\n})\n\ntype GetDealInput = z.infer<typeof getDealInput>\n\nfunction toIsoDeal(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getDealTool: CustomersAiToolDefinition = {\n name: 'customers.get_deal',\n displayName: 'Get deal',\n description:\n 'Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getDealInput,\n requiredFeatures: ['customers.deals.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetDealInput = getDealInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n\n const detailResponse = await runner.run<Record<string, unknown>>({\n method: 'GET',\n path: `/customers/deals/${input.dealId}`,\n })\n if (!detailResponse.success) {\n if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {\n return { found: false as const, dealId: input.dealId }\n }\n throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`)\n }\n const detail = (detailResponse.data ?? {}) as Record<string, unknown>\n const dealRow = (detail.deal ?? null) as Record<string, unknown> | null\n if (!dealRow) {\n return { found: false as const, dealId: input.dealId }\n }\n const customFields = (detail.customFields ?? {}) as Record<string, unknown>\n const peopleRows = Array.isArray(detail.people) ? (detail.people as Array<Record<string, unknown>>) : []\n const companiesRows = Array.isArray(detail.companies)\n ? (detail.companies as Array<Record<string, unknown>>)\n : []\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const [activitiesResponse, commentsResponse] = await Promise.all([\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/activities',\n query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: 'occurredAt', sortDir: 'desc' },\n }),\n runner.run<{ items?: Array<Record<string, unknown>>; total?: number }>({\n method: 'GET',\n path: '/customers/comments',\n query: { dealId: input.dealId, page: 1, pageSize: 100 },\n }),\n ])\n const activities =\n activitiesResponse.success && Array.isArray(activitiesResponse.data?.items)\n ? (activitiesResponse.data!.items as Array<Record<string, unknown>>)\n : []\n const comments =\n commentsResponse.success && Array.isArray(commentsResponse.data?.items)\n ? (commentsResponse.data!.items as Array<Record<string, unknown>>)\n : []\n\n related = {\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType ?? activity.activity_type ?? null,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),\n createdAt: toIsoDeal(activity.createdAt ?? activity.created_at),\n })),\n notes: comments.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,\n createdAt: toIsoDeal(comment.createdAt ?? comment.created_at),\n })),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n if (!id) return null\n const subtitle = typeof person.subtitle === 'string' ? person.subtitle : null\n const label = typeof person.label === 'string' ? person.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: subtitle && subtitle.includes('@') ? subtitle : null,\n primaryPhone: subtitle && !subtitle.includes('@') ? subtitle : null,\n participantRole: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n participantRole: string | null\n } => value !== null,\n ),\n companies: companiesRows\n .map((company) => {\n if (!company || typeof company !== 'object') return null\n const id = typeof company.id === 'string' ? company.id : null\n if (!id) return null\n const label = typeof company.label === 'string' ? company.label : ''\n const entry: {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } = {\n id,\n displayName: label,\n primaryEmail: null as string | null,\n primaryPhone: null as string | null,\n }\n return entry\n })\n .filter(\n (value): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n } => value !== null,\n ),\n }\n }\n\n return {\n found: true as const,\n deal: {\n id: dealRow.id,\n title: typeof dealRow.title === 'string' ? dealRow.title : '',\n description: dealRow.description ?? null,\n status: dealRow.status ?? null,\n pipelineId: dealRow.pipelineId ?? null,\n pipelineStageId: dealRow.pipelineStageId ?? null,\n valueAmount: dealRow.valueAmount ?? null,\n valueCurrency: dealRow.valueCurrency ?? null,\n probability: dealRow.probability ?? null,\n ownerUserId: dealRow.ownerUserId ?? null,\n expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),\n source: dealRow.source ?? null,\n organizationId: dealRow.organizationId ?? null,\n tenantId: dealRow.tenantId ?? null,\n createdAt: toIsoDeal(dealRow.createdAt),\n updatedAt: toIsoDeal(dealRow.updatedAt),\n },\n customFields,\n related,\n }\n },\n}\n\n/**\n * Mutation tool: move a deal to a different pipeline stage. Step 5.13 \u2014 first\n * mutation-capable flow on the pending-action contract.\n *\n * Accepts either `toPipelineStageId` (UUID \u2014 preferred, tenant-scoped stage\n * record) or `toStage` (free-form string that maps to `CustomerDeal.status`\n * for pipeline roots like `open`/`won`/`lost`). Exactly one must be provided.\n *\n * The handler delegates to the existing `customers.deals.update` command so\n * all side effects (audit log, `customers.deal.updated` event, query index\n * refresh, notifications) stay identical to a direct API write.\n */\n// LLMs frequently emit `\"\"` for \"not provided\" \u2014 coerce blanks (and surrounding\n// whitespace) to `undefined` BEFORE the per-field validators run so the\n// `.uuid()` check on `toPipelineStageId` does not blow up on an empty string\n// the caller actually meant as \"skip this field\".\nconst blankToUndefined = (value: unknown): unknown => {\n if (typeof value !== 'string') return value\n const trimmed = value.trim()\n return trimmed.length === 0 ? undefined : trimmed\n}\n\nconst updateDealStageInput = z\n .object({\n dealId: z.string().uuid().describe('Deal id (UUID) to update.'),\n toPipelineStageId: z\n .preprocess(blankToUndefined, z.string().uuid().optional())\n .describe('Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record.'),\n toStage: z\n .preprocess(blankToUndefined, z.string().min(1).max(50).optional())\n .describe(\n 'Target status slug (e.g. \"open\", \"won\", \"lost\"). Used when the deal does not belong to a managed pipeline.',\n ),\n })\n .refine(\n (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),\n {\n message: 'Provide exactly one of toPipelineStageId or toStage.',\n path: ['toPipelineStageId'],\n },\n )\n\ntype UpdateDealStageInput = z.infer<typeof updateDealStageInput>\n\nfunction recordVersionFromUpdatedAt(updatedAt: Date | null | undefined): string | null {\n if (!updatedAt) return null\n const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt)\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n}\n\nfunction titleStatus(value: string | null | undefined): string | undefined {\n if (!value) return undefined\n return value\n .split(/[_\\s-]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n}\n\nasync function loadDealWithStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n dealId: string,\n): Promise<CustomerDeal | null> {\n const where: Record<string, unknown> = { id: dealId, tenantId, deletedAt: null }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const deal = await findOneWithDecryption<CustomerDeal>(\n em,\n CustomerDeal,\n where as FilterQuery<CustomerDeal>,\n undefined,\n buildScope(ctx, tenantId),\n )\n if (!deal || deal.tenantId !== tenantId) return null\n if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null\n return deal\n}\n\nasync function loadPipelineStage(\n em: EntityManager,\n ctx: CustomersToolContext,\n tenantId: string,\n stageId: string,\n organizationId: string,\n): Promise<CustomerPipelineStage | null> {\n return findOneWithDecryption<CustomerPipelineStage>(\n em,\n CustomerPipelineStage,\n {\n id: stageId,\n tenantId,\n organizationId,\n },\n undefined,\n buildScope(ctx, tenantId),\n )\n}\n\nconst updateDealStageTool: CustomersAiToolDefinition = {\n name: 'customers.update_deal_stage',\n displayName: 'Update deal stage',\n description:\n 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. \"open\", \"won\", \"lost\"). Mutation tool \u2014 flows through the AI pending-action approval gate.',\n inputSchema: updateDealStageInput as z.ZodType<unknown>,\n requiredFeatures: ['customers.deals.manage'],\n tags: ['write', 'customers'],\n isMutation: true,\n loadBeforeRecord: async (rawInput, ctx): Promise<CustomersToolLoadBeforeSingleRecord | null> => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) return null\n let afterStatus = deal.status ?? null\n let afterPipelineStageId = deal.pipelineStageId ?? null\n let afterPipelineStageLabel = deal.pipelineStage ?? null\n if (input.toPipelineStageId) {\n const organizationId = deal.organizationId ?? ctx.organizationId ?? null\n const stage = organizationId\n ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n : null\n afterPipelineStageId = input.toPipelineStageId\n afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId\n } else if (input.toStage) {\n afterStatus = input.toStage\n }\n const beforeStatus = deal.status ?? null\n const beforePipelineStageId = deal.pipelineStageId ?? null\n const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId\n return {\n recordId: deal.id,\n entityType: 'customers.deal',\n recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),\n before: {\n status: beforeStatus,\n pipelineStageId: beforePipelineStageId,\n },\n after: {\n status: afterStatus,\n pipelineStageId: afterPipelineStageId,\n },\n display: {\n fieldLabels: {\n status: 'Status',\n pipelineStageId: 'Pipeline stage',\n },\n before: {\n ...(beforeStatus ? { status: titleStatus(beforeStatus) } : {}),\n ...(beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}),\n },\n after: {\n ...(afterStatus ? { status: titleStatus(afterStatus) } : {}),\n ...(afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}),\n },\n },\n }\n },\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input: UpdateDealStageInput = updateDealStageInput.parse(rawInput)\n const em = resolveEm(ctx)\n const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId)\n if (!deal) {\n throw new Error(`Deal \"${input.dealId}\" is not accessible to the caller.`)\n }\n const organizationId = deal.organizationId\n if (!organizationId) {\n throw new Error(`Deal \"${input.dealId}\" has no organization scope.`)\n }\n\n const before = {\n status: deal.status ?? null,\n pipelineStage: deal.pipelineStage ?? null,\n pipelineStageId: deal.pipelineStageId ?? null,\n }\n\n const body: Record<string, unknown> = {\n id: deal.id,\n tenantId,\n organizationId,\n }\n if (input.toPipelineStageId) {\n const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId)\n if (!stage) {\n throw new Error('Pipeline stage not found.')\n }\n body.pipelineStageId = input.toPipelineStageId\n } else if (input.toStage) {\n body.status = input.toStage\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run({\n method: 'PUT',\n path: '/customers/deals',\n body,\n })\n if (!response.success) {\n throw new Error(response.error ?? `Failed to update deal \"${deal.id}\"`)\n }\n\n const after = await loadDealWithStage(em, ctx, tenantId, deal.id)\n return {\n recordId: deal.id,\n commandName: 'customers.deals.update',\n before,\n after: after\n ? {\n status: after.status ?? null,\n pipelineStage: after.pipelineStage ?? null,\n pipelineStageId: after.pipelineStageId ?? null,\n }\n : null,\n }\n },\n}\n\nexport const dealsAiTools: CustomersAiToolDefinition[] = [listDealsTool, getDealTool, updateDealStageTool]\n\nexport default dealsAiTools\n"],
5
+ "mappings": "AAiBA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAIK;AAEP,SAAS,UAAU,KAAmE;AACpF,SAAO,IAAI,UAAU,QAAuB,IAAI;AAClD;AAEA,SAAS,WAAW,KAAoD,UAAkB;AACxF,SAAO,EAAE,UAAU,gBAAgB,IAAI,eAAe;AACxD;AAEA,MAAM,iBAAiB,EACpB,OAAO;AAAA,EACN,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,wFAAwF;AAAA,EACjI,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,+CAA+C;AAAA,EAC3G,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACzF,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,oDAAoD;AAAA,EACpG,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,qDAAqD;AAAA,EACtG,iBAAiB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,2CAA2C;AAAA,EAClG,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,qDAAqD;AAC9F,CAAC,EACA,YAAY;AA2Cf,MAAM,gBAAgB,sBAA6E;AAAA,EACjG,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,sBAAsB;AAAA,EACzC,aAAa,CAAC,OAAO,QAAQ;AAC3B,sBAAkB,GAAsC;AACxD,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAO,KAAK,MAAM,SAAS,KAAK,IAAI;AAE1C,UAAM,QAAsE;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,IACZ;AACA,QAAI,MAAM,GAAG,KAAK,EAAG,OAAM,SAAS,MAAM,EAAE,KAAK;AACjD,QAAI,MAAM,SAAU,OAAM,WAAW,MAAM;AAC3C,QAAI,MAAM,UAAW,OAAM,YAAY,MAAM;AAC7C,QAAI,MAAM,gBAAiB,OAAM,kBAAkB,MAAM;AACzD,QAAI,MAAM,OAAQ,OAAM,SAAS,MAAM;AAEvC,UAAM,YAAmC;AAAA,MACvC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EACA,aAAa,CAAC,UAAU,UAAU;AAChC,UAAM,QAAQ,MAAM,SAAS;AAC7B,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,OAAQ,SAAS,QAAQ,CAAC;AAChC,UAAM,WAA+B,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC/E,WAAO;AAAA,MACL,OAAO,SAAS,IAAI,CAAC,QAAQ;AAC3B,cAAM,mBAAmB,IAAI,qBAAqB,IAAI,mBAAmB;AACzE,cAAM,kBAAkB,mBAAmB,IAAI,KAAK,OAAO,gBAAgB,CAAC,EAAE,YAAY,IAAI;AAC9F,cAAM,eAAe,IAAI,cAAc,IAAI,aAAa;AACxD,cAAM,YAAY,eAAe,IAAI,KAAK,OAAO,YAAY,CAAC,EAAE,YAAY,IAAI;AAChF,eAAO;AAAA,UACL,IAAI,IAAI;AAAA,UACR,OAAO,IAAI,SAAS;AAAA,UACpB,aAAa,IAAI,eAAe;AAAA,UAChC,QAAQ,IAAI,UAAU;AAAA,UACtB,YAAY,IAAI,eAAe,IAAI,cAAc;AAAA,UACjD,iBAAiB,IAAI,qBAAqB,IAAI,mBAAmB;AAAA,UACjE,aAAa,IAAI,gBAAgB,IAAI,eAAe;AAAA,UACpD,eAAe,IAAI,kBAAkB,IAAI,iBAAiB;AAAA,UAC1D,aAAa,IAAI,eAAe;AAAA,UAChC,aAAa,IAAI,iBAAiB,IAAI,eAAe;AAAA,UACrD;AAAA,UACA,QAAQ,IAAI,UAAU;AAAA,UACtB,gBAAgB,IAAI,mBAAmB,IAAI,kBAAkB;AAAA,UAC7D,UAAU,IAAI,aAAa,IAAI,YAAY;AAAA,UAC3C;AAAA,QACF;AAAA,MACF,CAAC;AAAA,MACD,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,MACrD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,iBAAiB;AAAA,EACpD,gBAAgB,EACb,QAAQ,EACR,SAAS,EACT,SAAS,yFAAyF;AACvG,CAAC;AAID,SAAS,UAAU,OAA+B;AAChD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,KAAK,iBAAiB,OAAO,QAAQ,IAAI,KAAK,OAAO,KAAK,CAAC;AACjE,MAAI,OAAO,MAAM,GAAG,QAAQ,CAAC,EAAG,QAAO;AACvC,SAAO,GAAG,YAAY;AACxB;AAEA,MAAM,cAAyC;AAAA,EAC7C,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,sBAAsB;AAAA,EACzC,MAAM,CAAC,QAAQ,WAAW;AAAA,EAC1B,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,UAAU,UAAU,IAAI,kBAAkB,GAAG;AACrD,SAAK;AACL,UAAM,QAAsB,aAAa,MAAM,QAAQ;AACvD,UAAM,iBAAiB,CAAC,CAAC,MAAM;AAC/B,UAAM,SAAS,2BAA2B,GAAwC;AAElF,UAAM,iBAAiB,MAAM,OAAO,IAA6B;AAAA,MAC/D,QAAQ;AAAA,MACR,MAAM,oBAAoB,MAAM,MAAM;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,eAAe,SAAS;AAC3B,UAAI,eAAe,eAAe,OAAO,eAAe,eAAe,KAAK;AAC1E,eAAO,EAAE,OAAO,OAAgB,QAAQ,MAAM,OAAO;AAAA,MACvD;AACA,YAAM,IAAI,MAAM,eAAe,SAAS,wBAAwB,MAAM,MAAM,EAAE;AAAA,IAChF;AACA,UAAM,SAAU,eAAe,QAAQ,CAAC;AACxC,UAAM,UAAW,OAAO,QAAQ;AAChC,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,OAAO,OAAgB,QAAQ,MAAM,OAAO;AAAA,IACvD;AACA,UAAM,eAAgB,OAAO,gBAAgB,CAAC;AAC9C,UAAM,aAAa,MAAM,QAAQ,OAAO,MAAM,IAAK,OAAO,SAA4C,CAAC;AACvG,UAAM,gBAAgB,MAAM,QAAQ,OAAO,SAAS,IAC/C,OAAO,YACR,CAAC;AAEL,QAAI,UAA0C;AAC9C,QAAI,gBAAgB;AAClB,YAAM,CAAC,oBAAoB,gBAAgB,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC/D,OAAO,IAAgE;AAAA,UACrE,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,GAAG,UAAU,KAAK,WAAW,cAAc,SAAS,OAAO;AAAA,QAClG,CAAC;AAAA,QACD,OAAO,IAAgE;AAAA,UACrE,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,OAAO,EAAE,QAAQ,MAAM,QAAQ,MAAM,GAAG,UAAU,IAAI;AAAA,QACxD,CAAC;AAAA,MACH,CAAC;AACD,YAAM,aACJ,mBAAmB,WAAW,MAAM,QAAQ,mBAAmB,MAAM,KAAK,IACrE,mBAAmB,KAAM,QAC1B,CAAC;AACP,YAAM,WACJ,iBAAiB,WAAW,MAAM,QAAQ,iBAAiB,MAAM,KAAK,IACjE,iBAAiB,KAAM,QACxB,CAAC;AAEP,gBAAU;AAAA,QACR,YAAY,WAAW,IAAI,CAAC,cAAc;AAAA,UACxC,IAAI,SAAS;AAAA,UACb,cAAc,SAAS,gBAAgB,SAAS,iBAAiB;AAAA,UACjE,SAAS,SAAS,WAAW;AAAA,UAC7B,MAAM,SAAS,QAAQ;AAAA,UACvB,YAAY,UAAU,SAAS,cAAc,SAAS,WAAW;AAAA,UACjE,WAAW,UAAU,SAAS,aAAa,SAAS,UAAU;AAAA,QAChE,EAAE;AAAA,QACF,OAAO,SAAS,IAAI,CAAC,aAAa;AAAA,UAChC,IAAI,QAAQ;AAAA,UACZ,MAAM,QAAQ;AAAA,UACd,cAAc,QAAQ,gBAAgB,QAAQ,kBAAkB;AAAA,UAChE,WAAW,UAAU,QAAQ,aAAa,QAAQ,UAAU;AAAA,QAC9D,EAAE;AAAA,QACF,QAAQ,WACL,IAAI,CAAC,WAAW;AACf,cAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,gBAAM,KAAK,OAAO,OAAO,OAAO,WAAW,OAAO,KAAK;AACvD,cAAI,CAAC,GAAI,QAAO;AAChB,gBAAM,WAAW,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AACzE,gBAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAChE,gBAAM,QAMF;AAAA,YACF;AAAA,YACA,aAAa;AAAA,YACb,cAAc,YAAY,SAAS,SAAS,GAAG,IAAI,WAAW;AAAA,YAC9D,cAAc,YAAY,CAAC,SAAS,SAAS,GAAG,IAAI,WAAW;AAAA,YAC/D,iBAAiB;AAAA,UACnB;AACA,iBAAO;AAAA,QACT,CAAC,EACA;AAAA,UACC,CAAC,UAMI,UAAU;AAAA,QACjB;AAAA,QACF,WAAW,cACR,IAAI,CAAC,YAAY;AAChB,cAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,gBAAM,KAAK,OAAO,QAAQ,OAAO,WAAW,QAAQ,KAAK;AACzD,cAAI,CAAC,GAAI,QAAO;AAChB,gBAAM,QAAQ,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAClE,gBAAM,QAKF;AAAA,YACF;AAAA,YACA,aAAa;AAAA,YACb,cAAc;AAAA,YACd,cAAc;AAAA,UAChB;AACA,iBAAO;AAAA,QACT,CAAC,EACA;AAAA,UACC,CAAC,UAKI,UAAU;AAAA,QACjB;AAAA,MACJ;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,QACJ,IAAI,QAAQ;AAAA,QACZ,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,QAC3D,aAAa,QAAQ,eAAe;AAAA,QACpC,QAAQ,QAAQ,UAAU;AAAA,QAC1B,YAAY,QAAQ,cAAc;AAAA,QAClC,iBAAiB,QAAQ,mBAAmB;AAAA,QAC5C,aAAa,QAAQ,eAAe;AAAA,QACpC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,aAAa,QAAQ,eAAe;AAAA,QACpC,aAAa,QAAQ,eAAe;AAAA,QACpC,iBAAiB,UAAU,QAAQ,eAAe;AAAA,QAClD,QAAQ,QAAQ,UAAU;AAAA,QAC1B,gBAAgB,QAAQ,kBAAkB;AAAA,QAC1C,UAAU,QAAQ,YAAY;AAAA,QAC9B,WAAW,UAAU,QAAQ,SAAS;AAAA,QACtC,WAAW,UAAU,QAAQ,SAAS;AAAA,MACxC;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAkBA,MAAM,mBAAmB,CAAC,UAA4B;AACpD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,WAAW,IAAI,SAAY;AAC5C;AAEA,MAAM,uBAAuB,EAC1B,OAAO;AAAA,EACN,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,2BAA2B;AAAA,EAC9D,mBAAmB,EAChB,WAAW,kBAAkB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,EACzD,SAAS,+EAA0E;AAAA,EACtF,SAAS,EACN,WAAW,kBAAkB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,EACjE;AAAA,IACC;AAAA,EACF;AACJ,CAAC,EACA;AAAA,EACC,CAAC,UAAU,QAAQ,MAAM,iBAAiB,MAAM,QAAQ,MAAM,OAAO;AAAA,EACrE;AAAA,IACE,SAAS;AAAA,IACT,MAAM,CAAC,mBAAmB;AAAA,EAC5B;AACF;AAIF,SAAS,2BAA2B,WAAmD;AACrF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,qBAAqB,OAAO,YAAY,IAAI,KAAK,SAAS;AACxE,MAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,EAAG,QAAO;AAC1C,SAAO,MAAM,YAAY;AAC3B;AAEA,SAAS,YAAY,OAAsD;AACzE,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MACJ,MAAM,SAAS,EACf,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AACb;AAEA,eAAe,kBACb,IACA,KACA,UACA,QAC8B;AAC9B,QAAM,QAAiC,EAAE,IAAI,QAAQ,UAAU,WAAW,KAAK;AAC/E,MAAI,IAAI,eAAgB,OAAM,iBAAiB,IAAI;AACnD,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACA,MAAI,CAAC,QAAQ,KAAK,aAAa,SAAU,QAAO;AAChD,MAAI,IAAI,kBAAkB,KAAK,mBAAmB,IAAI,eAAgB,QAAO;AAC7E,SAAO;AACT;AAEA,eAAe,kBACb,IACA,KACA,UACA,SACA,gBACuC;AACvC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,IACA,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACF;AAEA,MAAM,sBAAiD;AAAA,EACrD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,wBAAwB;AAAA,EAC3C,MAAM,CAAC,SAAS,WAAW;AAAA,EAC3B,YAAY;AAAA,EACZ,kBAAkB,OAAO,UAAU,QAA6D;AAC9F,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAA8B,qBAAqB,MAAM,QAAQ;AACvE,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,OAAO,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,MAAM;AACpE,QAAI,CAAC,KAAM,QAAO;AAClB,QAAI,cAAc,KAAK,UAAU;AACjC,QAAI,uBAAuB,KAAK,mBAAmB;AACnD,QAAI,0BAA0B,KAAK,iBAAiB;AACpD,QAAI,MAAM,mBAAmB;AAC3B,YAAM,iBAAiB,KAAK,kBAAkB,IAAI,kBAAkB;AACpE,YAAM,QAAQ,iBACV,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,mBAAmB,cAAc,IAClF;AACJ,6BAAuB,MAAM;AAC7B,gCAA0B,OAAO,SAAS,MAAM;AAAA,IAClD,WAAW,MAAM,SAAS;AACxB,oBAAc,MAAM;AAAA,IACtB;AACA,UAAM,eAAe,KAAK,UAAU;AACpC,UAAM,wBAAwB,KAAK,mBAAmB;AACtD,UAAM,2BAA2B,KAAK,iBAAiB;AACvD,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,YAAY;AAAA,MACZ,eAAe,2BAA2B,KAAK,SAAS;AAAA,MACxD,QAAQ;AAAA,QACN,QAAQ;AAAA,QACR,iBAAiB;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,iBAAiB;AAAA,MACnB;AAAA,MACA,SAAS;AAAA,QACP,aAAa;AAAA,UACX,QAAQ;AAAA,UACR,iBAAiB;AAAA,QACnB;AAAA,QACA,QAAQ;AAAA,UACN,GAAI,eAAe,EAAE,QAAQ,YAAY,YAAY,EAAE,IAAI,CAAC;AAAA,UAC5D,GAAI,2BAA2B,EAAE,iBAAiB,yBAAyB,IAAI,CAAC;AAAA,QAClF;AAAA,QACA,OAAO;AAAA,UACL,GAAI,cAAc,EAAE,QAAQ,YAAY,WAAW,EAAE,IAAI,CAAC;AAAA,UAC1D,GAAI,0BAA0B,EAAE,iBAAiB,wBAAwB,IAAI,CAAC;AAAA,QAChF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAA8B,qBAAqB,MAAM,QAAQ;AACvE,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,OAAO,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,MAAM;AACpE,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,SAAS,MAAM,MAAM,oCAAoC;AAAA,IAC3E;AACA,UAAM,iBAAiB,KAAK;AAC5B,QAAI,CAAC,gBAAgB;AACnB,YAAM,IAAI,MAAM,SAAS,MAAM,MAAM,8BAA8B;AAAA,IACrE;AAEA,UAAM,SAAS;AAAA,MACb,QAAQ,KAAK,UAAU;AAAA,MACvB,eAAe,KAAK,iBAAiB;AAAA,MACrC,iBAAiB,KAAK,mBAAmB;AAAA,IAC3C;AAEA,UAAM,OAAgC;AAAA,MACpC,IAAI,KAAK;AAAA,MACT;AAAA,MACA;AAAA,IACF;AACA,QAAI,MAAM,mBAAmB;AAC3B,YAAM,QAAQ,MAAM,kBAAkB,IAAI,KAAK,UAAU,MAAM,mBAAmB,cAAc;AAChG,UAAI,CAAC,OAAO;AACV,cAAM,IAAI,MAAM,2BAA2B;AAAA,MAC7C;AACA,WAAK,kBAAkB,MAAM;AAAA,IAC/B,WAAW,MAAM,SAAS;AACxB,WAAK,SAAS,MAAM;AAAA,IACtB;AAEA,UAAM,SAAS,2BAA2B,GAAwC;AAClF,UAAM,WAAW,MAAM,OAAO,IAAI;AAAA,MAChC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,0BAA0B,KAAK,EAAE,GAAG;AAAA,IACxE;AAEA,UAAM,QAAQ,MAAM,kBAAkB,IAAI,KAAK,UAAU,KAAK,EAAE;AAChE,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,aAAa;AAAA,MACb;AAAA,MACA,OAAO,QACH;AAAA,QACE,QAAQ,MAAM,UAAU;AAAA,QACxB,eAAe,MAAM,iBAAiB;AAAA,QACtC,iBAAiB,MAAM,mBAAmB;AAAA,MAC5C,IACA;AAAA,IACN;AAAA,EACF;AACF;AAEO,MAAM,eAA4C,CAAC,eAAe,aAAa,mBAAmB;AAEzG,IAAO,qBAAQ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/ai-tools/types.ts"],
4
- "sourcesContent": ["/**\n * Local AI tool shape for the customers module (Phase 1 WS-C, Step 3.9).\n *\n * The customers module declares its read-only tool pack directly as plain\n * objects so jest can load it without pulling `@open-mercato/ai-assistant`\n * into the core package's module graph. This mirrors the pattern used by\n * `packages/core/src/modules/inbox_ops/ai-tools.ts`. The shape is a strict\n * subset of `AiToolDefinition` from `@open-mercato/ai-assistant`; the\n * generator walks every module root for a default/aiTools export with this\n * shape.\n */\nimport type { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\n\nexport interface CustomersToolContext {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n apiKeySecret?: string\n sessionId?: string\n}\n\n/**\n * Shape returned by `loadBeforeRecord` on a mutation tool. Mirrors\n * `AiToolLoadBeforeSingleRecord` from `@open-mercato/ai-assistant/lib/types`;\n * the customers module deliberately does not import that package so we keep a\n * local prefix-compatible declaration (same rule as `CustomersAiToolDefinition`).\n */\nexport interface CustomersToolLoadBeforeSingleRecord {\n recordId: string\n entityType: string\n recordVersion: string | null\n before: Record<string, unknown>\n}\n\nexport interface CustomersAiToolDefinition<TInput = unknown, TOutput = unknown> {\n name: string\n displayName?: string\n description: string\n inputSchema: z.ZodType<TInput>\n requiredFeatures?: string[]\n tags?: string[]\n isMutation?: boolean\n /**\n * Marks the mutation as destructive \u2014 gates the call through the\n * approval card under `destructive-confirm-required` policy. Accepts a\n * static boolean (whole tool destructive) or a predicate\n * `(input) => boolean` evaluated at every call so a multi-operation\n * tool (e.g. `manage_deal_comment` with create/update/delete) gates\n * only the destructive branches. Default `false`. See\n * `@open-mercato/ai-assistant` framework `AiToolDefinition.isDestructive`\n * for the canonical definition; this surface mirrors the contract for\n * type-safe authoring inside the customers module.\n */\n isDestructive?: boolean | ((input: TInput) => boolean)\n maxCallsPerTurn?: number\n supportsAttachments?: boolean\n handler: (input: TInput, context: CustomersToolContext) => Promise<TOutput>\n loadBeforeRecord?: (\n input: TInput,\n context: CustomersToolContext,\n ) => Promise<CustomersToolLoadBeforeSingleRecord | null>\n}\n\nexport function assertTenantScope(ctx: CustomersToolContext): {\n tenantId: string\n organizationId: string | null\n} {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for customers.* tools')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n"],
5
- "mappings": "AAmEO,SAAS,kBAAkB,KAGhC;AACA,MAAI,CAAC,IAAI,UAAU;AACjB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AACtE;",
4
+ "sourcesContent": ["/**\n * Local AI tool shape for the customers module (Phase 1 WS-C, Step 3.9).\n *\n * The customers module declares its read-only tool pack directly as plain\n * objects so jest can load it without pulling `@open-mercato/ai-assistant`\n * into the core package's module graph. This mirrors the pattern used by\n * `packages/core/src/modules/inbox_ops/ai-tools.ts`. The shape is a strict\n * subset of `AiToolDefinition` from `@open-mercato/ai-assistant`; the\n * generator walks every module root for a default/aiTools export with this\n * shape.\n */\nimport type { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\n\nexport interface CustomersToolContext {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n apiKeySecret?: string\n sessionId?: string\n}\n\n/**\n * Shape returned by `loadBeforeRecord` on a mutation tool. Mirrors\n * `AiToolLoadBeforeSingleRecord` from `@open-mercato/ai-assistant/lib/types`;\n * the customers module deliberately does not import that package so we keep a\n * local prefix-compatible declaration (same rule as `CustomersAiToolDefinition`).\n */\nexport interface CustomersToolLoadBeforeSingleRecord {\n recordId: string\n entityType: string\n recordVersion: string | null\n before: Record<string, unknown>\n after?: Record<string, unknown>\n display?: {\n fieldLabels?: Record<string, string>\n before?: Record<string, unknown>\n after?: Record<string, unknown>\n }\n}\n\nexport interface CustomersAiToolDefinition<TInput = unknown, TOutput = unknown> {\n name: string\n displayName?: string\n description: string\n inputSchema: z.ZodType<TInput>\n requiredFeatures?: string[]\n tags?: string[]\n isMutation?: boolean\n /**\n * Marks the mutation as destructive \u2014 gates the call through the\n * approval card under `destructive-confirm-required` policy. Accepts a\n * static boolean (whole tool destructive) or a predicate\n * `(input) => boolean` evaluated at every call so a multi-operation\n * tool (e.g. `manage_deal_comment` with create/update/delete) gates\n * only the destructive branches. Default `false`. See\n * `@open-mercato/ai-assistant` framework `AiToolDefinition.isDestructive`\n * for the canonical definition; this surface mirrors the contract for\n * type-safe authoring inside the customers module.\n */\n isDestructive?: boolean | ((input: TInput) => boolean)\n maxCallsPerTurn?: number\n supportsAttachments?: boolean\n handler: (input: TInput, context: CustomersToolContext) => Promise<TOutput>\n loadBeforeRecord?: (\n input: TInput,\n context: CustomersToolContext,\n ) => Promise<CustomersToolLoadBeforeSingleRecord | null>\n}\n\nexport function assertTenantScope(ctx: CustomersToolContext): {\n tenantId: string\n organizationId: string | null\n} {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for customers.* tools')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n"],
5
+ "mappings": "AAyEO,SAAS,kBAAkB,KAGhC;AACA,MAAI,CAAC,IAAI,UAAU;AACjB,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,SAAO,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AACtE;",
6
6
  "names": []
7
7
  }
@@ -380,7 +380,16 @@ async function GET(_req, ctx) {
380
380
  scopedOrganizations: Array.isArray(scope?.filterIds) ? scope.filterIds.length : 0
381
381
  });
382
382
  const em = container.resolve("em");
383
- const person = await findOneWithDecryption(em, CustomerEntity, { id: parse.data.id, kind: "person", deletedAt: null }, {}, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null });
383
+ const person = await findOneWithDecryption(
384
+ em,
385
+ CustomerEntity,
386
+ { id: parse.data.id, kind: "person", deletedAt: null },
387
+ {},
388
+ {
389
+ tenantId: scope?.tenantId ?? auth.tenantId ?? null,
390
+ organizationId: scope?.selectedId ?? auth.orgId ?? null
391
+ }
392
+ );
384
393
  profiler.mark("person_loaded", { found: !!person });
385
394
  if (!person) {
386
395
  statusCode = 404;