@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.219

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 (177) hide show
  1. package/README.md +2 -2
  2. package/package.json +17 -17
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/activity/ai-activity-actions.js +8 -1
  6. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  7. package/src/lib/components/ai-assist-provider.component.js +2 -2
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-brief-dialog.component.js +3 -3
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-experiment-cards.component.js +1 -1
  12. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.d.ts +8 -0
  14. package/src/lib/components/ai-job-plan.component.js +21 -4
  15. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  16. package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
  17. package/src/lib/components/ai-permissions-on-host.component.js +12 -3
  18. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  19. package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
  20. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  21. package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
  22. package/src/lib/components/ai-product-import-option.component.js.map +1 -1
  23. package/src/lib/components/ai-product-zones.d.ts +124 -0
  24. package/src/lib/components/ai-product-zones.js +32 -0
  25. package/src/lib/components/ai-product-zones.js.map +1 -0
  26. package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
  27. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  28. package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
  29. package/src/lib/components/ai-products-proposals.component.js.map +1 -1
  30. package/src/lib/components/ai-seo-audit-card.component.js +35 -92
  31. package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
  32. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js +21 -15
  35. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.js +2 -2
  37. package/src/lib/components/assist-panel.component.js.map +1 -1
  38. package/src/lib/components/assist-signals-page.component.js +2 -2
  39. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  40. package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
  41. package/src/lib/jobs/ai-email-bindings.js +75 -42
  42. package/src/lib/jobs/ai-email-bindings.js.map +1 -1
  43. package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
  44. package/src/lib/jobs/ai-job-campaign-step.js +5 -10
  45. package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-component-step.js +9 -3
  47. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-drafts.js +1 -1
  49. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  50. package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
  51. package/src/lib/jobs/ai-job-email-step.js +1 -3
  52. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  53. package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
  54. package/src/lib/jobs/ai-job-experiment-step.js +1 -1
  55. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  56. package/src/lib/jobs/ai-job-generation.d.ts +1 -1
  57. package/src/lib/jobs/ai-job-generation.js +10 -2
  58. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  59. package/src/lib/jobs/ai-job-layout-step.js +1 -1
  60. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  61. package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
  62. package/src/lib/jobs/ai-job-page-sections.js +7 -4
  63. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  64. package/src/lib/jobs/ai-job-page-step.js +7 -2
  65. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  66. package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
  67. package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
  68. package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
  69. package/src/lib/jobs/ai-job-plan-step.js +29 -17
  70. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  71. package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
  72. package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
  73. package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
  74. package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
  75. package/src/lib/jobs/ai-job-products-step.js +54 -32
  76. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  77. package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
  78. package/src/lib/jobs/ai-job-seo-step.js +35 -108
  79. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  80. package/src/lib/jobs/ai-job-template-step.js +1 -0
  81. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  82. package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
  83. package/src/lib/jobs/ai-job-theme-step.js +1 -1
  84. package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
  85. package/src/lib/jobs/ai-job-workflow-step.js +17 -9
  86. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  87. package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
  88. package/src/lib/jobs/ai-workflow-records.js +60 -27
  89. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  90. package/src/lib/model/ai-allotments.d.ts +11 -3
  91. package/src/lib/model/ai-allotments.js +21 -4
  92. package/src/lib/model/ai-allotments.js.map +1 -1
  93. package/src/lib/model/ai-automation-outline.d.ts +21 -2
  94. package/src/lib/model/ai-automation-outline.js.map +1 -1
  95. package/src/lib/model/ai-build-plan.d.ts +57 -0
  96. package/src/lib/model/ai-build-plan.js +116 -7
  97. package/src/lib/model/ai-build-plan.js.map +1 -1
  98. package/src/lib/model/ai-seo.d.ts +11 -51
  99. package/src/lib/model/ai-seo.js +0 -17
  100. package/src/lib/model/ai-seo.js.map +1 -1
  101. package/src/lib/model/ai-site-inventory.js +1 -1
  102. package/src/lib/model/ai-site-inventory.js.map +1 -1
  103. package/src/lib/model/ai-template-subjects.js +2 -2
  104. package/src/lib/model/ai-template-subjects.js.map +1 -1
  105. package/src/lib/model/ai-theme-proposal.d.ts +1 -1
  106. package/src/lib/model/ai-theme-proposal.js +1 -1
  107. package/src/lib/model/ai-theme-proposal.js.map +1 -1
  108. package/src/lib/model/ai-workflow-job.d.ts +1 -3
  109. package/src/lib/model/ai-workflow-job.js +1 -2
  110. package/src/lib/model/ai-workflow-job.js.map +1 -1
  111. package/src/lib/plan-entitlements.d.ts +149 -0
  112. package/src/lib/plan-entitlements.js +168 -0
  113. package/src/lib/plan-entitlements.js.map +1 -0
  114. package/src/lib/plugin.js +1 -1
  115. package/src/lib/plugin.js.map +1 -1
  116. package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
  117. package/src/lib/runtime/ai-doctrine-validators.js +178 -21
  118. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  119. package/src/lib/runtime/ai-doctrine.js +5 -5
  120. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  121. package/src/lib/runtime/ai-eval-live.js +29 -14
  122. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  123. package/src/lib/runtime/ai-eval.d.ts +8 -1
  124. package/src/lib/runtime/ai-eval.js +29 -2
  125. package/src/lib/runtime/ai-eval.js.map +1 -1
  126. package/src/lib/runtime/ai-node-tree.js +3 -3
  127. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  128. package/src/lib/runtime/ai-palette.d.ts +6 -0
  129. package/src/lib/runtime/ai-palette.generated.js +15 -12
  130. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  131. package/src/lib/runtime/ai-palette.js +7 -1
  132. package/src/lib/runtime/ai-palette.js.map +1 -1
  133. package/src/lib/runtime/seo-audit.d.ts +13 -63
  134. package/src/lib/runtime/seo-audit.js +22 -207
  135. package/src/lib/runtime/seo-audit.js.map +1 -1
  136. package/src/lib/runtime/site-inventory.js +1 -1
  137. package/src/lib/runtime/site-inventory.js.map +1 -1
  138. package/src/lib/server/ai-assist-prompts.js +3 -3
  139. package/src/lib/server/ai-assist-prompts.js.map +1 -1
  140. package/src/lib/server/assist-chat.js +11 -10
  141. package/src/lib/server/assist-chat.js.map +1 -1
  142. package/src/lib/server/assist-view-context.js +23 -23
  143. package/src/lib/server/assist-view-context.js.map +1 -1
  144. package/src/lib/server.d.ts +1 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
  147. package/src/lib/testing/stand-in-automation-index.js +102 -0
  148. package/src/lib/testing/stand-in-automation-index.js.map +1 -0
  149. package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
  150. package/src/lib/testing/stand-in-list-send-time.js +77 -0
  151. package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
  152. package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
  153. package/src/lib/testing/stand-in-pipeline-index.js +59 -0
  154. package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
  155. package/src/lib/testing/stand-in-product-index.d.ts +20 -0
  156. package/src/lib/testing/stand-in-product-index.js +105 -0
  157. package/src/lib/testing/stand-in-product-index.js.map +1 -0
  158. package/src/lib/tools/ai-component-tool.js +2 -2
  159. package/src/lib/tools/ai-component-tool.js.map +1 -1
  160. package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
  161. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  162. package/src/lib/tools/ai-seo-tool.d.ts +6 -18
  163. package/src/lib/tools/ai-seo-tool.js +7 -38
  164. package/src/lib/tools/ai-seo-tool.js.map +1 -1
  165. package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
  166. package/src/lib/tools/ai-workflow-tool.js +1 -1
  167. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  168. package/src/lib/usage/ai-allotment-alerts.js +8 -1
  169. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
  170. package/src/lib/usage/assist-free-taste.js +2 -1
  171. package/src/lib/usage/assist-free-taste.js.map +1 -1
  172. package/src/lib/usage-axes.d.ts +39 -0
  173. package/src/lib/usage-axes.js +70 -0
  174. package/src/lib/usage-axes.js.map +1 -0
  175. package/src/lib/runtime/seo-page-facts.d.ts +0 -61
  176. package/src/lib/runtime/seo-page-facts.js +0 -150
  177. package/src/lib/runtime/seo-page-facts.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-workflow-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n CONTACT_TAG_MAX_LENGTH,\n FLOW_TIMED_OUT_FIELD,\n FLOW_WAIT_MAX_MINUTES,\n FLOW_WAIT_MIN_MINUTES,\n HOST_ACTION_STEP_LABELS,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n type TriggerConditionOp,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_KINDS,\n CRM_TASK_KINDS,\n CRM_TASK_MAX_DUE_DAYS,\n type ContactLifecycleStage,\n type CrmActivityKind,\n type CrmTaskKind,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n HOST_EVENT_PAYLOAD_KEYS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/workflows'\nimport {\n AI_AUTOMATION_NEED_LABELS,\n AI_AUTOMATION_STEP_NEEDS,\n AI_AUTOMATION_STEP_TYPES,\n AI_AUTOMATION_TRIGGER_NEEDS,\n AI_AUTOMATION_UNSUPPORTED,\n AI_AUTOMATION_UNSUPPORTED_NEED,\n aiAutomationTriggerLabel,\n type AiAutomationCapabilities,\n type AiAutomationStepType,\n type AiAutomationUnsupported,\n} from '../model/ai-workflow-job'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport { HOSTILE_TEXT } from '../runtime/ai-node-tree'\n\n/**\n * The two tools a `workflow` job answers through (AGL-2919).\n *\n * `submit_automation` carries one automation in the Actions editor's own\n * vocabulary — a trigger from the host events, conditions over the fields the\n * trigger carries, and steps from the automation step types — with every\n * record it names written as WORDS. The step resolves those words against the\n * site after the answer, so the model is never shown the site's lists,\n * campaigns, workflows, webhooks or pipeline stages, and never writes an id it\n * could only have invented.\n *\n * `submit_explanation` carries a plain-words account of one automation, or of\n * why one of its runs failed.\n *\n * Both schemas are strict: every object forbids extra keys and requires every\n * key it has, and nothing in either is a `null`. Where a step may go without\n * something, the absence is an empty list or an empty string: a step that\n * always runs has an empty `when`, a notEmpty condition compares against\n * nothing, and an answer that builds an automation leaves `unsupported`\n * empty. Bounds a strict schema cannot state are stated in the descriptions\n * and held by the readers below, whose refusals name the field so the one\n * re-ask can fix it.\n *\n * A provider that constrains decoding compiles every strict schema of a\n * request into a grammar, and refuses one whose grammar grows past its size\n * (`AiToolSchemaLimits`). So the steps' union carries as little repetition as\n * a grammar can be made to hold:\n *\n * - a step is its guard and its action, `{ when, action }`, so the condition\n * is compiled once for the whole union instead of once per variant;\n * - a variant serves every step type that carries the same fields, named for\n * what they hold: the six steps that name a record share `reference`, and\n * the three that carry one line share `text`;\n * - every field stays required and every vocabulary stays an enum, so the\n * model is still decoded into the Actions editor's words.\n *\n * Ten variants carry all seventeen step types, and the reader below maps the\n * shared fields back onto the step's own key.\n */\n\nexport const AI_AUTOMATION_TOOL_NAME = 'submit_automation'\nexport const AI_WORKFLOW_EXPLANATION_TOOL_NAME = 'submit_explanation'\n\n/** The longest automation name the Actions editor stores. */\nexport const AI_AUTOMATION_NAME_MAX_CHARS = 60\n/** The longest subject the executor sends. */\nexport const AI_AUTOMATION_SUBJECT_MAX_CHARS = 200\n/** The longest email text the executor sends. */\nexport const AI_AUTOMATION_BODY_MAX_CHARS = 5_000\n/** The longest title, message or record name a step carries. */\nexport const AI_AUTOMATION_TEXT_MAX_CHARS = 300\n/** The longest words naming a record. */\nexport const AI_AUTOMATION_REFERENCE_MAX_CHARS = 100\n/** The longest condition value. */\nexport const AI_AUTOMATION_VALUE_MAX_CHARS = 200\n/** The most notes an answer keeps, and the longest. */\nexport const AI_AUTOMATION_NOTES_MAX = 3\nexport const AI_AUTOMATION_NOTE_MAX_CHARS = 300\n/**\n * The longest a whole answer may be, written out as JSON: what the routing\n * ceiling for `job.workflow` holds at three characters a token with as much\n * again to think in (`ai-job-workflow-step.spec.ts` measures it). Ten steps\n * with two emails of a paragraph or two each come to about 2,400, each step\n * carrying only its own fields.\n */\nexport const AI_AUTOMATION_ANSWER_MAX_CHARS = 6_000\n\n/** The explanation's bounds. */\nexport const AI_WORKFLOW_SUMMARY_MAX_CHARS = 400\nexport const AI_WORKFLOW_POINTS_MAX = 8\nexport const AI_WORKFLOW_SUGGESTIONS_MAX = 4\nexport const AI_WORKFLOW_LINE_MAX_CHARS = 300\n\nconst SEVERITIES = ['info', 'success', 'warning', 'error'] as const\ntype Severity = (typeof SEVERITIES)[number]\n\ntype Schema = Record<string, unknown>\n\n/**\n * What the schema defines once and refers to where it is used: a condition,\n * which the trigger and every step's guard share, and the guard itself.\n */\nconst DEFINITIONS: Schema = {\n condition: {\n type: 'object',\n additionalProperties: false,\n required: ['field', 'op', 'value'],\n properties: {\n field: {\n type: 'string',\n description: 'The field of the event the condition reads, as the trigger lists its fields.',\n },\n op: { type: 'string', enum: [...TRIGGER_CONDITION_OPS] },\n value: {\n type: 'string',\n description: 'What equals or contains compares against; empty for notEmpty.',\n },\n },\n },\n event: {\n type: 'string',\n enum: [...HOST_EVENT_TYPES],\n description: 'One of the things that happen on a site.',\n },\n // A list rather than a condition or `null`: a step goes without one by an\n // empty list, which costs no union.\n when: {\n type: 'array',\n description: 'At most one condition: the step runs only when it holds. Empty when the step always runs.',\n items: { $ref: '#/$defs/condition' },\n },\n}\n\n/** The steps that name one of the site's records, which they all name the same way. */\nconst REFERENCE_STEPS = [\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n] as const satisfies readonly AiAutomationStepType[]\n\n/** The steps whose only field is the one line they carry. */\nconst TEXT_STEPS = [\n 'notifyAdmins',\n 'addContactTag',\n 'assignContactOwner',\n] as const satisfies readonly AiAutomationStepType[]\n\n/** The key each `reference` step fills in the answer the reader gives back. */\nconst REFERENCE_KEY = {\n enrollList: 'list',\n assignCampaign: 'campaign',\n runWorkflow: 'workflow',\n datasetAppend: 'dataset',\n updateDataset: 'dataset',\n webhookPost: 'webhook',\n} as const satisfies Readonly<Record<(typeof REFERENCE_STEPS)[number], keyof AiAutomationAnswerStep>>\n\n/**\n * The key each step's one line fills in the answer the reader gives back. Six\n * step types carry one line between them, so the schema asks for all six\n * under `text` and the reader puts each back where the automation keeps it.\n */\nconst TEXT_KEY = {\n notifyAdmins: 'title',\n addContactTag: 'tag',\n assignContactOwner: 'owner',\n siteAlert: 'message',\n createCrmTask: 'title',\n logCrmActivity: 'body',\n} as const satisfies Readonly<Partial<Record<AiAutomationStepType, keyof AiAutomationAnswerStep>>>\n\nconst minutes = (what: string): Schema => ({\n type: 'integer',\n description: `${what}, in whole minutes from ${FLOW_WAIT_MIN_MINUTES} to ${FLOW_WAIT_MAX_MINUTES}; an hour is 60 and a day 1440.`,\n})\n\n/**\n * One variant of the steps' union: the step types it serves, and the fields\n * every one of them carries, each required and never nullable. A type belongs\n * to exactly one variant, so the type a step names settles which fields it\n * writes.\n */\nfunction stepVariant(\n types: readonly AiAutomationStepType[],\n fields: Readonly<Record<string, Schema>>,\n): Schema {\n return {\n type: 'object',\n additionalProperties: false,\n required: ['type', ...Object.keys(fields)],\n properties: { type: { type: 'string', enum: [...types] }, ...fields },\n }\n}\n\n/** Every step type an automation may carry, grouped by the fields it needs. */\nexport const AI_AUTOMATION_STEP_VARIANTS: readonly Schema[] = [\n stepVariant([...REFERENCE_STEPS], {\n reference: {\n type: 'string',\n description:\n 'The record the step acts on — the email list, campaign, workflow, dataset or outbound webhook — in the words of the description, such as \"newsletter\". Never an id.',\n },\n }),\n stepVariant([...TEXT_STEPS], {\n text: {\n type: 'string',\n description: `The one line the step carries: for notifyAdmins what the admins read, for addContactTag the tag (at most ${CONTACT_TAG_MAX_LENGTH} characters), and for assignContactOwner \"round robin\" or the teammate’s email address the description gives.`,\n },\n }),\n stepVariant(['sendEmail'], {\n subject: { type: 'string', description: 'The subject line.' },\n body: { type: 'string', description: 'The email, as plain text.' },\n toField: {\n type: 'string',\n description: 'The event field holding the address: \"email\" unless the description names another.',\n },\n }),\n stepVariant(['siteAlert'], {\n text: { type: 'string', description: 'What the visitor reads.' },\n severity: { type: 'string', enum: [...SEVERITIES] },\n }),\n stepVariant(['wait'], { minutes: minutes('How long to wait') }),\n stepVariant(['waitForEvent'], {\n event: { $ref: '#/$defs/event' },\n minutes: minutes('When to give up'),\n }),\n stepVariant(['exitFlow'], {}),\n stepVariant(['setContactStage'], { stage: { type: 'string', enum: [...CONTACT_LIFECYCLE_STAGES] } }),\n stepVariant(['createCrmTask'], {\n text: { type: 'string', description: 'The task.' },\n taskKind: { type: 'string', enum: [...CRM_TASK_KINDS] },\n dueInDays: {\n type: 'integer',\n description: `Days from the run, 0 for today, at most ${CRM_TASK_MAX_DUE_DAYS}.`,\n },\n }),\n stepVariant(['logCrmActivity'], {\n activityKind: { type: 'string', enum: [...CRM_ACTIVITY_KINDS] },\n text: { type: 'string', description: 'What happened.' },\n }),\n]\n\n/** The automation tool. The same bytes on every request, so it sits inside the cached prefix. */\nexport function aiAutomationTool(): AiTool {\n return {\n name: AI_AUTOMATION_TOOL_NAME,\n description: `Submit one automation: its trigger, the conditions on it, and its steps in order. Written out, the whole automation is at most ${AI_AUTOMATION_ANSWER_MAX_CHARS} characters, so keep its emails short.`,\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'trigger', 'steps', 'notes', 'unsupported'],\n properties: {\n name: {\n type: 'string',\n description: `A short name for the automation, at most ${AI_AUTOMATION_NAME_MAX_CHARS} characters.`,\n },\n trigger: {\n type: 'object',\n additionalProperties: false,\n required: ['event', 'conditions', 'combinator'],\n properties: {\n event: { $ref: '#/$defs/event' },\n conditions: {\n type: 'array',\n description: `What the event must meet: at most ${ACTION_MAX_CONDITIONS} conditions, empty when it runs every time.`,\n items: { $ref: '#/$defs/condition' },\n },\n combinator: { type: 'string', enum: [...TRIGGER_COMBINATORS] },\n },\n },\n steps: {\n type: 'array',\n description: `The steps in order, at most ${ACTION_MAX_STEPS}. Each is when it runs and what it does, and the action carries only the fields its type takes. Empty only with unsupported.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['when', 'action'],\n properties: {\n when: { $ref: '#/$defs/when' },\n action: { anyOf: AI_AUTOMATION_STEP_VARIANTS },\n },\n },\n },\n notes: {\n type: 'array',\n description: `What the person must decide that the automation cannot hold; at most ${AI_AUTOMATION_NOTES_MAX} short sentences.`,\n items: { type: 'string' },\n },\n unsupported: {\n type: 'string',\n enum: ['', ...AI_AUTOMATION_UNSUPPORTED],\n description: 'Why no automation can be built from this description; empty when one is built.',\n },\n },\n $defs: DEFINITIONS,\n },\n }\n}\n\n/** The explanation tool. */\nexport function aiWorkflowExplanationTool(): AiTool {\n return {\n name: AI_WORKFLOW_EXPLANATION_TOOL_NAME,\n description: 'Submit the explanation, in plain words for someone who does not write code.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['summary', 'points', 'suggestions'],\n properties: {\n summary: {\n type: 'string',\n description: `One or two sentences, at most ${AI_WORKFLOW_SUMMARY_MAX_CHARS} characters.`,\n },\n points: {\n type: 'array',\n description: `At most ${AI_WORKFLOW_POINTS_MAX}, one sentence each.`,\n items: { type: 'string' },\n },\n suggestions: {\n type: 'array',\n description: `At most ${AI_WORKFLOW_SUGGESTIONS_MAX}, one sentence each; empty when there is nothing to suggest.`,\n items: { type: 'string' },\n },\n },\n },\n }\n}\n\n// ── Reading an automation ─────────────────────────────────────────────────\n\nexport interface AiAutomationCondition {\n field: string\n op: TriggerConditionOp\n /** `null` for `notEmpty`. */\n value: string | null\n}\n\n/** One step as the reader gives it back, every field the step does not use `null`. */\nexport interface AiAutomationAnswerStep {\n type: AiAutomationStepType\n when: AiAutomationCondition | null\n list: string | null\n campaign: string | null\n workflow: string | null\n webhook: string | null\n dataset: string | null\n subject: string | null\n body: string | null\n toField: string | null\n title: string | null\n message: string | null\n severity: Severity | null\n minutes: number | null\n event: HostEventType | null\n stage: ContactLifecycleStage | null\n tag: string | null\n owner: string | null\n taskKind: CrmTaskKind | null\n dueInDays: number | null\n activityKind: CrmActivityKind | null\n}\n\nexport interface AiAutomationAnswer {\n name: string\n trigger: {\n event: HostEventType\n conditions: AiAutomationCondition[]\n combinator: TriggerCombinator\n }\n steps: AiAutomationAnswerStep[]\n notes: string[]\n unsupported: AiAutomationUnsupported | null\n}\n\n/** The codes that say an answer could not be read as an automation at all. */\nexport const AI_AUTOMATION_UNREADABLE_CODES: readonly string[] = [\n 'automation-shape',\n 'automation-trigger',\n 'automation-step-type',\n]\n\n/** The codes that say an answer is larger than an automation may be. */\nexport const AI_AUTOMATION_OVERSIZE_CODES: readonly string[] = [\n 'automation-too-many-steps',\n 'automation-too-many-conditions',\n 'automation-too-long',\n]\n\n/** The address field a step reads when it names none. */\nconst DEFAULT_TO_FIELD = 'email'\nconst FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\nconst EMAIL_ADDRESS = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst ROUND_ROBIN = /^\\s*round[\\s-]*robin\\s*$/i\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** A string trimmed to one space between words, or `null` for none. */\nfunction textOf(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const clean = value.replace(/[ \\t]+/g, ' ').trim()\n return clean ? clean : null\n}\n\n/** The fields a trigger's conditions and its steps' guards may read. */\nfunction fieldsFor(event: HostEventType): readonly string[] | null {\n // A form submission carries every field the form collects, which no fixed\n // list names, so any field name is readable on it.\n if (event === 'formSubmission') return null\n const keys = HOST_EVENT_PAYLOAD_KEYS[event]\n return keys?.length ? keys : null\n}\n\ninterface Reader {\n violations: AiDoctrineViolation[]\n offending: Array<[string, unknown]>\n}\n\nfunction refuse(\n reader: Reader,\n code: string,\n message: string,\n path: string,\n value: unknown,\n): void {\n reader.violations.push({ rule: null, code, message, paths: [path] })\n if (reader.offending.length < 6) reader.offending.push([path, value])\n}\n\nfunction readCondition(\n reader: Reader,\n raw: unknown,\n path: string,\n event: HostEventType,\n guard: boolean,\n): AiAutomationCondition | null {\n if (!isRecord(raw)) {\n refuse(reader, 'automation-shape', 'A condition is not an object with a field, an op and a value.', path, raw)\n return null\n }\n const field = textOf(raw['field'])\n const op = raw['op']\n if (!field || !FIELD_NAME.test(field)) {\n refuse(reader, 'automation-condition', 'Name the event field the condition reads.', path, raw)\n return null\n }\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n refuse(reader, 'automation-condition', `The condition on \"${field}\" needs equals, contains or notEmpty.`, path, raw)\n return null\n }\n if (event === 'formSubmission' && field === 'formId') {\n refuse(\n reader,\n 'automation-condition',\n 'A form submission names its form by formName, the form’s name as the site lists it.',\n path,\n raw,\n )\n return null\n }\n // A step's guard may also read whether the wait before it ran out of time.\n const readable = fieldsFor(event)\n if (readable && !readable.includes(field) && !(guard && field === FLOW_TIMED_OUT_FIELD)) {\n refuse(\n reader,\n 'automation-condition',\n `The trigger \"${aiAutomationTriggerLabel(event)}\" does not carry \"${field}\". It carries: ${readable.join(', ')}.`,\n path,\n raw,\n )\n return null\n }\n const value = textOf(raw['value'])\n if (op !== 'notEmpty' && !value) {\n refuse(reader, 'automation-condition', `The condition on \"${field}\" needs the value it compares against.`, path, raw)\n return null\n }\n if (value && value.length > AI_AUTOMATION_VALUE_MAX_CHARS) {\n refuse(reader, 'automation-too-long', `The value the condition on \"${field}\" compares against is too long.`, path, raw)\n return null\n }\n if (field === 'lifecycleStage' || field === 'previousStage') {\n const stage = value ? lifecycleStageOf(value) : null\n if (op !== 'notEmpty' && !stage) {\n refuse(\n reader,\n 'automation-condition',\n `A lifecycle stage is one of: ${CONTACT_LIFECYCLE_STAGES.join(', ')}.`,\n path,\n raw,\n )\n return null\n }\n return { field, op: op as TriggerConditionOp, value: op === 'notEmpty' ? null : stage }\n }\n return { field, op: op as TriggerConditionOp, value: op === 'notEmpty' ? null : value }\n}\n\n/** A lifecycle stage by its id or its label, case aside. */\nexport function lifecycleStageOf(value: string): ContactLifecycleStage | null {\n const wanted = value.trim().toLowerCase()\n return (\n CONTACT_LIFECYCLE_STAGES.find(\n (stage) => stage === wanted || CONTACT_LIFECYCLE_STAGE_LABELS[stage].toLowerCase() === wanted,\n ) ?? null\n )\n}\n\nfunction requireText(\n reader: Reader,\n step: Record<string, unknown>,\n key: string,\n max: number,\n path: string,\n what: string,\n): string | null {\n const value = textOf(step[key])\n if (!value) {\n refuse(reader, 'automation-step-field', `${what} needs ${key}.`, path, step)\n return null\n }\n if (value.length > max) {\n refuse(reader, 'automation-too-long', `${what}: ${key} is longer than ${max} characters.`, path, step)\n return null\n }\n if (HOSTILE_TEXT.test(value)) {\n refuse(reader, 'automation-markup', `${what}: write ${key} as plain text, with no HTML or script.`, path, step)\n return null\n }\n return value\n}\n\nfunction wholeNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isInteger(value) ? value : null\n}\n\nfunction readStep(\n reader: Reader,\n raw: unknown,\n index: number,\n event: HostEventType,\n capabilities: AiAutomationCapabilities,\n): AiAutomationAnswerStep | null {\n const path = `steps[${index}]`\n if (!isRecord(raw)) {\n refuse(reader, 'automation-shape', `Step ${index + 1} is not an object.`, path, raw)\n return null\n }\n // A step is its guard and its action: `{ when, action }`. An answer that\n // writes the action's own fields beside `when` reads the same, so a model\n // that flattens the step does not spend the one re-ask.\n const action = raw['action']\n const fields: Record<string, unknown> = isRecord(action) ? { ...action, when: raw['when'] } : raw\n const type = fields['type']\n if (!(AI_AUTOMATION_STEP_TYPES as readonly unknown[]).includes(type)) {\n refuse(\n reader,\n 'automation-step-type',\n `Step ${index + 1} is not one of the steps offered: ${AI_AUTOMATION_STEP_TYPES.join(', ')}.`,\n path,\n fields,\n )\n return null\n }\n const stepType = type as AiAutomationStepType\n const what = `Step ${index + 1} (${HOST_ACTION_STEP_LABELS[stepType]})`\n const need = AI_AUTOMATION_STEP_NEEDS[stepType]\n if (need && !capabilities[need]) {\n refuse(\n reader,\n 'automation-plan',\n `${what} needs ${AI_AUTOMATION_NEED_LABELS[need]}, which this workspace does not have. Leave it out, or answer with unsupported \"needs-${need}\".`,\n path,\n fields,\n )\n return null\n }\n const before = reader.violations.length\n const step: AiAutomationAnswerStep = {\n type: stepType,\n when: null,\n list: null,\n campaign: null,\n workflow: null,\n webhook: null,\n dataset: null,\n subject: null,\n body: null,\n toField: null,\n title: null,\n message: null,\n severity: null,\n minutes: null,\n event: null,\n stage: null,\n tag: null,\n owner: null,\n taskKind: null,\n dueInDays: null,\n activityKind: null,\n }\n // A step's `when` is a list of at most one condition; a lone condition, or\n // none, reads as the list it stands for.\n const when = fields['when']\n const guards = Array.isArray(when) ? when : when === null || when === undefined ? [] : [when]\n if (guards.length > 1) {\n refuse(reader, 'automation-condition', `${what}: when holds one condition at most.`, `${path}.when`, when)\n } else if (guards.length === 1) {\n step.when = readCondition(reader, guards[0], `${path}.when`, event, true)\n }\n // The six steps that name a record share `reference`, and the three that\n // carry one line share `text`; an answer that writes the step's own key\n // instead — `list`, `tag`, `owner` — reads the same.\n const shared = (key: 'reference' | 'text', own: keyof AiAutomationAnswerStep): unknown =>\n fields[key] ?? fields[own]\n const own = TEXT_KEY[stepType as keyof typeof TEXT_KEY]\n const text: Record<string, unknown> = own ? { text: shared('text', own) } : fields\n switch (stepType) {\n case 'sendEmail': {\n step.subject = requireText(reader, fields, 'subject', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n step.body = requireText(reader, fields, 'body', AI_AUTOMATION_BODY_MAX_CHARS, path, what)\n const toField = textOf(fields['toField'])\n if (toField && !FIELD_NAME.test(toField)) {\n refuse(reader, 'automation-step-field', `${what}: toField names the event field holding the address.`, path, fields)\n } else {\n step.toField = toField && toField !== DEFAULT_TO_FIELD ? toField : null\n }\n break\n }\n case 'notifyAdmins':\n step.title = requireText(reader, text, 'text', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n break\n case 'enrollList':\n case 'assignCampaign':\n case 'runWorkflow':\n case 'webhookPost':\n case 'datasetAppend':\n case 'updateDataset':\n step[REFERENCE_KEY[stepType]] = requireText(\n reader,\n { reference: shared('reference', REFERENCE_KEY[stepType]) },\n 'reference',\n AI_AUTOMATION_REFERENCE_MAX_CHARS,\n path,\n what,\n )\n break\n case 'siteAlert': {\n step.message = requireText(reader, text, 'text', AI_AUTOMATION_TEXT_MAX_CHARS, path, what)\n const severity = fields['severity']\n step.severity = (SEVERITIES as readonly unknown[]).includes(severity) ? (severity as Severity) : 'info'\n break\n }\n case 'wait':\n case 'waitForEvent': {\n const minutes = wholeNumber(fields['minutes'])\n if (minutes === null || minutes < FLOW_WAIT_MIN_MINUTES || minutes > FLOW_WAIT_MAX_MINUTES) {\n refuse(\n reader,\n 'automation-step-field',\n `${what} needs minutes, a whole number from ${FLOW_WAIT_MIN_MINUTES} to ${FLOW_WAIT_MAX_MINUTES}.`,\n path,\n fields,\n )\n } else {\n step.minutes = minutes\n }\n if (stepType === 'waitForEvent') {\n const waited = fields['event']\n if (!(HOST_EVENT_TYPES as readonly unknown[]).includes(waited)) {\n refuse(reader, 'automation-step-field', `${what} needs the event it waits for.`, path, fields)\n } else {\n step.event = waited as HostEventType\n }\n }\n break\n }\n case 'exitFlow':\n break\n case 'setContactStage': {\n const stage = fields['stage']\n if (!(CONTACT_LIFECYCLE_STAGES as readonly unknown[]).includes(stage)) {\n refuse(reader, 'automation-step-field', `${what} needs the lifecycle stage to set.`, path, fields)\n } else {\n step.stage = stage as ContactLifecycleStage\n }\n break\n }\n case 'addContactTag':\n step.tag = requireText(reader, text, 'text', CONTACT_TAG_MAX_LENGTH, path, what)\n break\n case 'assignContactOwner': {\n const owner = textOf(text['text'])\n if (!owner || !(ROUND_ROBIN.test(owner) || EMAIL_ADDRESS.test(owner))) {\n refuse(\n reader,\n 'automation-owner',\n `${what}: give the owner as \"round robin\", or as the teammate’s email address the description gives. A teammate named without one goes in notes, not in a step.`,\n path,\n fields,\n )\n } else {\n step.owner = ROUND_ROBIN.test(owner) ? 'round robin' : owner\n }\n break\n }\n case 'createCrmTask': {\n step.title = requireText(reader, text, 'text', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n const kind = fields['taskKind']\n const due = wholeNumber(fields['dueInDays'])\n if (!(CRM_TASK_KINDS as readonly unknown[]).includes(kind)) {\n refuse(reader, 'automation-step-field', `${what} needs taskKind.`, path, fields)\n } else {\n step.taskKind = kind as CrmTaskKind\n }\n if (due === null || due < 0 || due > CRM_TASK_MAX_DUE_DAYS) {\n refuse(reader, 'automation-step-field', `${what} needs dueInDays, from 0 to ${CRM_TASK_MAX_DUE_DAYS}.`, path, fields)\n } else {\n step.dueInDays = due\n }\n break\n }\n case 'logCrmActivity': {\n const kind = fields['activityKind']\n if (!(CRM_ACTIVITY_KINDS as readonly unknown[]).includes(kind)) {\n refuse(reader, 'automation-step-field', `${what} needs activityKind.`, path, fields)\n } else {\n step.activityKind = kind as CrmActivityKind\n }\n step.body = requireText(reader, text, 'text', AI_AUTOMATION_TEXT_MAX_CHARS, path, what)\n break\n }\n }\n return reader.violations.length === before ? step : null\n}\n\n/**\n * The automation tool's answer read and held to the vocabulary and to what\n * this workspace can run — the check the doctrine's loop runs on each answer,\n * beside rule 13. `value` is the automation when nothing is refused.\n */\nexport function readAiAutomationAnswer(\n answer: Record<string, unknown>,\n capabilities: AiAutomationCapabilities,\n): AiGenerationCheckResult<AiAutomationAnswer> {\n const reader: Reader = { violations: [], offending: [] }\n const done = (value: AiAutomationAnswer | null): AiGenerationCheckResult<AiAutomationAnswer> => ({\n value: reader.violations.length ? null : value,\n violations: reader.violations,\n ...(reader.offending.length ? { offending: Object.fromEntries(reader.offending) } : {}),\n })\n const notes = (Array.isArray(answer['notes']) ? answer['notes'] : [])\n .map(textOf)\n .filter((note): note is string => note !== null && !HOSTILE_TEXT.test(note))\n .slice(0, AI_AUTOMATION_NOTES_MAX)\n .map((note) => note.slice(0, AI_AUTOMATION_NOTE_MAX_CHARS))\n const name = (textOf(answer['name']) ?? '').slice(0, AI_AUTOMATION_NAME_MAX_CHARS)\n const written = JSON.stringify(answer).length\n if (written > AI_AUTOMATION_ANSWER_MAX_CHARS) {\n refuse(\n reader,\n 'automation-too-long',\n `Written out, the automation is ${written} characters, over the ${AI_AUTOMATION_ANSWER_MAX_CHARS} it may be. Shorten its emails and messages, or use fewer steps.`,\n 'steps',\n written,\n )\n return done(null)\n }\n\n // An answer that builds an automation leaves `unsupported` empty, which the\n // schema states as an empty string rather than a `null`.\n const unsupported = answer['unsupported']\n if (unsupported !== null && unsupported !== undefined && unsupported !== '') {\n if (!(AI_AUTOMATION_UNSUPPORTED as readonly unknown[]).includes(unsupported)) {\n refuse(reader, 'automation-shape', 'unsupported is empty, or one of the reasons offered.', 'unsupported', unsupported)\n return done(null)\n }\n const need = AI_AUTOMATION_UNSUPPORTED_NEED[unsupported as AiAutomationUnsupported]\n if (need && capabilities[need]) {\n refuse(\n reader,\n 'automation-unsupported',\n `This workspace has ${AI_AUTOMATION_NEED_LABELS[need]}: build the automation with it.`,\n 'unsupported',\n unsupported,\n )\n return done(null)\n }\n return done({\n name,\n trigger: { event: HOST_EVENT_TYPES[0], conditions: [], combinator: 'and' },\n steps: [],\n notes,\n unsupported: unsupported as AiAutomationUnsupported,\n })\n }\n\n const trigger = answer['trigger']\n const steps = answer['steps']\n if (!isRecord(trigger) || !Array.isArray(steps)) {\n refuse(reader, 'automation-shape', 'The answer has no trigger and steps.', 'trigger', trigger)\n return done(null)\n }\n const event = trigger['event']\n if (!(HOST_EVENT_TYPES as readonly unknown[]).includes(event)) {\n refuse(reader, 'automation-trigger', 'The trigger is not one of the events offered.', 'trigger.event', event)\n return done(null)\n }\n const hostEvent = event as HostEventType\n const need = AI_AUTOMATION_TRIGGER_NEEDS[hostEvent]\n if (need && !capabilities[need]) {\n refuse(\n reader,\n 'automation-plan',\n `The trigger \"${aiAutomationTriggerLabel(hostEvent)}\" needs ${AI_AUTOMATION_NEED_LABELS[need]}, which this workspace does not have. Pick another trigger, or answer with unsupported \"needs-${need}\".`,\n 'trigger.event',\n event,\n )\n return done(null)\n }\n const rawConditions = Array.isArray(trigger['conditions']) ? trigger['conditions'] : []\n if (rawConditions.length > ACTION_MAX_CONDITIONS) {\n refuse(\n reader,\n 'automation-too-many-conditions',\n `A trigger holds at most ${ACTION_MAX_CONDITIONS} conditions.`,\n 'trigger.conditions',\n rawConditions.length,\n )\n }\n const conditions = rawConditions\n .slice(0, ACTION_MAX_CONDITIONS)\n .map((raw, index) => readCondition(reader, raw, `trigger.conditions[${index}]`, hostEvent, false))\n .filter((condition): condition is AiAutomationCondition => condition !== null)\n const combinator = (TRIGGER_COMBINATORS as readonly unknown[]).includes(trigger['combinator'])\n ? (trigger['combinator'] as TriggerCombinator)\n : 'and'\n\n if (!steps.length) {\n refuse(reader, 'automation-shape', 'An automation has at least one step; answer unsupported when none fits.', 'steps', steps)\n }\n if (steps.length > ACTION_MAX_STEPS) {\n refuse(reader, 'automation-too-many-steps', `An automation holds at most ${ACTION_MAX_STEPS} steps.`, 'steps', steps.length)\n }\n const read = steps\n .slice(0, ACTION_MAX_STEPS)\n .map((raw, index) => readStep(reader, raw, index, hostEvent, capabilities))\n .filter((step): step is AiAutomationAnswerStep => step !== null)\n\n return done({\n name,\n trigger: { event: hostEvent, conditions, combinator },\n steps: read,\n notes,\n unsupported: null,\n })\n}\n\n// ── Reading an explanation ────────────────────────────────────────────────\n\nexport interface AiWorkflowExplanation {\n summary: string\n points: string[]\n suggestions: string[]\n}\n\n/** Whether a line carries an email address, which nothing an explanation was shown holds. */\nconst CARRIES_ADDRESS = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/\n\nfunction lines(value: unknown, max: number): string[] {\n return (Array.isArray(value) ? value : [])\n .map(textOf)\n .filter((line): line is string => line !== null)\n .slice(0, max)\n .map((line) => line.slice(0, AI_WORKFLOW_LINE_MAX_CHARS))\n}\n\n/**\n * The explanation tool's answer: a summary, and at most so many points and\n * suggestions, each cut to a line. Refused when it has no summary, or carries\n * markup or an address — plain words for a person, about a record whose\n * personal details were removed before the model saw it.\n */\nexport function readAiWorkflowExplanation(\n answer: Record<string, unknown>,\n): AiGenerationCheckResult<AiWorkflowExplanation> {\n const summary = (textOf(answer['summary']) ?? '').slice(0, AI_WORKFLOW_SUMMARY_MAX_CHARS)\n const value: AiWorkflowExplanation = {\n summary,\n points: lines(answer['points'], AI_WORKFLOW_POINTS_MAX),\n suggestions: lines(answer['suggestions'], AI_WORKFLOW_SUGGESTIONS_MAX),\n }\n const violations: AiDoctrineViolation[] = []\n if (!summary) {\n violations.push({ rule: null, code: 'explanation-shape', message: 'The explanation has no summary.' })\n }\n const all = [value.summary, ...value.points, ...value.suggestions]\n if (all.some((line) => HOSTILE_TEXT.test(line))) {\n violations.push({\n rule: null,\n code: 'explanation-markup',\n message: 'Write the explanation as plain text, with no HTML or script.',\n })\n }\n if (all.some((line) => CARRIES_ADDRESS.test(line))) {\n violations.push({\n rule: null,\n code: 'explanation-address',\n message: 'The explanation names an email address. Describe the person or the teammate instead.',\n })\n }\n return { value: violations.length ? null : value, violations }\n}\n"],"names":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","CONTACT_TAG_MAX_LENGTH","FLOW_TIMED_OUT_FIELD","FLOW_WAIT_MAX_MINUTES","FLOW_WAIT_MIN_MINUTES","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_KINDS","CRM_TASK_KINDS","CRM_TASK_MAX_DUE_DAYS","HOST_EVENT_PAYLOAD_KEYS","HOST_EVENT_TYPES","AI_AUTOMATION_NEED_LABELS","AI_AUTOMATION_STEP_NEEDS","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_TRIGGER_NEEDS","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","aiAutomationTriggerLabel","HOSTILE_TEXT","AI_AUTOMATION_TOOL_NAME","AI_WORKFLOW_EXPLANATION_TOOL_NAME","AI_AUTOMATION_NAME_MAX_CHARS","AI_AUTOMATION_SUBJECT_MAX_CHARS","AI_AUTOMATION_BODY_MAX_CHARS","AI_AUTOMATION_TEXT_MAX_CHARS","AI_AUTOMATION_REFERENCE_MAX_CHARS","AI_AUTOMATION_VALUE_MAX_CHARS","AI_AUTOMATION_NOTES_MAX","AI_AUTOMATION_NOTE_MAX_CHARS","AI_AUTOMATION_ANSWER_MAX_CHARS","AI_WORKFLOW_SUMMARY_MAX_CHARS","AI_WORKFLOW_POINTS_MAX","AI_WORKFLOW_SUGGESTIONS_MAX","AI_WORKFLOW_LINE_MAX_CHARS","SEVERITIES","DEFINITIONS","condition","type","additionalProperties","required","properties","field","description","op","enum","value","event","when","items","$ref","REFERENCE_STEPS","TEXT_STEPS","REFERENCE_KEY","enrollList","assignCampaign","runWorkflow","datasetAppend","updateDataset","webhookPost","TEXT_KEY","notifyAdmins","addContactTag","assignContactOwner","siteAlert","createCrmTask","logCrmActivity","minutes","what","stepVariant","types","fields","Object","keys","AI_AUTOMATION_STEP_VARIANTS","reference","text","subject","body","toField","severity","stage","taskKind","dueInDays","activityKind","aiAutomationTool","name","strict","inputSchema","trigger","conditions","combinator","steps","action","anyOf","notes","unsupported","$defs","aiWorkflowExplanationTool","summary","points","suggestions","AI_AUTOMATION_UNREADABLE_CODES","AI_AUTOMATION_OVERSIZE_CODES","DEFAULT_TO_FIELD","FIELD_NAME","EMAIL_ADDRESS","ROUND_ROBIN","isRecord","Array","isArray","textOf","clean","replace","trim","fieldsFor","length","refuse","reader","code","message","path","violations","push","rule","paths","offending","readCondition","raw","guard","test","includes","readable","join","lifecycleStageOf","wanted","toLowerCase","find","requireText","step","key","max","wholeNumber","Number","isInteger","readStep","index","capabilities","stepType","need","before","list","campaign","workflow","webhook","dataset","title","tag","owner","guards","undefined","shared","own","waited","kind","due","readAiAutomationAnswer","answer","done","fromEntries","map","filter","note","slice","written","JSON","stringify","hostEvent","rawConditions","read","CARRIES_ADDRESS","lines","line","readAiWorkflowExplanation","all","some"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-workflow-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ACTION_MAX_CONDITIONS,\n ACTION_MAX_STEPS,\n CONTACT_TAG_MAX_LENGTH,\n FLOW_TIMED_OUT_FIELD,\n FLOW_WAIT_MAX_MINUTES,\n FLOW_WAIT_MIN_MINUTES,\n HOST_ACTION_STEP_LABELS,\n TRIGGER_COMBINATORS,\n TRIGGER_CONDITION_OPS,\n type TriggerCombinator,\n type TriggerConditionOp,\n} from '@aglyn/aglyn/app-utils/actions'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_KINDS,\n CRM_TASK_KINDS,\n CRM_TASK_MAX_DUE_DAYS,\n type ContactLifecycleStage,\n type CrmActivityKind,\n type CrmTaskKind,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n HOST_EVENT_PAYLOAD_KEYS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/host-events'\nimport {\n AI_AUTOMATION_NEED_LABELS,\n AI_AUTOMATION_STEP_NEEDS,\n AI_AUTOMATION_STEP_TYPES,\n AI_AUTOMATION_TRIGGER_NEEDS,\n AI_AUTOMATION_UNSUPPORTED,\n AI_AUTOMATION_UNSUPPORTED_NEED,\n aiAutomationTriggerLabel,\n type AiAutomationCapabilities,\n type AiAutomationStepType,\n type AiAutomationUnsupported,\n} from '../model/ai-workflow-job'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport { HOSTILE_TEXT } from '../runtime/ai-node-tree'\n\n/**\n * The two tools a `workflow` job answers through (AGL-2919).\n *\n * `submit_automation` carries one automation in the Actions editor's own\n * vocabulary — a trigger from the host events, conditions over the fields the\n * trigger carries, and steps from the automation step types — with every\n * record it names written as WORDS. The step resolves those words against the\n * site after the answer, so the model is never shown the site's lists,\n * campaigns, workflows, webhooks or pipeline stages, and never writes an id it\n * could only have invented.\n *\n * `submit_explanation` carries a plain-words account of one automation, or of\n * why one of its runs failed.\n *\n * Both schemas are strict: every object forbids extra keys and requires every\n * key it has, and nothing in either is a `null`. Where a step may go without\n * something, the absence is an empty list or an empty string: a step that\n * always runs has an empty `when`, a notEmpty condition compares against\n * nothing, and an answer that builds an automation leaves `unsupported`\n * empty. Bounds a strict schema cannot state are stated in the descriptions\n * and held by the readers below, whose refusals name the field so the one\n * re-ask can fix it.\n *\n * A provider that constrains decoding compiles every strict schema of a\n * request into a grammar, and refuses one whose grammar grows past its size\n * (`AiToolSchemaLimits`). So the steps' union carries as little repetition as\n * a grammar can be made to hold:\n *\n * - a step is its guard and its action, `{ when, action }`, so the condition\n * is compiled once for the whole union instead of once per variant;\n * - a variant serves every step type that carries the same fields, named for\n * what they hold: the six steps that name a record share `reference`, and\n * the three that carry one line share `text`;\n * - every field stays required and every vocabulary stays an enum, so the\n * model is still decoded into the Actions editor's words.\n *\n * Ten variants carry all seventeen step types, and the reader below maps the\n * shared fields back onto the step's own key.\n */\n\nexport const AI_AUTOMATION_TOOL_NAME = 'submit_automation'\nexport const AI_WORKFLOW_EXPLANATION_TOOL_NAME = 'submit_explanation'\n\n/** The longest automation name the Actions editor stores. */\nexport const AI_AUTOMATION_NAME_MAX_CHARS = 60\n/** The longest subject the executor sends. */\nexport const AI_AUTOMATION_SUBJECT_MAX_CHARS = 200\n/** The longest email text the executor sends. */\nexport const AI_AUTOMATION_BODY_MAX_CHARS = 5_000\n/** The longest title, message or record name a step carries. */\nexport const AI_AUTOMATION_TEXT_MAX_CHARS = 300\n/** The longest words naming a record. */\nexport const AI_AUTOMATION_REFERENCE_MAX_CHARS = 100\n/** The longest condition value. */\nexport const AI_AUTOMATION_VALUE_MAX_CHARS = 200\n/** The most notes an answer keeps, and the longest. */\nexport const AI_AUTOMATION_NOTES_MAX = 3\nexport const AI_AUTOMATION_NOTE_MAX_CHARS = 300\n/**\n * The longest a whole answer may be, written out as JSON: what the routing\n * ceiling for `job.workflow` holds at three characters a token with as much\n * again to think in (`ai-job-workflow-step.spec.ts` measures it). Ten steps\n * with two emails of a paragraph or two each come to about 2,400, each step\n * carrying only its own fields.\n */\nexport const AI_AUTOMATION_ANSWER_MAX_CHARS = 6_000\n\n/** The explanation's bounds. */\nexport const AI_WORKFLOW_SUMMARY_MAX_CHARS = 400\nexport const AI_WORKFLOW_POINTS_MAX = 8\nexport const AI_WORKFLOW_SUGGESTIONS_MAX = 4\nexport const AI_WORKFLOW_LINE_MAX_CHARS = 300\n\nconst SEVERITIES = ['info', 'success', 'warning', 'error'] as const\ntype Severity = (typeof SEVERITIES)[number]\n\ntype Schema = Record<string, unknown>\n\n/**\n * What the schema defines once and refers to where it is used: a condition,\n * which the trigger and every step's guard share, and the guard itself.\n */\nconst DEFINITIONS: Schema = {\n condition: {\n type: 'object',\n additionalProperties: false,\n required: ['field', 'op', 'value'],\n properties: {\n field: {\n type: 'string',\n description: 'The field of the event the condition reads, as the trigger lists its fields.',\n },\n op: { type: 'string', enum: [...TRIGGER_CONDITION_OPS] },\n value: {\n type: 'string',\n description: 'What equals or contains compares against; empty for notEmpty.',\n },\n },\n },\n event: {\n type: 'string',\n enum: [...HOST_EVENT_TYPES],\n description: 'One of the things that happen on a site.',\n },\n // A list rather than a condition or `null`: a step goes without one by an\n // empty list, which costs no union.\n when: {\n type: 'array',\n description: 'At most one condition: the step runs only when it holds. Empty when the step always runs.',\n items: { $ref: '#/$defs/condition' },\n },\n}\n\n/** The steps that name one of the site's records, which they all name the same way. */\nconst REFERENCE_STEPS = [\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n] as const satisfies readonly AiAutomationStepType[]\n\n/** The steps whose only field is the one line they carry. */\nconst TEXT_STEPS = [\n 'notifyAdmins',\n 'addContactTag',\n 'assignContactOwner',\n] as const satisfies readonly AiAutomationStepType[]\n\n/** The key each `reference` step fills in the answer the reader gives back. */\nconst REFERENCE_KEY = {\n enrollList: 'list',\n assignCampaign: 'campaign',\n runWorkflow: 'workflow',\n datasetAppend: 'dataset',\n updateDataset: 'dataset',\n webhookPost: 'webhook',\n} as const satisfies Readonly<Record<(typeof REFERENCE_STEPS)[number], keyof AiAutomationAnswerStep>>\n\n/**\n * The key each step's one line fills in the answer the reader gives back. Six\n * step types carry one line between them, so the schema asks for all six\n * under `text` and the reader puts each back where the automation keeps it.\n */\nconst TEXT_KEY = {\n notifyAdmins: 'title',\n addContactTag: 'tag',\n assignContactOwner: 'owner',\n siteAlert: 'message',\n createCrmTask: 'title',\n logCrmActivity: 'body',\n} as const satisfies Readonly<Partial<Record<AiAutomationStepType, keyof AiAutomationAnswerStep>>>\n\nconst minutes = (what: string): Schema => ({\n type: 'integer',\n description: `${what}, in whole minutes from ${FLOW_WAIT_MIN_MINUTES} to ${FLOW_WAIT_MAX_MINUTES}; an hour is 60 and a day 1440.`,\n})\n\n/**\n * One variant of the steps' union: the step types it serves, and the fields\n * every one of them carries, each required and never nullable. A type belongs\n * to exactly one variant, so the type a step names settles which fields it\n * writes.\n */\nfunction stepVariant(\n types: readonly AiAutomationStepType[],\n fields: Readonly<Record<string, Schema>>,\n): Schema {\n return {\n type: 'object',\n additionalProperties: false,\n required: ['type', ...Object.keys(fields)],\n properties: { type: { type: 'string', enum: [...types] }, ...fields },\n }\n}\n\n/** Every step type an automation may carry, grouped by the fields it needs. */\nexport const AI_AUTOMATION_STEP_VARIANTS: readonly Schema[] = [\n stepVariant([...REFERENCE_STEPS], {\n reference: {\n type: 'string',\n description:\n 'The record the step acts on — the email list, campaign, workflow, dataset or outbound webhook — in the words of the description, such as \"newsletter\". Never an id.',\n },\n }),\n stepVariant([...TEXT_STEPS], {\n text: {\n type: 'string',\n description: `The one line the step carries: for notifyAdmins what the admins read, for addContactTag the tag (at most ${CONTACT_TAG_MAX_LENGTH} characters), and for assignContactOwner \"round robin\" or the teammate’s email address the description gives.`,\n },\n }),\n stepVariant(['sendEmail'], {\n subject: { type: 'string', description: 'The subject line.' },\n body: { type: 'string', description: 'The email, as plain text.' },\n toField: {\n type: 'string',\n description: 'The event field holding the address: \"email\" unless the description names another.',\n },\n }),\n stepVariant(['siteAlert'], {\n text: { type: 'string', description: 'What the visitor reads.' },\n severity: { type: 'string', enum: [...SEVERITIES] },\n }),\n stepVariant(['wait'], { minutes: minutes('How long to wait') }),\n stepVariant(['waitForEvent'], {\n event: { $ref: '#/$defs/event' },\n minutes: minutes('When to give up'),\n }),\n stepVariant(['exitFlow'], {}),\n stepVariant(['setContactStage'], { stage: { type: 'string', enum: [...CONTACT_LIFECYCLE_STAGES] } }),\n stepVariant(['createCrmTask'], {\n text: { type: 'string', description: 'The task.' },\n taskKind: { type: 'string', enum: [...CRM_TASK_KINDS] },\n dueInDays: {\n type: 'integer',\n description: `Days from the run, 0 for today, at most ${CRM_TASK_MAX_DUE_DAYS}.`,\n },\n }),\n stepVariant(['logCrmActivity'], {\n activityKind: { type: 'string', enum: [...CRM_ACTIVITY_KINDS] },\n text: { type: 'string', description: 'What happened.' },\n }),\n]\n\n/** The automation tool. The same bytes on every request, so it sits inside the cached prefix. */\nexport function aiAutomationTool(): AiTool {\n return {\n name: AI_AUTOMATION_TOOL_NAME,\n description: `Submit one automation: its trigger, the conditions on it, and its steps in order. Written out, the whole automation is at most ${AI_AUTOMATION_ANSWER_MAX_CHARS} characters, so keep its emails short.`,\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'trigger', 'steps', 'notes', 'unsupported'],\n properties: {\n name: {\n type: 'string',\n description: `A short name for the automation, at most ${AI_AUTOMATION_NAME_MAX_CHARS} characters.`,\n },\n trigger: {\n type: 'object',\n additionalProperties: false,\n required: ['event', 'conditions', 'combinator'],\n properties: {\n event: { $ref: '#/$defs/event' },\n conditions: {\n type: 'array',\n description: `What the event must meet: at most ${ACTION_MAX_CONDITIONS} conditions, empty when it runs every time.`,\n items: { $ref: '#/$defs/condition' },\n },\n combinator: { type: 'string', enum: [...TRIGGER_COMBINATORS] },\n },\n },\n steps: {\n type: 'array',\n description: `The steps in order, at most ${ACTION_MAX_STEPS}. Each is when it runs and what it does, and the action carries only the fields its type takes. Empty only with unsupported.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['when', 'action'],\n properties: {\n when: { $ref: '#/$defs/when' },\n action: { anyOf: AI_AUTOMATION_STEP_VARIANTS },\n },\n },\n },\n notes: {\n type: 'array',\n description: `What the person must decide that the automation cannot hold; at most ${AI_AUTOMATION_NOTES_MAX} short sentences.`,\n items: { type: 'string' },\n },\n unsupported: {\n type: 'string',\n enum: ['', ...AI_AUTOMATION_UNSUPPORTED],\n description: 'Why no automation can be built from this description; empty when one is built.',\n },\n },\n $defs: DEFINITIONS,\n },\n }\n}\n\n/** The explanation tool. */\nexport function aiWorkflowExplanationTool(): AiTool {\n return {\n name: AI_WORKFLOW_EXPLANATION_TOOL_NAME,\n description: 'Submit the explanation, in plain words for someone who does not write code.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['summary', 'points', 'suggestions'],\n properties: {\n summary: {\n type: 'string',\n description: `One or two sentences, at most ${AI_WORKFLOW_SUMMARY_MAX_CHARS} characters.`,\n },\n points: {\n type: 'array',\n description: `At most ${AI_WORKFLOW_POINTS_MAX}, one sentence each.`,\n items: { type: 'string' },\n },\n suggestions: {\n type: 'array',\n description: `At most ${AI_WORKFLOW_SUGGESTIONS_MAX}, one sentence each; empty when there is nothing to suggest.`,\n items: { type: 'string' },\n },\n },\n },\n }\n}\n\n// ── Reading an automation ─────────────────────────────────────────────────\n\nexport interface AiAutomationCondition {\n field: string\n op: TriggerConditionOp\n /** `null` for `notEmpty`. */\n value: string | null\n}\n\n/** One step as the reader gives it back, every field the step does not use `null`. */\nexport interface AiAutomationAnswerStep {\n type: AiAutomationStepType\n when: AiAutomationCondition | null\n list: string | null\n campaign: string | null\n workflow: string | null\n webhook: string | null\n dataset: string | null\n subject: string | null\n body: string | null\n toField: string | null\n title: string | null\n message: string | null\n severity: Severity | null\n minutes: number | null\n event: HostEventType | null\n stage: ContactLifecycleStage | null\n tag: string | null\n owner: string | null\n taskKind: CrmTaskKind | null\n dueInDays: number | null\n activityKind: CrmActivityKind | null\n}\n\nexport interface AiAutomationAnswer {\n name: string\n trigger: {\n event: HostEventType\n conditions: AiAutomationCondition[]\n combinator: TriggerCombinator\n }\n steps: AiAutomationAnswerStep[]\n notes: string[]\n unsupported: AiAutomationUnsupported | null\n}\n\n/** The codes that say an answer could not be read as an automation at all. */\nexport const AI_AUTOMATION_UNREADABLE_CODES: readonly string[] = [\n 'automation-shape',\n 'automation-trigger',\n 'automation-step-type',\n]\n\n/** The codes that say an answer is larger than an automation may be. */\nexport const AI_AUTOMATION_OVERSIZE_CODES: readonly string[] = [\n 'automation-too-many-steps',\n 'automation-too-many-conditions',\n 'automation-too-long',\n]\n\n/** The address field a step reads when it names none. */\nconst DEFAULT_TO_FIELD = 'email'\nconst FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\nconst EMAIL_ADDRESS = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst ROUND_ROBIN = /^\\s*round[\\s-]*robin\\s*$/i\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** A string trimmed to one space between words, or `null` for none. */\nfunction textOf(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const clean = value.replace(/[ \\t]+/g, ' ').trim()\n return clean ? clean : null\n}\n\n/** The fields a trigger's conditions and its steps' guards may read. */\nfunction fieldsFor(event: HostEventType): readonly string[] | null {\n // A form submission carries every field the form collects, which no fixed\n // list names, so any field name is readable on it.\n if (event === 'formSubmission') return null\n const keys = HOST_EVENT_PAYLOAD_KEYS[event]\n return keys?.length ? keys : null\n}\n\ninterface Reader {\n violations: AiDoctrineViolation[]\n offending: Array<[string, unknown]>\n}\n\nfunction refuse(\n reader: Reader,\n code: string,\n message: string,\n path: string,\n value: unknown,\n): void {\n reader.violations.push({ rule: null, code, message, paths: [path] })\n if (reader.offending.length < 6) reader.offending.push([path, value])\n}\n\nfunction readCondition(\n reader: Reader,\n raw: unknown,\n path: string,\n event: HostEventType,\n guard: boolean,\n): AiAutomationCondition | null {\n if (!isRecord(raw)) {\n refuse(reader, 'automation-shape', 'A condition is not an object with a field, an op and a value.', path, raw)\n return null\n }\n const field = textOf(raw['field'])\n const op = raw['op']\n if (!field || !FIELD_NAME.test(field)) {\n refuse(reader, 'automation-condition', 'Name the event field the condition reads.', path, raw)\n return null\n }\n if (!(TRIGGER_CONDITION_OPS as readonly unknown[]).includes(op)) {\n refuse(reader, 'automation-condition', `The condition on \"${field}\" needs equals, contains or notEmpty.`, path, raw)\n return null\n }\n if (event === 'formSubmission' && field === 'formId') {\n refuse(\n reader,\n 'automation-condition',\n 'A form submission names its form by formName, the form’s name as the site lists it.',\n path,\n raw,\n )\n return null\n }\n // A step's guard may also read whether the wait before it ran out of time.\n const readable = fieldsFor(event)\n if (readable && !readable.includes(field) && !(guard && field === FLOW_TIMED_OUT_FIELD)) {\n refuse(\n reader,\n 'automation-condition',\n `The trigger \"${aiAutomationTriggerLabel(event)}\" does not carry \"${field}\". It carries: ${readable.join(', ')}.`,\n path,\n raw,\n )\n return null\n }\n const value = textOf(raw['value'])\n if (op !== 'notEmpty' && !value) {\n refuse(reader, 'automation-condition', `The condition on \"${field}\" needs the value it compares against.`, path, raw)\n return null\n }\n if (value && value.length > AI_AUTOMATION_VALUE_MAX_CHARS) {\n refuse(reader, 'automation-too-long', `The value the condition on \"${field}\" compares against is too long.`, path, raw)\n return null\n }\n if (field === 'lifecycleStage' || field === 'previousStage') {\n const stage = value ? lifecycleStageOf(value) : null\n if (op !== 'notEmpty' && !stage) {\n refuse(\n reader,\n 'automation-condition',\n `A lifecycle stage is one of: ${CONTACT_LIFECYCLE_STAGES.join(', ')}.`,\n path,\n raw,\n )\n return null\n }\n return { field, op: op as TriggerConditionOp, value: op === 'notEmpty' ? null : stage }\n }\n return { field, op: op as TriggerConditionOp, value: op === 'notEmpty' ? null : value }\n}\n\n/** A lifecycle stage by its id or its label, case aside. */\nexport function lifecycleStageOf(value: string): ContactLifecycleStage | null {\n const wanted = value.trim().toLowerCase()\n return (\n CONTACT_LIFECYCLE_STAGES.find(\n (stage) => stage === wanted || CONTACT_LIFECYCLE_STAGE_LABELS[stage].toLowerCase() === wanted,\n ) ?? null\n )\n}\n\nfunction requireText(\n reader: Reader,\n step: Record<string, unknown>,\n key: string,\n max: number,\n path: string,\n what: string,\n): string | null {\n const value = textOf(step[key])\n if (!value) {\n refuse(reader, 'automation-step-field', `${what} needs ${key}.`, path, step)\n return null\n }\n if (value.length > max) {\n refuse(reader, 'automation-too-long', `${what}: ${key} is longer than ${max} characters.`, path, step)\n return null\n }\n if (HOSTILE_TEXT.test(value)) {\n refuse(reader, 'automation-markup', `${what}: write ${key} as plain text, with no HTML or script.`, path, step)\n return null\n }\n return value\n}\n\nfunction wholeNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isInteger(value) ? value : null\n}\n\nfunction readStep(\n reader: Reader,\n raw: unknown,\n index: number,\n event: HostEventType,\n capabilities: AiAutomationCapabilities,\n): AiAutomationAnswerStep | null {\n const path = `steps[${index}]`\n if (!isRecord(raw)) {\n refuse(reader, 'automation-shape', `Step ${index + 1} is not an object.`, path, raw)\n return null\n }\n // A step is its guard and its action: `{ when, action }`. An answer that\n // writes the action's own fields beside `when` reads the same, so a model\n // that flattens the step does not spend the one re-ask.\n const action = raw['action']\n const fields: Record<string, unknown> = isRecord(action) ? { ...action, when: raw['when'] } : raw\n const type = fields['type']\n if (!(AI_AUTOMATION_STEP_TYPES as readonly unknown[]).includes(type)) {\n refuse(\n reader,\n 'automation-step-type',\n `Step ${index + 1} is not one of the steps offered: ${AI_AUTOMATION_STEP_TYPES.join(', ')}.`,\n path,\n fields,\n )\n return null\n }\n const stepType = type as AiAutomationStepType\n const what = `Step ${index + 1} (${HOST_ACTION_STEP_LABELS[stepType]})`\n const need = AI_AUTOMATION_STEP_NEEDS[stepType]\n if (need && !capabilities[need]) {\n refuse(\n reader,\n 'automation-plan',\n `${what} needs ${AI_AUTOMATION_NEED_LABELS[need]}, which this workspace does not have. Leave it out, or answer with unsupported \"needs-${need}\".`,\n path,\n fields,\n )\n return null\n }\n const before = reader.violations.length\n const step: AiAutomationAnswerStep = {\n type: stepType,\n when: null,\n list: null,\n campaign: null,\n workflow: null,\n webhook: null,\n dataset: null,\n subject: null,\n body: null,\n toField: null,\n title: null,\n message: null,\n severity: null,\n minutes: null,\n event: null,\n stage: null,\n tag: null,\n owner: null,\n taskKind: null,\n dueInDays: null,\n activityKind: null,\n }\n // A step's `when` is a list of at most one condition; a lone condition, or\n // none, reads as the list it stands for.\n const when = fields['when']\n const guards = Array.isArray(when) ? when : when === null || when === undefined ? [] : [when]\n if (guards.length > 1) {\n refuse(reader, 'automation-condition', `${what}: when holds one condition at most.`, `${path}.when`, when)\n } else if (guards.length === 1) {\n step.when = readCondition(reader, guards[0], `${path}.when`, event, true)\n }\n // The six steps that name a record share `reference`, and the three that\n // carry one line share `text`; an answer that writes the step's own key\n // instead — `list`, `tag`, `owner` — reads the same.\n const shared = (key: 'reference' | 'text', own: keyof AiAutomationAnswerStep): unknown =>\n fields[key] ?? fields[own]\n const own = TEXT_KEY[stepType as keyof typeof TEXT_KEY]\n const text: Record<string, unknown> = own ? { text: shared('text', own) } : fields\n switch (stepType) {\n case 'sendEmail': {\n step.subject = requireText(reader, fields, 'subject', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n step.body = requireText(reader, fields, 'body', AI_AUTOMATION_BODY_MAX_CHARS, path, what)\n const toField = textOf(fields['toField'])\n if (toField && !FIELD_NAME.test(toField)) {\n refuse(reader, 'automation-step-field', `${what}: toField names the event field holding the address.`, path, fields)\n } else {\n step.toField = toField && toField !== DEFAULT_TO_FIELD ? toField : null\n }\n break\n }\n case 'notifyAdmins':\n step.title = requireText(reader, text, 'text', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n break\n case 'enrollList':\n case 'assignCampaign':\n case 'runWorkflow':\n case 'webhookPost':\n case 'datasetAppend':\n case 'updateDataset':\n step[REFERENCE_KEY[stepType]] = requireText(\n reader,\n { reference: shared('reference', REFERENCE_KEY[stepType]) },\n 'reference',\n AI_AUTOMATION_REFERENCE_MAX_CHARS,\n path,\n what,\n )\n break\n case 'siteAlert': {\n step.message = requireText(reader, text, 'text', AI_AUTOMATION_TEXT_MAX_CHARS, path, what)\n const severity = fields['severity']\n step.severity = (SEVERITIES as readonly unknown[]).includes(severity) ? (severity as Severity) : 'info'\n break\n }\n case 'wait':\n case 'waitForEvent': {\n const minutes = wholeNumber(fields['minutes'])\n if (minutes === null || minutes < FLOW_WAIT_MIN_MINUTES || minutes > FLOW_WAIT_MAX_MINUTES) {\n refuse(\n reader,\n 'automation-step-field',\n `${what} needs minutes, a whole number from ${FLOW_WAIT_MIN_MINUTES} to ${FLOW_WAIT_MAX_MINUTES}.`,\n path,\n fields,\n )\n } else {\n step.minutes = minutes\n }\n if (stepType === 'waitForEvent') {\n const waited = fields['event']\n if (!(HOST_EVENT_TYPES as readonly unknown[]).includes(waited)) {\n refuse(reader, 'automation-step-field', `${what} needs the event it waits for.`, path, fields)\n } else {\n step.event = waited as HostEventType\n }\n }\n break\n }\n case 'exitFlow':\n break\n case 'setContactStage': {\n const stage = fields['stage']\n if (!(CONTACT_LIFECYCLE_STAGES as readonly unknown[]).includes(stage)) {\n refuse(reader, 'automation-step-field', `${what} needs the lifecycle stage to set.`, path, fields)\n } else {\n step.stage = stage as ContactLifecycleStage\n }\n break\n }\n case 'addContactTag':\n step.tag = requireText(reader, text, 'text', CONTACT_TAG_MAX_LENGTH, path, what)\n break\n case 'assignContactOwner': {\n const owner = textOf(text['text'])\n if (!owner || !(ROUND_ROBIN.test(owner) || EMAIL_ADDRESS.test(owner))) {\n refuse(\n reader,\n 'automation-owner',\n `${what}: give the owner as \"round robin\", or as the teammate’s email address the description gives. A teammate named without one goes in notes, not in a step.`,\n path,\n fields,\n )\n } else {\n step.owner = ROUND_ROBIN.test(owner) ? 'round robin' : owner\n }\n break\n }\n case 'createCrmTask': {\n step.title = requireText(reader, text, 'text', AI_AUTOMATION_SUBJECT_MAX_CHARS, path, what)\n const kind = fields['taskKind']\n const due = wholeNumber(fields['dueInDays'])\n if (!(CRM_TASK_KINDS as readonly unknown[]).includes(kind)) {\n refuse(reader, 'automation-step-field', `${what} needs taskKind.`, path, fields)\n } else {\n step.taskKind = kind as CrmTaskKind\n }\n if (due === null || due < 0 || due > CRM_TASK_MAX_DUE_DAYS) {\n refuse(reader, 'automation-step-field', `${what} needs dueInDays, from 0 to ${CRM_TASK_MAX_DUE_DAYS}.`, path, fields)\n } else {\n step.dueInDays = due\n }\n break\n }\n case 'logCrmActivity': {\n const kind = fields['activityKind']\n if (!(CRM_ACTIVITY_KINDS as readonly unknown[]).includes(kind)) {\n refuse(reader, 'automation-step-field', `${what} needs activityKind.`, path, fields)\n } else {\n step.activityKind = kind as CrmActivityKind\n }\n step.body = requireText(reader, text, 'text', AI_AUTOMATION_TEXT_MAX_CHARS, path, what)\n break\n }\n }\n return reader.violations.length === before ? step : null\n}\n\n/**\n * The automation tool's answer read and held to the vocabulary and to what\n * this workspace can run — the check the doctrine's loop runs on each answer,\n * beside rule 13. `value` is the automation when nothing is refused.\n */\nexport function readAiAutomationAnswer(\n answer: Record<string, unknown>,\n capabilities: AiAutomationCapabilities,\n): AiGenerationCheckResult<AiAutomationAnswer> {\n const reader: Reader = { violations: [], offending: [] }\n const done = (value: AiAutomationAnswer | null): AiGenerationCheckResult<AiAutomationAnswer> => ({\n value: reader.violations.length ? null : value,\n violations: reader.violations,\n ...(reader.offending.length ? { offending: Object.fromEntries(reader.offending) } : {}),\n })\n const notes = (Array.isArray(answer['notes']) ? answer['notes'] : [])\n .map(textOf)\n .filter((note): note is string => note !== null && !HOSTILE_TEXT.test(note))\n .slice(0, AI_AUTOMATION_NOTES_MAX)\n .map((note) => note.slice(0, AI_AUTOMATION_NOTE_MAX_CHARS))\n const name = (textOf(answer['name']) ?? '').slice(0, AI_AUTOMATION_NAME_MAX_CHARS)\n const written = JSON.stringify(answer).length\n if (written > AI_AUTOMATION_ANSWER_MAX_CHARS) {\n refuse(\n reader,\n 'automation-too-long',\n `Written out, the automation is ${written} characters, over the ${AI_AUTOMATION_ANSWER_MAX_CHARS} it may be. Shorten its emails and messages, or use fewer steps.`,\n 'steps',\n written,\n )\n return done(null)\n }\n\n // An answer that builds an automation leaves `unsupported` empty, which the\n // schema states as an empty string rather than a `null`.\n const unsupported = answer['unsupported']\n if (unsupported !== null && unsupported !== undefined && unsupported !== '') {\n if (!(AI_AUTOMATION_UNSUPPORTED as readonly unknown[]).includes(unsupported)) {\n refuse(reader, 'automation-shape', 'unsupported is empty, or one of the reasons offered.', 'unsupported', unsupported)\n return done(null)\n }\n const need = AI_AUTOMATION_UNSUPPORTED_NEED[unsupported as AiAutomationUnsupported]\n if (need && capabilities[need]) {\n refuse(\n reader,\n 'automation-unsupported',\n `This workspace has ${AI_AUTOMATION_NEED_LABELS[need]}: build the automation with it.`,\n 'unsupported',\n unsupported,\n )\n return done(null)\n }\n return done({\n name,\n trigger: { event: HOST_EVENT_TYPES[0], conditions: [], combinator: 'and' },\n steps: [],\n notes,\n unsupported: unsupported as AiAutomationUnsupported,\n })\n }\n\n const trigger = answer['trigger']\n const steps = answer['steps']\n if (!isRecord(trigger) || !Array.isArray(steps)) {\n refuse(reader, 'automation-shape', 'The answer has no trigger and steps.', 'trigger', trigger)\n return done(null)\n }\n const event = trigger['event']\n if (!(HOST_EVENT_TYPES as readonly unknown[]).includes(event)) {\n refuse(reader, 'automation-trigger', 'The trigger is not one of the events offered.', 'trigger.event', event)\n return done(null)\n }\n const hostEvent = event as HostEventType\n const need = AI_AUTOMATION_TRIGGER_NEEDS[hostEvent]\n if (need && !capabilities[need]) {\n refuse(\n reader,\n 'automation-plan',\n `The trigger \"${aiAutomationTriggerLabel(hostEvent)}\" needs ${AI_AUTOMATION_NEED_LABELS[need]}, which this workspace does not have. Pick another trigger, or answer with unsupported \"needs-${need}\".`,\n 'trigger.event',\n event,\n )\n return done(null)\n }\n const rawConditions = Array.isArray(trigger['conditions']) ? trigger['conditions'] : []\n if (rawConditions.length > ACTION_MAX_CONDITIONS) {\n refuse(\n reader,\n 'automation-too-many-conditions',\n `A trigger holds at most ${ACTION_MAX_CONDITIONS} conditions.`,\n 'trigger.conditions',\n rawConditions.length,\n )\n }\n const conditions = rawConditions\n .slice(0, ACTION_MAX_CONDITIONS)\n .map((raw, index) => readCondition(reader, raw, `trigger.conditions[${index}]`, hostEvent, false))\n .filter((condition): condition is AiAutomationCondition => condition !== null)\n const combinator = (TRIGGER_COMBINATORS as readonly unknown[]).includes(trigger['combinator'])\n ? (trigger['combinator'] as TriggerCombinator)\n : 'and'\n\n if (!steps.length) {\n refuse(reader, 'automation-shape', 'An automation has at least one step; answer unsupported when none fits.', 'steps', steps)\n }\n if (steps.length > ACTION_MAX_STEPS) {\n refuse(reader, 'automation-too-many-steps', `An automation holds at most ${ACTION_MAX_STEPS} steps.`, 'steps', steps.length)\n }\n const read = steps\n .slice(0, ACTION_MAX_STEPS)\n .map((raw, index) => readStep(reader, raw, index, hostEvent, capabilities))\n .filter((step): step is AiAutomationAnswerStep => step !== null)\n\n return done({\n name,\n trigger: { event: hostEvent, conditions, combinator },\n steps: read,\n notes,\n unsupported: null,\n })\n}\n\n// ── Reading an explanation ────────────────────────────────────────────────\n\nexport interface AiWorkflowExplanation {\n summary: string\n points: string[]\n suggestions: string[]\n}\n\n/** Whether a line carries an email address, which nothing an explanation was shown holds. */\nconst CARRIES_ADDRESS = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/\n\nfunction lines(value: unknown, max: number): string[] {\n return (Array.isArray(value) ? value : [])\n .map(textOf)\n .filter((line): line is string => line !== null)\n .slice(0, max)\n .map((line) => line.slice(0, AI_WORKFLOW_LINE_MAX_CHARS))\n}\n\n/**\n * The explanation tool's answer: a summary, and at most so many points and\n * suggestions, each cut to a line. Refused when it has no summary, or carries\n * markup or an address — plain words for a person, about a record whose\n * personal details were removed before the model saw it.\n */\nexport function readAiWorkflowExplanation(\n answer: Record<string, unknown>,\n): AiGenerationCheckResult<AiWorkflowExplanation> {\n const summary = (textOf(answer['summary']) ?? '').slice(0, AI_WORKFLOW_SUMMARY_MAX_CHARS)\n const value: AiWorkflowExplanation = {\n summary,\n points: lines(answer['points'], AI_WORKFLOW_POINTS_MAX),\n suggestions: lines(answer['suggestions'], AI_WORKFLOW_SUGGESTIONS_MAX),\n }\n const violations: AiDoctrineViolation[] = []\n if (!summary) {\n violations.push({ rule: null, code: 'explanation-shape', message: 'The explanation has no summary.' })\n }\n const all = [value.summary, ...value.points, ...value.suggestions]\n if (all.some((line) => HOSTILE_TEXT.test(line))) {\n violations.push({\n rule: null,\n code: 'explanation-markup',\n message: 'Write the explanation as plain text, with no HTML or script.',\n })\n }\n if (all.some((line) => CARRIES_ADDRESS.test(line))) {\n violations.push({\n rule: null,\n code: 'explanation-address',\n message: 'The explanation names an email address. Describe the person or the teammate instead.',\n })\n }\n return { value: violations.length ? null : value, violations }\n}\n"],"names":["ACTION_MAX_CONDITIONS","ACTION_MAX_STEPS","CONTACT_TAG_MAX_LENGTH","FLOW_TIMED_OUT_FIELD","FLOW_WAIT_MAX_MINUTES","FLOW_WAIT_MIN_MINUTES","HOST_ACTION_STEP_LABELS","TRIGGER_COMBINATORS","TRIGGER_CONDITION_OPS","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_KINDS","CRM_TASK_KINDS","CRM_TASK_MAX_DUE_DAYS","HOST_EVENT_PAYLOAD_KEYS","HOST_EVENT_TYPES","AI_AUTOMATION_NEED_LABELS","AI_AUTOMATION_STEP_NEEDS","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_TRIGGER_NEEDS","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","aiAutomationTriggerLabel","HOSTILE_TEXT","AI_AUTOMATION_TOOL_NAME","AI_WORKFLOW_EXPLANATION_TOOL_NAME","AI_AUTOMATION_NAME_MAX_CHARS","AI_AUTOMATION_SUBJECT_MAX_CHARS","AI_AUTOMATION_BODY_MAX_CHARS","AI_AUTOMATION_TEXT_MAX_CHARS","AI_AUTOMATION_REFERENCE_MAX_CHARS","AI_AUTOMATION_VALUE_MAX_CHARS","AI_AUTOMATION_NOTES_MAX","AI_AUTOMATION_NOTE_MAX_CHARS","AI_AUTOMATION_ANSWER_MAX_CHARS","AI_WORKFLOW_SUMMARY_MAX_CHARS","AI_WORKFLOW_POINTS_MAX","AI_WORKFLOW_SUGGESTIONS_MAX","AI_WORKFLOW_LINE_MAX_CHARS","SEVERITIES","DEFINITIONS","condition","type","additionalProperties","required","properties","field","description","op","enum","value","event","when","items","$ref","REFERENCE_STEPS","TEXT_STEPS","REFERENCE_KEY","enrollList","assignCampaign","runWorkflow","datasetAppend","updateDataset","webhookPost","TEXT_KEY","notifyAdmins","addContactTag","assignContactOwner","siteAlert","createCrmTask","logCrmActivity","minutes","what","stepVariant","types","fields","Object","keys","AI_AUTOMATION_STEP_VARIANTS","reference","text","subject","body","toField","severity","stage","taskKind","dueInDays","activityKind","aiAutomationTool","name","strict","inputSchema","trigger","conditions","combinator","steps","action","anyOf","notes","unsupported","$defs","aiWorkflowExplanationTool","summary","points","suggestions","AI_AUTOMATION_UNREADABLE_CODES","AI_AUTOMATION_OVERSIZE_CODES","DEFAULT_TO_FIELD","FIELD_NAME","EMAIL_ADDRESS","ROUND_ROBIN","isRecord","Array","isArray","textOf","clean","replace","trim","fieldsFor","length","refuse","reader","code","message","path","violations","push","rule","paths","offending","readCondition","raw","guard","test","includes","readable","join","lifecycleStageOf","wanted","toLowerCase","find","requireText","step","key","max","wholeNumber","Number","isInteger","readStep","index","capabilities","stepType","need","before","list","campaign","workflow","webhook","dataset","title","tag","owner","guards","undefined","shared","own","waited","kind","due","readAiAutomationAnswer","answer","done","fromEntries","map","filter","note","slice","written","JSON","stringify","hostEvent","rawConditions","read","CARRIES_ADDRESS","lines","line","readAiWorkflowExplanation","all","some"],"mappings":"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"}
@@ -109,6 +109,9 @@ async function recipientsFor(firestore, orgId, standing, reachable) {
109
109
  const link = input.orgSlug ? `${buildRoute(Route.MANAGE_BILLING_USAGE, {
110
110
  orgSlug: input.orgSlug
111
111
  })}#ai-allotments` : null;
112
+ // `skipEmail`: the email below is this crossing's email, so the
113
+ // per-person channel does not send a second one to anybody who switched
114
+ // billing email on (AGL-3431).
112
115
  await notify(recipients.managerUids, _extends({
113
116
  type: 'billing.usage',
114
117
  title: copy.title,
@@ -116,7 +119,9 @@ async function recipientsFor(firestore, orgId, standing, reachable) {
116
119
  orgId: input.orgId
117
120
  }, link ? {
118
121
  link
119
- } : {}));
122
+ } : {}), {
123
+ skipEmail: true
124
+ });
120
125
  if (recipients.subjectUid && !recipients.managerUids.includes(recipients.subjectUid)) {
121
126
  await notify([
122
127
  recipients.subjectUid
@@ -125,6 +130,8 @@ async function recipientsFor(firestore, orgId, standing, reachable) {
125
130
  title: copy.title,
126
131
  body: copy.body,
127
132
  orgId: input.orgId
133
+ }, {
134
+ skipEmail: true
128
135
  });
129
136
  }
130
137
  const origin = consoleOrigin();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-allotment-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport { meterPlatformEmail } from '@aglyn/tenant-data-admin/server/email-metering'\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport {\n aiAllotmentAlertCopy,\n type AiAllotmentStanding,\n type AiAllotmentThreshold,\n} from '../model/ai-allotments'\nimport { aiAllotmentRef } from './ai-allotments'\n\n/**\n * A SOFT allotment's 80% and 100% (AGL-2942), through the alert pipeline's\n * two channels: the console notification every usage threshold writes\n * (`billing.usage`, to `users/{uid}/notifications`) and the email beside it,\n * sent past the platform suppression list and metered as platform mail —\n * the same primitives the usage-alert sweep composes.\n *\n * Told when the request that finds the subject at a threshold is admitted,\n * rather than on the next daily sweep: an allotment is a line a manager drew\n * for one person or one site, and a day's delay is most of what a soft line\n * is for. The sweep is untouched.\n *\n * ## Who is told\n *\n * The workspace's owners and admins — the audience of every usage alert —\n * and, for a member's or a collaborator's allotment, the person it is for.\n * The person gets the words without the Billing link, which a collaborator\n * cannot open.\n *\n * ## Once per threshold per month\n *\n * The marker is `alerted: { month, threshold }` on the allotment itself,\n * claimed in a transaction before anything is sent, so two requests at the\n * line cannot both announce it. A write to the allotment clears the marker,\n * so a raised allotment announces its own next crossing.\n */\n\n/** A crossing to announce, as the reservation measured it. */\nexport interface AiAllotmentAlertInput {\n orgId: string\n /** The workspace's slug, for the Billing link; `null` omits the link. */\n orgSlug: string | null\n month: string\n alerts: ReadonlyArray<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The pipeline's primitives, replaceable in a spec. */\nexport interface AiAllotmentAlertChannels {\n notify: typeof notifyUsers\n send: typeof sendEmail\n filter: typeof filterSuppressedEmails\n meter: typeof meterPlatformEmail\n /** The uids given, minus locked or disabled accounts (AGL-3418). */\n reachable: typeof withoutMailWithheldAccounts\n}\n\nconst DEFAULT_CHANNELS: AiAllotmentAlertChannels = {\n notify: notifyUsers,\n send: sendEmail,\n filter: filterSuppressedEmails,\n meter: meterPlatformEmail,\n reachable: withoutMailWithheldAccounts,\n}\n\n/** The console's absolute origin for an email link, or `''` when unset. */\nfunction consoleOrigin(): string {\n return (process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim().replace(/\\/+$/, '')\n}\n\n/** Take the marker for `threshold` this month; `false` when it was taken. */\nexport async function claimAiAllotmentAlert(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n subject: string,\n month: string,\n threshold: AiAllotmentThreshold,\n): Promise<boolean> {\n const ref = aiAllotmentRef(firestore.collection('orgs').doc(orgId), subject)\n return firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n if (!snapshot.exists) return false\n const alerted = snapshot.get('alerted') as { month?: unknown; threshold?: unknown } | null\n if (alerted?.month === month && Number(alerted?.threshold) >= threshold) return false\n tx.set(ref, { alerted: { month, threshold } }, { merge: true })\n return true\n })\n}\n\ninterface Recipients {\n managerUids: string[]\n managerEmails: string[]\n subjectUid: string | null\n subjectEmail: string | null\n subjectName: string\n}\n\nasync function recipientsFor(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n standing: AiAllotmentStanding,\n reachable: AiAllotmentAlertChannels['reachable'],\n): Promise<Recipients> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const members = await orgRef.collection('members').get()\n // A locked or disabled account is told nothing, by either channel; it is\n // still named when the alert is about it.\n const open = new Set(await reachable(members.docs.map((member) => member.id)))\n const managerUids: string[] = []\n const managerEmails: string[] = []\n let subjectEmail: string | null = null\n let subjectName = ''\n for (const member of members.docs) {\n const role = member.get('role')\n const email = String(member.get('email') ?? '').trim()\n if ((role === 'owner' || role === 'admin') && open.has(member.id)) {\n managerUids.push(member.id)\n if (email.includes('@')) managerEmails.push(email)\n }\n if (standing.uid && member.id === standing.uid) {\n subjectEmail = email.includes('@') && open.has(member.id) ? email : null\n subjectName = String(member.get('displayName') ?? '').trim() || email\n }\n }\n if (standing.scope === 'host' && standing.hostId) {\n const host = await firestore.collection('hosts').doc(standing.hostId).get()\n subjectName =\n String(host.get('displayName') ?? '').trim() ||\n String(host.get('subdomain') ?? '').trim()\n }\n return {\n managerUids,\n managerEmails,\n subjectUid:\n standing.scope === 'host' || !standing.uid || !open.has(standing.uid)\n ? null\n : standing.uid,\n subjectEmail: standing.scope === 'host' ? null : subjectEmail,\n subjectName: subjectName || (standing.scope === 'host' ? 'this' : 'A member'),\n }\n}\n\n/**\n * Announce every crossing the reservation found. Never throws, and answers\n * how many were announced — one failing crossing does not stop the next.\n */\nexport async function announceAiAllotmentAlerts(\n firestore: FirebaseFirestore.Firestore,\n input: AiAllotmentAlertInput,\n channels: Partial<AiAllotmentAlertChannels> = {},\n): Promise<number> {\n const { notify, send, filter, meter, reachable } = { ...DEFAULT_CHANNELS, ...channels }\n let announced = 0\n for (const { standing, threshold } of input.alerts) {\n try {\n const claimed = await claimAiAllotmentAlert(\n firestore,\n input.orgId,\n standing.subject,\n input.month,\n threshold,\n )\n if (!claimed) continue\n const recipients = await recipientsFor(firestore, input.orgId, standing, reachable)\n const copy = aiAllotmentAlertCopy({\n scope: standing.scope,\n threshold,\n used: standing.used,\n credits: standing.credits,\n name: recipients.subjectName,\n })\n const link = input.orgSlug\n ? `${buildRoute(Route.MANAGE_BILLING_USAGE, { orgSlug: input.orgSlug })}#ai-allotments`\n : null\n await notify(recipients.managerUids, {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n orgId: input.orgId,\n ...(link ? { link } : {}),\n })\n if (recipients.subjectUid && !recipients.managerUids.includes(recipients.subjectUid)) {\n await notify([recipients.subjectUid], {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n orgId: input.orgId,\n })\n }\n const origin = consoleOrigin()\n const managerText = link && origin ? `${copy.body}\\n\\n${origin}${link}` : copy.body\n const managers = await filter(recipients.managerEmails)\n if (managers.length) {\n const result = await send({\n to: managers,\n subject: copy.title,\n text: managerText,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n const subjectAddress = recipients.subjectEmail\n ? (await filter([recipients.subjectEmail])).filter(\n (address) => !managers.includes(address),\n )\n : []\n if (subjectAddress.length) {\n const result = await send({\n to: subjectAddress,\n subject: copy.title,\n text: copy.body,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n announced += 1\n } catch (error) {\n console.error('[ai-allotments] announcing a crossing failed', input.orgId, standing.subject, error)\n }\n }\n return announced\n}\n"],"names":["buildRoute","Route","sendEmail","withoutMailWithheldAccounts","meterPlatformEmail","filterSuppressedEmails","notifyUsers","aiAllotmentAlertCopy","aiAllotmentRef","DEFAULT_CHANNELS","notify","send","filter","meter","reachable","consoleOrigin","process","env","NEXT_PUBLIC_CONSOLE_URL","trim","replace","claimAiAllotmentAlert","firestore","orgId","subject","month","threshold","ref","collection","doc","runTransaction","tx","snapshot","get","exists","alerted","Number","set","merge","recipientsFor","standing","orgRef","members","open","Set","docs","map","member","id","managerUids","managerEmails","subjectEmail","subjectName","role","email","String","has","push","includes","uid","scope","hostId","host","subjectUid","announceAiAllotmentAlerts","input","channels","announced","alerts","claimed","recipients","copy","used","credits","name","link","orgSlug","MANAGE_BILLING_USAGE","type","title","body","origin","managerText","managers","length","result","to","text","context","sent","catch","undefined","subjectAddress","address","error","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-allotment-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport { meterPlatformEmail } from '@aglyn/tenant-data-admin/server/email-metering'\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport {\n aiAllotmentAlertCopy,\n type AiAllotmentStanding,\n type AiAllotmentThreshold,\n} from '../model/ai-allotments'\nimport { aiAllotmentRef } from './ai-allotments'\n\n/**\n * A SOFT allotment's 80% and 100% (AGL-2942), through the alert pipeline's\n * two channels: the console notification every usage threshold writes\n * (`billing.usage`, to `users/{uid}/notifications`) and the email beside it,\n * sent past the platform suppression list and metered as platform mail —\n * the same primitives the usage-alert sweep composes.\n *\n * Told when the request that finds the subject at a threshold is admitted,\n * rather than on the next daily sweep: an allotment is a line a manager drew\n * for one person or one site, and a day's delay is most of what a soft line\n * is for. The sweep is untouched.\n *\n * ## Who is told\n *\n * The workspace's owners and admins — the audience of every usage alert —\n * and, for a member's or a collaborator's allotment, the person it is for.\n * The person gets the words without the Billing link, which a collaborator\n * cannot open.\n *\n * ## Once per threshold per month\n *\n * The marker is `alerted: { month, threshold }` on the allotment itself,\n * claimed in a transaction before anything is sent, so two requests at the\n * line cannot both announce it. A write to the allotment clears the marker,\n * so a raised allotment announces its own next crossing.\n */\n\n/** A crossing to announce, as the reservation measured it. */\nexport interface AiAllotmentAlertInput {\n orgId: string\n /** The workspace's slug, for the Billing link; `null` omits the link. */\n orgSlug: string | null\n month: string\n alerts: ReadonlyArray<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The pipeline's primitives, replaceable in a spec. */\nexport interface AiAllotmentAlertChannels {\n notify: typeof notifyUsers\n send: typeof sendEmail\n filter: typeof filterSuppressedEmails\n meter: typeof meterPlatformEmail\n /** The uids given, minus locked or disabled accounts (AGL-3418). */\n reachable: typeof withoutMailWithheldAccounts\n}\n\nconst DEFAULT_CHANNELS: AiAllotmentAlertChannels = {\n notify: notifyUsers,\n send: sendEmail,\n filter: filterSuppressedEmails,\n meter: meterPlatformEmail,\n reachable: withoutMailWithheldAccounts,\n}\n\n/** The console's absolute origin for an email link, or `''` when unset. */\nfunction consoleOrigin(): string {\n return (process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim().replace(/\\/+$/, '')\n}\n\n/** Take the marker for `threshold` this month; `false` when it was taken. */\nexport async function claimAiAllotmentAlert(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n subject: string,\n month: string,\n threshold: AiAllotmentThreshold,\n): Promise<boolean> {\n const ref = aiAllotmentRef(firestore.collection('orgs').doc(orgId), subject)\n return firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n if (!snapshot.exists) return false\n const alerted = snapshot.get('alerted') as { month?: unknown; threshold?: unknown } | null\n if (alerted?.month === month && Number(alerted?.threshold) >= threshold) return false\n tx.set(ref, { alerted: { month, threshold } }, { merge: true })\n return true\n })\n}\n\ninterface Recipients {\n managerUids: string[]\n managerEmails: string[]\n subjectUid: string | null\n subjectEmail: string | null\n subjectName: string\n}\n\nasync function recipientsFor(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n standing: AiAllotmentStanding,\n reachable: AiAllotmentAlertChannels['reachable'],\n): Promise<Recipients> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const members = await orgRef.collection('members').get()\n // A locked or disabled account is told nothing, by either channel; it is\n // still named when the alert is about it.\n const open = new Set(await reachable(members.docs.map((member) => member.id)))\n const managerUids: string[] = []\n const managerEmails: string[] = []\n let subjectEmail: string | null = null\n let subjectName = ''\n for (const member of members.docs) {\n const role = member.get('role')\n const email = String(member.get('email') ?? '').trim()\n if ((role === 'owner' || role === 'admin') && open.has(member.id)) {\n managerUids.push(member.id)\n if (email.includes('@')) managerEmails.push(email)\n }\n if (standing.uid && member.id === standing.uid) {\n subjectEmail = email.includes('@') && open.has(member.id) ? email : null\n subjectName = String(member.get('displayName') ?? '').trim() || email\n }\n }\n if (standing.scope === 'host' && standing.hostId) {\n const host = await firestore.collection('hosts').doc(standing.hostId).get()\n subjectName =\n String(host.get('displayName') ?? '').trim() ||\n String(host.get('subdomain') ?? '').trim()\n }\n return {\n managerUids,\n managerEmails,\n subjectUid:\n standing.scope === 'host' || !standing.uid || !open.has(standing.uid)\n ? null\n : standing.uid,\n subjectEmail: standing.scope === 'host' ? null : subjectEmail,\n subjectName: subjectName || (standing.scope === 'host' ? 'this' : 'A member'),\n }\n}\n\n/**\n * Announce every crossing the reservation found. Never throws, and answers\n * how many were announced — one failing crossing does not stop the next.\n */\nexport async function announceAiAllotmentAlerts(\n firestore: FirebaseFirestore.Firestore,\n input: AiAllotmentAlertInput,\n channels: Partial<AiAllotmentAlertChannels> = {},\n): Promise<number> {\n const { notify, send, filter, meter, reachable } = { ...DEFAULT_CHANNELS, ...channels }\n let announced = 0\n for (const { standing, threshold } of input.alerts) {\n try {\n const claimed = await claimAiAllotmentAlert(\n firestore,\n input.orgId,\n standing.subject,\n input.month,\n threshold,\n )\n if (!claimed) continue\n const recipients = await recipientsFor(firestore, input.orgId, standing, reachable)\n const copy = aiAllotmentAlertCopy({\n scope: standing.scope,\n threshold,\n used: standing.used,\n credits: standing.credits,\n name: recipients.subjectName,\n })\n const link = input.orgSlug\n ? `${buildRoute(Route.MANAGE_BILLING_USAGE, { orgSlug: input.orgSlug })}#ai-allotments`\n : null\n // `skipEmail`: the email below is this crossing's email, so the\n // per-person channel does not send a second one to anybody who switched\n // billing email on (AGL-3431).\n await notify(\n recipients.managerUids,\n {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n orgId: input.orgId,\n ...(link ? { link } : {}),\n },\n { skipEmail: true },\n )\n if (recipients.subjectUid && !recipients.managerUids.includes(recipients.subjectUid)) {\n await notify(\n [recipients.subjectUid],\n {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n orgId: input.orgId,\n },\n { skipEmail: true },\n )\n }\n const origin = consoleOrigin()\n const managerText = link && origin ? `${copy.body}\\n\\n${origin}${link}` : copy.body\n const managers = await filter(recipients.managerEmails)\n if (managers.length) {\n const result = await send({\n to: managers,\n subject: copy.title,\n text: managerText,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n const subjectAddress = recipients.subjectEmail\n ? (await filter([recipients.subjectEmail])).filter(\n (address) => !managers.includes(address),\n )\n : []\n if (subjectAddress.length) {\n const result = await send({\n to: subjectAddress,\n subject: copy.title,\n text: copy.body,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n announced += 1\n } catch (error) {\n console.error('[ai-allotments] announcing a crossing failed', input.orgId, standing.subject, error)\n }\n }\n return announced\n}\n"],"names":["buildRoute","Route","sendEmail","withoutMailWithheldAccounts","meterPlatformEmail","filterSuppressedEmails","notifyUsers","aiAllotmentAlertCopy","aiAllotmentRef","DEFAULT_CHANNELS","notify","send","filter","meter","reachable","consoleOrigin","process","env","NEXT_PUBLIC_CONSOLE_URL","trim","replace","claimAiAllotmentAlert","firestore","orgId","subject","month","threshold","ref","collection","doc","runTransaction","tx","snapshot","get","exists","alerted","Number","set","merge","recipientsFor","standing","orgRef","members","open","Set","docs","map","member","id","managerUids","managerEmails","subjectEmail","subjectName","role","email","String","has","push","includes","uid","scope","hostId","host","subjectUid","announceAiAllotmentAlerts","input","channels","announced","alerts","claimed","recipients","copy","used","credits","name","link","orgSlug","MANAGE_BILLING_USAGE","type","title","body","skipEmail","origin","managerText","managers","length","result","to","text","context","sent","catch","undefined","subjectAddress","address","error","console"],"mappings":"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"}
@@ -16,7 +16,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
16
16
  * limitations under the License.
17
17
  */ import { FieldValue } from "firebase-admin/firestore";
18
18
  import { assistUsdFromCredits } from "@aglyn/aglyn/app-utils/assist-credits";
19
- import { FREE_AI_TASTE_CREDITS_PER_MONTH, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
+ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
+ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
20
21
  import { sendStaffAlertEmail } from "@aglyn/tenant-data-admin/server/staff-alert-email";
21
22
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
22
23
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/assist-free-taste.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n assistUsdFromCredits,\n type AssistRefusedBy,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport {\n FREE_AI_TASTE_CREDITS_PER_MONTH,\n resolveEffectivePlan,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { sendStaffAlertEmail } from '@aglyn/tenant-data-admin/server/staff-alert-email'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n/**\n * The Free AI taste's precautions (AGL-2925) — the half of the meter that\n * exists only because the Free band has no invoice behind it.\n *\n * `FREE_AI_TASTE_CREDITS_PER_MONTH` gives every Free workspace 300 credits a\n * month as a wall. A wall per WORKSPACE is not a bound per PERSON: one\n * account may hold three free workspaces (AGL-2265), a script can mint\n * accounts faster than a person can, and a refused brief costs the same\n * tokens as an answered one. So the taste is metered a second time, per\n * account, and bounded three more ways. Everything below is read and\n * decided INSIDE `reserveAssistMessage`'s transaction, so a burst of\n * concurrent requests cannot each read \"under\" and all proceed — the same\n * property every other rung of that reservation has.\n *\n * ## Documents (both ABSENT from firebase-firestore.rules on purpose)\n *\n * users/{uid}/aiUsage/{YYYY-MM} the ACCOUNT's month:\n * { month, estCostUsd, requests, days: { [YYYY-MM-DD]: { requests,\n * refusals } }, updatedAt }\n * platformAiFreeSpend/{YYYY-MM-DD} the PLATFORM's day of free spend:\n * { day, estCostUsd, requests, refusals, alertedAt?, pausedAt?,\n * updatedAt }\n *\n * The account document sits under `users/{uid}`, whose owner may read and\n * write the user document itself. It may not touch this subcollection: the\n * `users` block matches its subcollections BY NAME and carries no wildcard,\n * so an unmatched name is default-deny for every client, staff included —\n * the same property `assistUsage` relies on one level down. An owner who\n * could write here could reset their own allowance, which is the whole\n * meter. The platform document is a top-level collection alongside\n * `platformCronBeats`, unmatched and therefore closed, and every reader is\n * an Admin-SDK reader behind the staff gate.\n *\n * ## Whose account\n *\n * The WORKSPACE OWNER's. A member invited to someone else's free workspace\n * draws from that workspace's band and from its owner's allowance, never\n * from their own account: the owner is the person the free workspace is\n * attributed to for the ceiling on how many they may hold, and attributing\n * the spend the same way is what keeps the two bounds describing one\n * person. `ownerUid` first, `createdByUid` for an org that predates\n * ownership stamping — the same union the free-workspace count reads.\n */\n\n/** Where an account's month of free AI spend lives, under `users/{uid}`. */\nexport const FREE_AI_ACCOUNT_USAGE_COLLECTION = 'aiUsage'\n\n/** The top-level collection holding one document per UTC day of free spend. */\nexport const PLATFORM_AI_FREE_SPEND_COLLECTION = 'platformAiFreeSpend'\n\n/**\n * Refusals a day before an account's free generation pauses. Three, not\n * one: a single declined brief is a normal outcome of asking an assistant\n * for things, and pausing on it would punish an ordinary afternoon. Three\n * in a day is somebody probing what the model will write.\n */\nexport const AI_FREE_REFUSALS_PER_DAY = 3\n\n/** The share of the platform ceiling at which staff are told, once a day. */\nexport const AI_FREE_PLATFORM_ALERT_SHARE = 0.8\n\n/** The account allowance, in USD of provider spend — the taste, per person. */\nexport const FREE_AI_ACCOUNT_BUDGET_USD = assistUsdFromCredits(\n FREE_AI_TASTE_CREDITS_PER_MONTH,\n)\n\n/**\n * Free requests one account may make a UTC day, across its workspaces\n * (`AI_FREE_DAILY_REQUESTS`, default 30).\n *\n * A request, not a message: this counts every reservation the account's\n * free workspaces take, at either door. Thirty is three times the\n * per-workspace message cap, so one workspace never hits it before its own\n * cap and three workspaces hit it exactly when their caps would have\n * summed — it bounds the multiplier, not the ordinary day. Zero is honored\n * as \"no free requests\", which is a decision an operator may write down;\n * junk and an empty value take the default.\n */\nexport function aiFreeDailyRequests(): number {\n const raw = process.env.AI_FREE_DAILY_REQUESTS\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 30\n}\n\n/**\n * The platform-wide ceiling on ONE UTC day of free-tier provider spend\n * (`AI_FREE_DAILY_PLATFORM_CEILING_USD`, default $25).\n *\n * Every other bound here is per something — per workspace, per account,\n * per address — and a wave of new accounts clears all of them one at a\n * time. This is the bound on the sum. At 80% of it staff are mailed; at\n * 100% every free reservation refuses until the day rolls, paid workspaces\n * untouched, and the manual `ai-generate` lockdown key remains the switch\n * for anything the ceiling did not catch.\n *\n * Fails to the DEFAULT, never to \"no ceiling\", for the reason the operator\n * COGS limit does: an empty, negative, zero or unparseable value is not a\n * decision to run unbounded. There is deliberately no `off` word — a\n * deployment that wants no ceiling sets a figure it is content to spend.\n */\nexport function aiFreeDailyPlatformCeilingUsd(): number {\n const raw = String(process.env.AI_FREE_DAILY_PLATFORM_CEILING_USD ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 25\n}\n\n/** The account a free workspace's spend is attributed to. */\nexport interface FreeAssistAccount {\n /**\n * The workspace owner's uid, or `null` for an org document that names no\n * owner and no creator — every org created since ownership was stamped\n * names one, so `null` is the shape of a fixture or a very old record.\n * The account rungs are skipped for it; the org's own band still binds.\n */\n accountUid: string | null\n}\n\n/** The org fields the attribution reads, beside the billing ones. */\nexport type AssistMeteredOrg = Partial<AglynOrgBilling> & {\n ownerUid?: string | null\n createdByUid?: string | null\n}\n\n/**\n * The account to meter a workspace's free spend against, or `null` when the\n * workspace is not on the Free plan and none of this applies.\n *\n * `resolveEffectivePlan`, not `org.plan`: a workspace whose subscription\n * died is Free again, and its spend is the taste's spend again.\n */\nexport function freeAssistAccount(\n org: AssistMeteredOrg | null | undefined,\n): FreeAssistAccount | null {\n if (resolveEffectivePlan(org) !== 'free') return null\n const owner = String(org?.ownerUid ?? '').trim()\n const creator = String(org?.createdByUid ?? '').trim()\n return { accountUid: owner || creator || null }\n}\n\nexport function freeAccountUsageRef(\n firestore: FirebaseFirestore.Firestore,\n accountUid: string,\n month: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('users')\n .doc(accountUid)\n .collection(FREE_AI_ACCOUNT_USAGE_COLLECTION)\n .doc(month)\n}\n\nexport function platformFreeSpendRef(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): FirebaseFirestore.DocumentReference {\n return firestore.collection(PLATFORM_AI_FREE_SPEND_COLLECTION).doc(day)\n}\n\n/** What the reservation read about the account and the platform. */\nexport interface FreeTasteReads {\n accountCostUsd: number\n accountRequestsToday: number\n accountRefusalsToday: number\n platformCostUsd: number\n platformPaused: boolean\n}\n\n/** The two snapshots, as the transaction handed them over, into figures. */\nexport function freeTasteReadsFrom(\n account: FirebaseFirestore.DocumentSnapshot | null,\n platform: FirebaseFirestore.DocumentSnapshot | null,\n day: string,\n): FreeTasteReads {\n const days = (account?.get('days') ?? {}) as Record<\n string,\n { requests?: unknown; refusals?: unknown } | undefined\n >\n const today = days[day] ?? {}\n return {\n accountCostUsd: Number(account?.get('estCostUsd') ?? 0),\n accountRequestsToday: Number(today.requests ?? 0),\n accountRefusalsToday: Number(today.refusals ?? 0),\n platformCostUsd: Number(platform?.get('estCostUsd') ?? 0),\n platformPaused: Boolean(platform?.get('pausedAt')),\n }\n}\n\n/**\n * Which of the taste's own precautions refuses, or `null` when none does —\n * pure, so the spec can force every rung red without a Firestore.\n *\n * Checked in the order the reads are cheapest to explain: the daily\n * request cap and the refusal pause (both reset on a clock the customer\n * can be told about), then the account's month (an upgrade is the way\n * out), then the platform ceiling (nobody's doing; try again tomorrow).\n * The org's own rungs — its daily message cap, its band — run before this\n * in the reservation, so a workspace at its own band hears about its own\n * band rather than about its owner's other workspaces.\n *\n * Every comparison is `>=` against a count the reservation is about to\n * increment, so a cap of N admits exactly N. A paused platform refuses\n * regardless of the running figure: the pause is a decision recorded on\n * the document, and a ceiling raised by an operator mid-day does not undo\n * it — the day rolls, or staff clear the document.\n */\nexport function freeTasteRefusal(\n reads: FreeTasteReads,\n limits: {\n dailyRequests?: number\n refusalsPerDay?: number\n accountBudgetUsd?: number\n platformCeilingUsd?: number\n } = {},\n): Extract<AssistRefusedBy, 'requests' | 'refusals' | 'account' | 'platform'> | null {\n const dailyRequests = limits.dailyRequests ?? aiFreeDailyRequests()\n const refusalsPerDay = limits.refusalsPerDay ?? AI_FREE_REFUSALS_PER_DAY\n const accountBudgetUsd = limits.accountBudgetUsd ?? FREE_AI_ACCOUNT_BUDGET_USD\n const platformCeilingUsd =\n limits.platformCeilingUsd ?? aiFreeDailyPlatformCeilingUsd()\n if (reads.accountRequestsToday >= dailyRequests) return 'requests'\n if (reads.accountRefusalsToday >= refusalsPerDay) return 'refusals'\n if (reads.accountCostUsd >= accountBudgetUsd) return 'account'\n if (reads.platformPaused || reads.platformCostUsd >= platformCeilingUsd) {\n return 'platform'\n }\n return null\n}\n\n/**\n * The account-side write one admitted free reservation owes: a request\n * counted for the month and for the day. `set(…, { merge: true })` with\n * nested increments, so the first request of a month conjures the document\n * and a day's map entry appears the first time that day is counted.\n */\nexport function freeAccountReservationWrite(\n month: string,\n day: string,\n): Record<string, unknown> {\n const increment = FieldValue.increment\n return {\n month,\n requests: increment(1),\n days: { [day]: { requests: increment(1) } },\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/**\n * The two writes one metered free turn owes, beside the org's own rollup.\n *\n * A `refusal` stop is the one case where the meters disagree on purpose. The\n * tokens were spent, so the PLATFORM document — which bounds our money —\n * takes the cost and counts the refusal. The ACCOUNT's credits do not move:\n * a refusal never draws credits, because a customer whose brief was\n * declined has not been given anything, and because a run of declined\n * briefs is answered by the refusal pause rather than by an emptied band.\n * The account's day counts the refusal, which is what the pause reads.\n */\nexport function freeTasteMeterWrites(input: {\n estCostUsd: number\n refused: boolean\n day: string\n month: string\n}): { account: Record<string, unknown>; platform: Record<string, unknown> } {\n const increment = FieldValue.increment\n const serverTimestamp = FieldValue.serverTimestamp\n const { estCostUsd, refused, day, month } = input\n return {\n account: refused\n ? {\n month,\n days: { [day]: { refusals: increment(1) } },\n updatedAt: serverTimestamp(),\n }\n : { month, estCostUsd: increment(estCostUsd), updatedAt: serverTimestamp() },\n platform: {\n day,\n estCostUsd: increment(estCostUsd),\n requests: increment(1),\n refusals: increment(refused ? 1 : 0),\n updatedAt: serverTimestamp(),\n },\n }\n}\n\n/**\n * After a free turn is metered: tell staff at 80% of the day's ceiling,\n * and record the pause at 100% — each ONCE per UTC day, deduplicated on\n * the day document itself inside a transaction, so two turns landing\n * together cannot both cross a threshold and both announce it.\n *\n * The refusal at the ceiling is NOT decided here; `freeTasteRefusal` reads\n * the figure inside every reservation and refuses on it directly. This\n * only says so out loud: the email, and an `adminAudit` row under\n * `platform.aiFreeSpend.paused` so the staff audit trail shows the day the\n * taste switched itself off. Both are best-effort and neither can fail the\n * turn that triggered them — the turn is already paid for.\n *\n * Returns what it announced, for the spec and for the log.\n */\nexport async function announcePlatformFreeSpend(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): Promise<{ alerted: boolean; paused: boolean; estCostUsd: number }> {\n const ceilingUsd = aiFreeDailyPlatformCeilingUsd()\n const ref = platformFreeSpendRef(firestore, day)\n const crossed = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const estCostUsd = Number(snapshot.get('estCostUsd') ?? 0)\n const alert =\n !snapshot.get('alertedAt') &&\n estCostUsd >= ceilingUsd * AI_FREE_PLATFORM_ALERT_SHARE\n const pause = !snapshot.get('pausedAt') && estCostUsd >= ceilingUsd\n if (alert || pause) {\n tx.set(\n ref,\n {\n ...(alert ? { alertedAt: FieldValue.serverTimestamp() } : {}),\n ...(pause ? { pausedAt: FieldValue.serverTimestamp() } : {}),\n },\n { merge: true },\n )\n }\n return { alerted: alert, paused: pause, estCostUsd }\n })\n if (!crossed.alerted && !crossed.paused) return crossed\n\n const figure = `$${crossed.estCostUsd.toFixed(2)} of the $${ceilingUsd.toFixed(2)} ceiling`\n if (crossed.paused) {\n await addAdminAudit(firestore, {\n actorUid: 'system:ai-free-spend',\n action: 'platform.aiFreeSpend.paused',\n target: `${PLATFORM_AI_FREE_SPEND_COLLECTION}/${day}`,\n after: { day, estCostUsd: crossed.estCostUsd, ceilingUsd },\n at: FieldValue.serverTimestamp(),\n })\n .catch((error) =>\n console.error('[ai-free-spend] audit row failed', error),\n )\n }\n await sendStaffAlertEmail({\n subject: crossed.paused\n ? `Free AI generation paused for ${day}: ${figure}`\n : `Free AI spend at 80% of today's ceiling (${day}): ${figure}`,\n text: crossed.paused\n ? `Free-tier AI spend for ${day} (UTC) reached ${figure} ` +\n '(AI_FREE_DAILY_PLATFORM_CEILING_USD). Every Free workspace is ' +\n 'refused AI generation until the UTC day rolls; paid workspaces are ' +\n 'unaffected. If this is an abuse wave rather than organic use, the ' +\n '`ai-generate` feature lock on Staff → Lockdown stops generation ' +\n 'for everyone, and the assist signals page shows which workspaces ' +\n 'spent it.'\n : `Free-tier AI spend for ${day} (UTC) is at ${figure} ` +\n '(AI_FREE_DAILY_PLATFORM_CEILING_USD). At 100% every Free ' +\n 'workspace is refused AI generation until the day rolls. The ' +\n 'assist signals page shows the day so far; the `ai-generate` ' +\n 'feature lock on Staff → Lockdown is the manual stop.',\n context: 'ai-free-spend',\n })\n return crossed\n}\n\n/** The staff readout of today's free spend, for the assist signals page. */\nexport interface PlatformFreeSpendReadout {\n day: string\n estCostUsd: number\n requests: number\n refusals: number\n ceilingUsd: number\n /** True once the 80% mail went out today. */\n alerted: boolean\n /** True while free generation is refused for the day. */\n paused: boolean\n}\n\nexport async function readPlatformFreeSpend(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): Promise<PlatformFreeSpendReadout> {\n const snapshot = await platformFreeSpendRef(firestore, day).get()\n const ceilingUsd = aiFreeDailyPlatformCeilingUsd()\n const estCostUsd = Number(snapshot.get('estCostUsd') ?? 0)\n return {\n day,\n estCostUsd,\n requests: Number(snapshot.get('requests') ?? 0),\n refusals: Number(snapshot.get('refusals') ?? 0),\n ceilingUsd,\n alerted: Boolean(snapshot.get('alertedAt')),\n // The pause is the recorded decision OR the live figure: a day that\n // crossed the ceiling before anything announced it is paused all the\n // same, because the reservation refuses on the figure.\n paused: Boolean(snapshot.get('pausedAt')) || estCostUsd >= ceilingUsd,\n }\n}\n"],"names":["FieldValue","assistUsdFromCredits","FREE_AI_TASTE_CREDITS_PER_MONTH","resolveEffectivePlan","sendStaffAlertEmail","addAdminAudit","FREE_AI_ACCOUNT_USAGE_COLLECTION","PLATFORM_AI_FREE_SPEND_COLLECTION","AI_FREE_REFUSALS_PER_DAY","AI_FREE_PLATFORM_ALERT_SHARE","FREE_AI_ACCOUNT_BUDGET_USD","aiFreeDailyRequests","raw","process","env","AI_FREE_DAILY_REQUESTS","parsed","Number","NaN","isFinite","Math","floor","aiFreeDailyPlatformCeilingUsd","String","AI_FREE_DAILY_PLATFORM_CEILING_USD","trim","freeAssistAccount","org","owner","ownerUid","creator","createdByUid","accountUid","freeAccountUsageRef","firestore","month","collection","doc","platformFreeSpendRef","day","freeTasteReadsFrom","account","platform","days","today","get","accountCostUsd","accountRequestsToday","requests","accountRefusalsToday","refusals","platformCostUsd","platformPaused","Boolean","freeTasteRefusal","reads","limits","dailyRequests","refusalsPerDay","accountBudgetUsd","platformCeilingUsd","freeAccountReservationWrite","increment","updatedAt","serverTimestamp","freeTasteMeterWrites","input","estCostUsd","refused","announcePlatformFreeSpend","ceilingUsd","ref","crossed","runTransaction","tx","snapshot","alert","pause","set","alertedAt","pausedAt","merge","alerted","paused","figure","toFixed","actorUid","action","target","after","at","catch","error","console","subject","text","context","readPlatformFreeSpend"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/assist-free-taste.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n assistUsdFromCredits,\n type AssistRefusedBy,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { sendStaffAlertEmail } from '@aglyn/tenant-data-admin/server/staff-alert-email'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\n\n/**\n * The Free AI taste's precautions (AGL-2925) — the half of the meter that\n * exists only because the Free band has no invoice behind it.\n *\n * `FREE_AI_TASTE_CREDITS_PER_MONTH` gives every Free workspace 300 credits a\n * month as a wall. A wall per WORKSPACE is not a bound per PERSON: one\n * account may hold three free workspaces (AGL-2265), a script can mint\n * accounts faster than a person can, and a refused brief costs the same\n * tokens as an answered one. So the taste is metered a second time, per\n * account, and bounded three more ways. Everything below is read and\n * decided INSIDE `reserveAssistMessage`'s transaction, so a burst of\n * concurrent requests cannot each read \"under\" and all proceed — the same\n * property every other rung of that reservation has.\n *\n * ## Documents (both ABSENT from firebase-firestore.rules on purpose)\n *\n * users/{uid}/aiUsage/{YYYY-MM} the ACCOUNT's month:\n * { month, estCostUsd, requests, days: { [YYYY-MM-DD]: { requests,\n * refusals } }, updatedAt }\n * platformAiFreeSpend/{YYYY-MM-DD} the PLATFORM's day of free spend:\n * { day, estCostUsd, requests, refusals, alertedAt?, pausedAt?,\n * updatedAt }\n *\n * The account document sits under `users/{uid}`, whose owner may read and\n * write the user document itself. It may not touch this subcollection: the\n * `users` block matches its subcollections BY NAME and carries no wildcard,\n * so an unmatched name is default-deny for every client, staff included —\n * the same property `assistUsage` relies on one level down. An owner who\n * could write here could reset their own allowance, which is the whole\n * meter. The platform document is a top-level collection alongside\n * `platformCronBeats`, unmatched and therefore closed, and every reader is\n * an Admin-SDK reader behind the staff gate.\n *\n * ## Whose account\n *\n * The WORKSPACE OWNER's. A member invited to someone else's free workspace\n * draws from that workspace's band and from its owner's allowance, never\n * from their own account: the owner is the person the free workspace is\n * attributed to for the ceiling on how many they may hold, and attributing\n * the spend the same way is what keeps the two bounds describing one\n * person. `ownerUid` first, `createdByUid` for an org that predates\n * ownership stamping — the same union the free-workspace count reads.\n */\n\n/** Where an account's month of free AI spend lives, under `users/{uid}`. */\nexport const FREE_AI_ACCOUNT_USAGE_COLLECTION = 'aiUsage'\n\n/** The top-level collection holding one document per UTC day of free spend. */\nexport const PLATFORM_AI_FREE_SPEND_COLLECTION = 'platformAiFreeSpend'\n\n/**\n * Refusals a day before an account's free generation pauses. Three, not\n * one: a single declined brief is a normal outcome of asking an assistant\n * for things, and pausing on it would punish an ordinary afternoon. Three\n * in a day is somebody probing what the model will write.\n */\nexport const AI_FREE_REFUSALS_PER_DAY = 3\n\n/** The share of the platform ceiling at which staff are told, once a day. */\nexport const AI_FREE_PLATFORM_ALERT_SHARE = 0.8\n\n/** The account allowance, in USD of provider spend — the taste, per person. */\nexport const FREE_AI_ACCOUNT_BUDGET_USD = assistUsdFromCredits(\n FREE_AI_TASTE_CREDITS_PER_MONTH,\n)\n\n/**\n * Free requests one account may make a UTC day, across its workspaces\n * (`AI_FREE_DAILY_REQUESTS`, default 30).\n *\n * A request, not a message: this counts every reservation the account's\n * free workspaces take, at either door. Thirty is three times the\n * per-workspace message cap, so one workspace never hits it before its own\n * cap and three workspaces hit it exactly when their caps would have\n * summed — it bounds the multiplier, not the ordinary day. Zero is honored\n * as \"no free requests\", which is a decision an operator may write down;\n * junk and an empty value take the default.\n */\nexport function aiFreeDailyRequests(): number {\n const raw = process.env.AI_FREE_DAILY_REQUESTS\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 30\n}\n\n/**\n * The platform-wide ceiling on ONE UTC day of free-tier provider spend\n * (`AI_FREE_DAILY_PLATFORM_CEILING_USD`, default $25).\n *\n * Every other bound here is per something — per workspace, per account,\n * per address — and a wave of new accounts clears all of them one at a\n * time. This is the bound on the sum. At 80% of it staff are mailed; at\n * 100% every free reservation refuses until the day rolls, paid workspaces\n * untouched, and the manual `ai-generate` lockdown key remains the switch\n * for anything the ceiling did not catch.\n *\n * Fails to the DEFAULT, never to \"no ceiling\", for the reason the operator\n * COGS limit does: an empty, negative, zero or unparseable value is not a\n * decision to run unbounded. There is deliberately no `off` word — a\n * deployment that wants no ceiling sets a figure it is content to spend.\n */\nexport function aiFreeDailyPlatformCeilingUsd(): number {\n const raw = String(process.env.AI_FREE_DAILY_PLATFORM_CEILING_USD ?? '').trim()\n const parsed = raw ? Number(raw) : Number.NaN\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 25\n}\n\n/** The account a free workspace's spend is attributed to. */\nexport interface FreeAssistAccount {\n /**\n * The workspace owner's uid, or `null` for an org document that names no\n * owner and no creator — every org created since ownership was stamped\n * names one, so `null` is the shape of a fixture or a very old record.\n * The account rungs are skipped for it; the org's own band still binds.\n */\n accountUid: string | null\n}\n\n/** The org fields the attribution reads, beside the billing ones. */\nexport type AssistMeteredOrg = Partial<AglynOrgBilling> & {\n ownerUid?: string | null\n createdByUid?: string | null\n}\n\n/**\n * The account to meter a workspace's free spend against, or `null` when the\n * workspace is not on the Free plan and none of this applies.\n *\n * `resolveEffectivePlan`, not `org.plan`: a workspace whose subscription\n * died is Free again, and its spend is the taste's spend again.\n */\nexport function freeAssistAccount(\n org: AssistMeteredOrg | null | undefined,\n): FreeAssistAccount | null {\n if (resolveEffectivePlan(org) !== 'free') return null\n const owner = String(org?.ownerUid ?? '').trim()\n const creator = String(org?.createdByUid ?? '').trim()\n return { accountUid: owner || creator || null }\n}\n\nexport function freeAccountUsageRef(\n firestore: FirebaseFirestore.Firestore,\n accountUid: string,\n month: string,\n): FirebaseFirestore.DocumentReference {\n return firestore\n .collection('users')\n .doc(accountUid)\n .collection(FREE_AI_ACCOUNT_USAGE_COLLECTION)\n .doc(month)\n}\n\nexport function platformFreeSpendRef(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): FirebaseFirestore.DocumentReference {\n return firestore.collection(PLATFORM_AI_FREE_SPEND_COLLECTION).doc(day)\n}\n\n/** What the reservation read about the account and the platform. */\nexport interface FreeTasteReads {\n accountCostUsd: number\n accountRequestsToday: number\n accountRefusalsToday: number\n platformCostUsd: number\n platformPaused: boolean\n}\n\n/** The two snapshots, as the transaction handed them over, into figures. */\nexport function freeTasteReadsFrom(\n account: FirebaseFirestore.DocumentSnapshot | null,\n platform: FirebaseFirestore.DocumentSnapshot | null,\n day: string,\n): FreeTasteReads {\n const days = (account?.get('days') ?? {}) as Record<\n string,\n { requests?: unknown; refusals?: unknown } | undefined\n >\n const today = days[day] ?? {}\n return {\n accountCostUsd: Number(account?.get('estCostUsd') ?? 0),\n accountRequestsToday: Number(today.requests ?? 0),\n accountRefusalsToday: Number(today.refusals ?? 0),\n platformCostUsd: Number(platform?.get('estCostUsd') ?? 0),\n platformPaused: Boolean(platform?.get('pausedAt')),\n }\n}\n\n/**\n * Which of the taste's own precautions refuses, or `null` when none does —\n * pure, so the spec can force every rung red without a Firestore.\n *\n * Checked in the order the reads are cheapest to explain: the daily\n * request cap and the refusal pause (both reset on a clock the customer\n * can be told about), then the account's month (an upgrade is the way\n * out), then the platform ceiling (nobody's doing; try again tomorrow).\n * The org's own rungs — its daily message cap, its band — run before this\n * in the reservation, so a workspace at its own band hears about its own\n * band rather than about its owner's other workspaces.\n *\n * Every comparison is `>=` against a count the reservation is about to\n * increment, so a cap of N admits exactly N. A paused platform refuses\n * regardless of the running figure: the pause is a decision recorded on\n * the document, and a ceiling raised by an operator mid-day does not undo\n * it — the day rolls, or staff clear the document.\n */\nexport function freeTasteRefusal(\n reads: FreeTasteReads,\n limits: {\n dailyRequests?: number\n refusalsPerDay?: number\n accountBudgetUsd?: number\n platformCeilingUsd?: number\n } = {},\n): Extract<AssistRefusedBy, 'requests' | 'refusals' | 'account' | 'platform'> | null {\n const dailyRequests = limits.dailyRequests ?? aiFreeDailyRequests()\n const refusalsPerDay = limits.refusalsPerDay ?? AI_FREE_REFUSALS_PER_DAY\n const accountBudgetUsd = limits.accountBudgetUsd ?? FREE_AI_ACCOUNT_BUDGET_USD\n const platformCeilingUsd =\n limits.platformCeilingUsd ?? aiFreeDailyPlatformCeilingUsd()\n if (reads.accountRequestsToday >= dailyRequests) return 'requests'\n if (reads.accountRefusalsToday >= refusalsPerDay) return 'refusals'\n if (reads.accountCostUsd >= accountBudgetUsd) return 'account'\n if (reads.platformPaused || reads.platformCostUsd >= platformCeilingUsd) {\n return 'platform'\n }\n return null\n}\n\n/**\n * The account-side write one admitted free reservation owes: a request\n * counted for the month and for the day. `set(…, { merge: true })` with\n * nested increments, so the first request of a month conjures the document\n * and a day's map entry appears the first time that day is counted.\n */\nexport function freeAccountReservationWrite(\n month: string,\n day: string,\n): Record<string, unknown> {\n const increment = FieldValue.increment\n return {\n month,\n requests: increment(1),\n days: { [day]: { requests: increment(1) } },\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/**\n * The two writes one metered free turn owes, beside the org's own rollup.\n *\n * A `refusal` stop is the one case where the meters disagree on purpose. The\n * tokens were spent, so the PLATFORM document — which bounds our money —\n * takes the cost and counts the refusal. The ACCOUNT's credits do not move:\n * a refusal never draws credits, because a customer whose brief was\n * declined has not been given anything, and because a run of declined\n * briefs is answered by the refusal pause rather than by an emptied band.\n * The account's day counts the refusal, which is what the pause reads.\n */\nexport function freeTasteMeterWrites(input: {\n estCostUsd: number\n refused: boolean\n day: string\n month: string\n}): { account: Record<string, unknown>; platform: Record<string, unknown> } {\n const increment = FieldValue.increment\n const serverTimestamp = FieldValue.serverTimestamp\n const { estCostUsd, refused, day, month } = input\n return {\n account: refused\n ? {\n month,\n days: { [day]: { refusals: increment(1) } },\n updatedAt: serverTimestamp(),\n }\n : { month, estCostUsd: increment(estCostUsd), updatedAt: serverTimestamp() },\n platform: {\n day,\n estCostUsd: increment(estCostUsd),\n requests: increment(1),\n refusals: increment(refused ? 1 : 0),\n updatedAt: serverTimestamp(),\n },\n }\n}\n\n/**\n * After a free turn is metered: tell staff at 80% of the day's ceiling,\n * and record the pause at 100% — each ONCE per UTC day, deduplicated on\n * the day document itself inside a transaction, so two turns landing\n * together cannot both cross a threshold and both announce it.\n *\n * The refusal at the ceiling is NOT decided here; `freeTasteRefusal` reads\n * the figure inside every reservation and refuses on it directly. This\n * only says so out loud: the email, and an `adminAudit` row under\n * `platform.aiFreeSpend.paused` so the staff audit trail shows the day the\n * taste switched itself off. Both are best-effort and neither can fail the\n * turn that triggered them — the turn is already paid for.\n *\n * Returns what it announced, for the spec and for the log.\n */\nexport async function announcePlatformFreeSpend(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): Promise<{ alerted: boolean; paused: boolean; estCostUsd: number }> {\n const ceilingUsd = aiFreeDailyPlatformCeilingUsd()\n const ref = platformFreeSpendRef(firestore, day)\n const crossed = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const estCostUsd = Number(snapshot.get('estCostUsd') ?? 0)\n const alert =\n !snapshot.get('alertedAt') &&\n estCostUsd >= ceilingUsd * AI_FREE_PLATFORM_ALERT_SHARE\n const pause = !snapshot.get('pausedAt') && estCostUsd >= ceilingUsd\n if (alert || pause) {\n tx.set(\n ref,\n {\n ...(alert ? { alertedAt: FieldValue.serverTimestamp() } : {}),\n ...(pause ? { pausedAt: FieldValue.serverTimestamp() } : {}),\n },\n { merge: true },\n )\n }\n return { alerted: alert, paused: pause, estCostUsd }\n })\n if (!crossed.alerted && !crossed.paused) return crossed\n\n const figure = `$${crossed.estCostUsd.toFixed(2)} of the $${ceilingUsd.toFixed(2)} ceiling`\n if (crossed.paused) {\n await addAdminAudit(firestore, {\n actorUid: 'system:ai-free-spend',\n action: 'platform.aiFreeSpend.paused',\n target: `${PLATFORM_AI_FREE_SPEND_COLLECTION}/${day}`,\n after: { day, estCostUsd: crossed.estCostUsd, ceilingUsd },\n at: FieldValue.serverTimestamp(),\n })\n .catch((error) =>\n console.error('[ai-free-spend] audit row failed', error),\n )\n }\n await sendStaffAlertEmail({\n subject: crossed.paused\n ? `Free AI generation paused for ${day}: ${figure}`\n : `Free AI spend at 80% of today's ceiling (${day}): ${figure}`,\n text: crossed.paused\n ? `Free-tier AI spend for ${day} (UTC) reached ${figure} ` +\n '(AI_FREE_DAILY_PLATFORM_CEILING_USD). Every Free workspace is ' +\n 'refused AI generation until the UTC day rolls; paid workspaces are ' +\n 'unaffected. If this is an abuse wave rather than organic use, the ' +\n '`ai-generate` feature lock on Staff → Lockdown stops generation ' +\n 'for everyone, and the assist signals page shows which workspaces ' +\n 'spent it.'\n : `Free-tier AI spend for ${day} (UTC) is at ${figure} ` +\n '(AI_FREE_DAILY_PLATFORM_CEILING_USD). At 100% every Free ' +\n 'workspace is refused AI generation until the day rolls. The ' +\n 'assist signals page shows the day so far; the `ai-generate` ' +\n 'feature lock on Staff → Lockdown is the manual stop.',\n context: 'ai-free-spend',\n })\n return crossed\n}\n\n/** The staff readout of today's free spend, for the assist signals page. */\nexport interface PlatformFreeSpendReadout {\n day: string\n estCostUsd: number\n requests: number\n refusals: number\n ceilingUsd: number\n /** True once the 80% mail went out today. */\n alerted: boolean\n /** True while free generation is refused for the day. */\n paused: boolean\n}\n\nexport async function readPlatformFreeSpend(\n firestore: FirebaseFirestore.Firestore,\n day: string,\n): Promise<PlatformFreeSpendReadout> {\n const snapshot = await platformFreeSpendRef(firestore, day).get()\n const ceilingUsd = aiFreeDailyPlatformCeilingUsd()\n const estCostUsd = Number(snapshot.get('estCostUsd') ?? 0)\n return {\n day,\n estCostUsd,\n requests: Number(snapshot.get('requests') ?? 0),\n refusals: Number(snapshot.get('refusals') ?? 0),\n ceilingUsd,\n alerted: Boolean(snapshot.get('alertedAt')),\n // The pause is the recorded decision OR the live figure: a day that\n // crossed the ceiling before anything announced it is paused all the\n // same, because the reservation refuses on the figure.\n paused: Boolean(snapshot.get('pausedAt')) || estCostUsd >= ceilingUsd,\n }\n}\n"],"names":["FieldValue","assistUsdFromCredits","resolveEffectivePlan","FREE_AI_TASTE_CREDITS_PER_MONTH","sendStaffAlertEmail","addAdminAudit","FREE_AI_ACCOUNT_USAGE_COLLECTION","PLATFORM_AI_FREE_SPEND_COLLECTION","AI_FREE_REFUSALS_PER_DAY","AI_FREE_PLATFORM_ALERT_SHARE","FREE_AI_ACCOUNT_BUDGET_USD","aiFreeDailyRequests","raw","process","env","AI_FREE_DAILY_REQUESTS","parsed","Number","NaN","isFinite","Math","floor","aiFreeDailyPlatformCeilingUsd","String","AI_FREE_DAILY_PLATFORM_CEILING_USD","trim","freeAssistAccount","org","owner","ownerUid","creator","createdByUid","accountUid","freeAccountUsageRef","firestore","month","collection","doc","platformFreeSpendRef","day","freeTasteReadsFrom","account","platform","days","today","get","accountCostUsd","accountRequestsToday","requests","accountRefusalsToday","refusals","platformCostUsd","platformPaused","Boolean","freeTasteRefusal","reads","limits","dailyRequests","refusalsPerDay","accountBudgetUsd","platformCeilingUsd","freeAccountReservationWrite","increment","updatedAt","serverTimestamp","freeTasteMeterWrites","input","estCostUsd","refused","announcePlatformFreeSpend","ceilingUsd","ref","crossed","runTransaction","tx","snapshot","alert","pause","set","alertedAt","pausedAt","merge","alerted","paused","figure","toFixed","actorUid","action","target","after","at","catch","error","console","subject","text","context","readPlatformFreeSpend"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginUsageAxesDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-usage-axes';
18
+ /**
19
+ * THE AI PLUGIN'S METER (AGL-3080): provider spend, in the platform's cost
20
+ * model and against the credit band in its utilization table. Compiled into
21
+ * core by the manifest generator (`register.usageAxes`).
22
+ *
23
+ * The rollup records `assistCostUsd` — what the month's tokens cost US, at
24
+ * the provider's rates (AGL-3015) — so the axis enters the cost model at ×1
25
+ * and names no rate. It is the one line on the platform whose unit cost is
26
+ * real money paid to a third party, and the only one that can clear the
27
+ * $2/site floor on its own, which is why a margin surface that could not see
28
+ * it would rate a token-heavy workspace exactly as it rates an idle one.
29
+ *
30
+ * LIVE, because the rollup is a snapshot from when the cron ran and assist
31
+ * keeps spending after it: a reader that fetches the month's
32
+ * `assistUsage/{month}` document takes the provider figure, or the billed
33
+ * one on a month closed before the two were split, over the snapshot.
34
+ *
35
+ * The band is sold in credits, and a credit is `ASSIST_CREDIT_COST_USD` of
36
+ * spend, so what was used is the dollars over that — rounded up, the one
37
+ * conversion the customer's own meter uses.
38
+ */
39
+ export declare function aiUsageAxes(): PluginUsageAxesDeclaration;