@aglyn/plugins-ai 1.0.0-beta.236 → 1.0.0-beta.238

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 (164) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +117 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  9. package/src/lib/components/ai-job-plan.component.js +48 -12
  10. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  11. package/src/lib/components/ai-job-requests.d.ts +10 -0
  12. package/src/lib/components/ai-job-requests.js +12 -4
  13. package/src/lib/components/ai-job-requests.js.map +1 -1
  14. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  15. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  16. package/src/lib/components/ai-site-build-page.component.js +48 -9
  17. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  18. package/src/lib/components/ai-site-start-card.component.js +68 -22
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  21. package/src/lib/components/ai-usage-strip.component.js +40 -9
  22. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  23. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  24. package/src/lib/components/assist-build-card.component.js +58 -7
  25. package/src/lib/components/assist-build-card.component.js.map +1 -1
  26. package/src/lib/components/assist-jobs-drawer.component.js +66 -22
  27. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  28. package/src/lib/components/assist-panel.component.js +89 -52
  29. package/src/lib/components/assist-panel.component.js.map +1 -1
  30. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  31. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  32. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  33. package/src/lib/jobs/ai-job-build-step.js +54 -35
  34. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  36. package/src/lib/jobs/ai-job-cancel.js +102 -0
  37. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  38. package/src/lib/jobs/ai-job-component-step.js +4 -1
  39. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.js +2 -0
  41. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  42. package/src/lib/jobs/ai-job-form-step.js +4 -1
  43. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  45. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  46. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-language.js +12 -0
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  52. package/src/lib/jobs/ai-job-page-step.js +46 -4
  53. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
  55. package/src/lib/jobs/ai-job-plan-step.js +118 -18
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
  58. package/src/lib/jobs/ai-job-site-content.js +196 -37
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-step.js +49 -16
  61. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-template-step.js +4 -1
  63. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  64. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  65. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  66. package/src/lib/jobs/ai-jobs.d.ts +55 -13
  67. package/src/lib/jobs/ai-jobs.js +213 -41
  68. package/src/lib/jobs/ai-jobs.js.map +1 -1
  69. package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
  70. package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
  71. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  72. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  73. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  74. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  78. package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
  79. package/src/lib/layout-language/ai-layout-compiler.js +381 -18
  80. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  81. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  82. package/src/lib/layout-language/ai-layout-design.js +5 -3
  83. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  84. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  85. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  86. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  87. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  88. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  89. package/src/lib/layout-language/ai-layout-language.js +1 -1
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  92. package/src/lib/layout-language/ai-layout-listings.js +117 -47
  93. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  95. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  96. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  98. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  99. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  100. package/src/lib/model/ai-build-job.d.ts +39 -0
  101. package/src/lib/model/ai-build-job.js +100 -1
  102. package/src/lib/model/ai-build-job.js.map +1 -1
  103. package/src/lib/model/ai-build-progress.d.ts +8 -0
  104. package/src/lib/model/ai-build-progress.js +35 -0
  105. package/src/lib/model/ai-build-progress.js.map +1 -1
  106. package/src/lib/model/ai-credit-estimate.d.ts +249 -0
  107. package/src/lib/model/ai-credit-estimate.js +278 -0
  108. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  109. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  110. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  111. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  112. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  113. package/src/lib/model/ai-job-failure-copy.js +8 -3
  114. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  115. package/src/lib/model/ai-jobs.types.d.ts +35 -0
  116. package/src/lib/model/ai-jobs.types.js.map +1 -1
  117. package/src/lib/model/ai-site-build-progress.js +7 -4
  118. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-site-job.d.ts +76 -22
  120. package/src/lib/model/ai-site-job.js +125 -23
  121. package/src/lib/model/ai-site-job.js.map +1 -1
  122. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  123. package/src/lib/model/ai-site-kinds.js +35 -7
  124. package/src/lib/model/ai-site-kinds.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  126. package/src/lib/runtime/ai-doctrine-validators.js +161 -7
  127. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  128. package/src/lib/runtime/ai-doctrine.js +4 -3
  129. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  130. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  131. package/src/lib/runtime/ai-node-tree.js +26 -0
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.generated.js +592 -14
  134. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  135. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  136. package/src/lib/server/ai-jobs-cancel.js +27 -9
  137. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  138. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  139. package/src/lib/server/ai-jobs-gate.js +20 -9
  140. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  141. package/src/lib/server/ai-jobs-resume.js +93 -35
  142. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  143. package/src/lib/server/ai-jobs-route.js +45 -4
  144. package/src/lib/server/ai-jobs-route.js.map +1 -1
  145. package/src/lib/server/assist-chat.js +4 -1
  146. package/src/lib/server/assist-chat.js.map +1 -1
  147. package/src/lib/server/assist-edit.js +3 -1
  148. package/src/lib/server/assist-edit.js.map +1 -1
  149. package/src/lib/server/assist-music.d.ts +36 -0
  150. package/src/lib/server/assist-music.js +36 -0
  151. package/src/lib/server/assist-music.js.map +1 -0
  152. package/src/lib/server.js +6 -0
  153. package/src/lib/server.js.map +1 -1
  154. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  155. package/src/lib/usage/ai-usage-meter.js +17 -1
  156. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  157. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  158. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  159. package/src/lib/usage/assist-usage.d.ts +7 -0
  160. package/src/lib/usage/assist-usage.js +14 -4
  161. package/src/lib/usage/assist-usage.js.map +1 -1
  162. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  163. package/src/lib/usage/free-ai-credits-left.js +4 -2
  164. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-template-step.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { normalizeScreenSlug } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\n isAiAddressToken,\n isAiUnreadToken,\n parseAiTemplateJobInputs,\n type AiTemplateSubjectDefinition,\n} from '../model/ai-template-subjects'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiTemplateExamplesSystemBlock } from '../runtime/ai-template-examples'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiTemplateDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiCopySourceLines,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlannedTemplateTokens, aiPlanTemplateTokenViolations } from './ai-job-plan-conformance'\nimport {\n aiBuildingPlanCreations,\n aiJobPlanCreationsRefusal,\n aiPlanCreationsRunMinimumMs,\n} from './ai-job-plan-creations'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The template step (AGL-2909): the generation step of a `template` job, run\n * once a member confirmed the job's plan.\n *\n * It builds ONE page template — the page a site renders once per record: a\n * content collection's entry, a product, an author — through\n * `runValidatedGeneration('template', …)`. Everything that differs per\n * record is a binding token from the subject's vocabulary\n * (`model/ai-template-subjects.ts`), which is the list the besigner's insert\n * picker offers for that page; a link or media prop may hold one of those\n * tokens whole, which the palette validator admits only because this door\n * names them. The door adds one check of its own through `extend` (rule 8):\n * no token the page does not fill, an h1 bound to the subject's title, no\n * block that fills itself only on another subject's page, and no address,\n * slug or timestamp printed in its copy.\n *\n * The model is shown the platform's starter pages as examples, in a cached\n * block (`runtime/ai-template-examples.ts`), and writes against them.\n *\n * The template lands in the site's library as a new draft\n * (`ai-job-drafts.ts`), inert until a member uses it: bound to no\n * collection, set as no product or author page, served nowhere. A plan that\n * starts from a copy of a template the site has gets that copy instead.\n */\n\n/** The longest answer a template may run to; the tree is held to the template budget either way. */\nexport const AI_JOB_TEMPLATE_MAX_TOKENS = AI_ROUTING_TABLE['job.template'].maxTokens\n\n/**\n * The template step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.template` is served from, fitted to what a beat can give a step. It\n * runs on the job beat, never at an inline door, and asks for no more of the\n * ceiling than fits that time on the model it runs.\n */\nexport const AI_JOB_TEMPLATE_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.template'],\n maxTokens: AI_JOB_TEMPLATE_MAX_TOKENS,\n})\n\n/** The least time one template step needs before it starts. */\nexport const AI_JOB_TEMPLATE_STEP_MINIMUM_MS = AI_JOB_TEMPLATE_STEP_BUDGET.minimumMs\n\n/** The template step's own instructions, cached after the doctrine and before the examples. */\nexport const AI_JOB_TEMPLATE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one page template: the page a website renders once for each record it keeps, such as each entry of a content collection, each product or each author. Answer with submit_template: the whole page as one flat node map.',\n 'Everything that differs from one record to the next is a binding token, never typed copy. The request names what the page is for, the tokens that page fills and the blocks that fill themselves there; bind only those tokens.',\n 'The page’s one h1 holds the title token the request names. Copy that reads the same on every record, such as a section heading, is written in the site’s voice.',\n 'A link or media prop holds exactly one token from the request’s link and media tokens, such as an Image whose src is {{entry.coverImage}}; its alt text may combine words and tokens. Copy never prints a link or media token, a slug or a timestamp.',\n 'A template carries no third-party player. It plays a video token only when the confirmed plan lists it, in a Video (video) whose src is that token and whose poster is the cover image token.',\n 'The page renders inside the site’s layout, so it carries no header, navigation or footer of its own.',\n ].join('\\n'),\n },\n]\n\n/** A content collection an entry template is for, as the prompt and the draft name it. */\nexport interface AiTemplateCollection {\n id: string\n name: string\n slug: string\n}\n\n/** The content collection by that id on the site, or `null` when there is none. */\nexport async function readAiTemplateCollection(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n collectionId: string,\n): Promise<AiTemplateCollection | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .doc(collectionId)\n .get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (hostCollectionKind(data) !== 'content') return null\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const slug = text(data['slug'])\n return {\n id: collectionId,\n name: text(data['displayName']) || text(data['name']) || slug || collectionId,\n slug,\n }\n}\n\n/** The template's name: the plan's, else the collection's or the subject's. */\nexport function aiTemplateDraftName(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n planned: string | null | undefined,\n): string {\n return planned || (collection ? `${collection.name} entry template` : definition.defaultName)\n}\n\n/** The address Use template offers for the page it creates. */\nexport function aiTemplateDraftSlug(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n): string | null {\n return (\n normalizeScreenSlug(\n collection\n ? `${collection.slug || collection.id}-entry-template`\n : `${definition.subject}-page-template`,\n ) ?? null\n )\n}\n\n/** The template job as the generation's user turn, with the template a short copy would have started from (AGL-3143 §14). */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: string,\n source: readonly string[] = [],\n): string {\n const of = collection\n ? ` of the \"${collection.name}\" collection${collection.slug ? ` (/${collection.slug})` : ''}`\n : ''\n return [\n `Template name: ${name}`,\n `The page is for ${definition.noun}${of}.`,\n `Title token: ${definition.titleToken}`,\n `Tokens this page fills: ${definition.tokens.map((entry) => `${entry.token} ${entry.label}`).join('; ')}`,\n `Link and media tokens: ${aiTemplateAddressTokens(definition).join(', ')}`,\n definition.blocks.length\n ? `Blocks that fill themselves on this page: ${definition.blocks.join(', ')}`\n : 'No block fills itself on this page: bind the tokens.',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ...source,\n ].join('\\n')\n}\n\n/** Which subject's page each self-filling block belongs to. */\nconst BLOCK_SUBJECTS = new Map<string, AiTemplateSubjectDefinition>(\n AI_TEMPLATE_SUBJECTS.flatMap((subject) =>\n AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].blocks.map(\n (block) => [block, AI_TEMPLATE_SUBJECT_DEFINITIONS[subject]] as const,\n ),\n ),\n)\n\n/** Every string a prop value holds, however deeply. */\nfunction stringsIn(value: unknown, out: string[] = []): string[] {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) stringsIn(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) stringsIn(inner, out)\n }\n return out\n}\n\n/** The page's title: a Typography that renders as an h1. */\nfunction isPageTitle(node: AiDoctrineNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n const element = typeof node.props?.['component'] === 'string' ? String(node.props['component']) : ''\n return (element || String(node.props?.['variant'] ?? '')) === 'h1'\n}\n\n/**\n * The tokens a prop prints for a reader that no reader reads: a link or\n * picture token, a slug or a timestamp, in copy (AGL-3143 §16). Copy is a\n * prop the palette gives the text role, read as words where it holds more\n * than one token or is the element's children. A block handed one token\n * whole reads it as a value, as Entry Meta's avatar takes the author's\n * portrait and a Video's publication date takes the timestamp, and a\n * markdown link's target is followed, not read.\n */\nfunction printedTokens(node: AiDoctrineNode): string[] {\n const roles = AI_PALETTE[node.componentId]?.propRoles ?? {}\n return Object.entries(node.props ?? {}).flatMap(([key, value]) => {\n if (typeof value !== 'string' || roles[key] !== 'text') return []\n const tokens = aiBindingTokensIn(value.replace(/\\]\\([^)]*\\)/g, ']'))\n const unread = tokens.filter((token) => isAiAddressToken(token) || isAiUnreadToken(token))\n const whole = tokens.length === 1 && value.trim() === tokens[0]\n return unread.length && (key === 'children' || !whole) ? unread : []\n })\n}\n\n/**\n * The template door's check on a tree the doctrine admitted (rule 8): every\n * token is one the page fills, the h1 binds the subject's title, no block\n * that fills itself on another subject's page is placed — except inside a\n * Collection Entries card, where each fills from the card's own entry — and\n * no copy prints an address, a slug or a timestamp.\n */\nexport function aiTemplateBindingCheck(\n definition: AiTemplateSubjectDefinition,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const allowed = new Set(definition.tokens.map((entry) => entry.token))\n return (tree) => {\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const unknown = new Set<string>()\n const unknownAt: string[] = []\n const foreign: Array<{ id: string; componentId: string; owner: AiTemplateSubjectDefinition }> = []\n const titles: string[] = []\n const printed = new Set<string>()\n const printedAt: string[] = []\n let titleBound = false\n for (const { id, node, ancestors } of walkTree({ rootId: tree.rootId, nodes })) {\n const tokens = stringsIn(node.props).flatMap(aiBindingTokensIn)\n const strays = tokens.filter((token) => !allowed.has(token))\n if (strays.length) {\n for (const token of strays) unknown.add(token)\n unknownAt.push(id)\n }\n const shown = printedTokens(node).filter((token) => allowed.has(token))\n if (shown.length) {\n for (const token of shown) printed.add(token)\n printedAt.push(id)\n }\n const owner = BLOCK_SUBJECTS.get(node.componentId)\n const inCard = ancestors.some((ancestor) => nodes[ancestor]?.componentId === 'collectionEntries')\n if (owner && owner.subject !== definition.subject && !inCard) {\n foreign.push({ id, componentId: node.componentId, owner })\n }\n if (isPageTitle(node)) {\n titles.push(id)\n if (tokens.includes(definition.titleToken)) titleBound = true\n }\n }\n const violations: AiDoctrineViolation[] = []\n if (unknown.size) {\n violations.push({\n rule: 8,\n code: 'unknown-binding',\n message: `This binds ${[...unknown].slice(0, 3).join(', ')}, which ${definition.noun}’s page does not fill. Bind only the tokens listed for this page.`,\n nodeIds: aiModelNodeIds(unknownAt, tree.sourceIds),\n })\n }\n if (printed.size) {\n violations.push({\n rule: 8,\n code: 'printed-token',\n message: `This prints ${[...printed].slice(0, 3).join(', ')} in the page's copy, where a reader sees an address, a slug or a timestamp. A link or picture token goes whole in a link or media prop, and a slug or a timestamp stays off the page.`,\n nodeIds: aiModelNodeIds(printedAt, tree.sourceIds),\n })\n }\n // The doctrine already refuses a page with no h1 or with several.\n if (titles.length === 1 && !titleBound) {\n violations.push({\n rule: 8,\n code: 'typed-title',\n message: `The page title is typed, so every record would show the same one. Put ${definition.titleToken} in the h1.`,\n nodeIds: aiModelNodeIds(titles, tree.sourceIds),\n })\n }\n if (foreign.length) {\n const [first] = foreign\n violations.push({\n rule: 8,\n code: 'foreign-block',\n message: `The ${AI_PALETTE[first.componentId]?.displayName ?? first.componentId} block fills itself only on ${first.owner.noun}’s page, and this template is for ${definition.noun}. Remove it.`,\n nodeIds: aiModelNodeIds(\n foreign.map((entry) => entry.id),\n tree.sourceIds,\n ),\n })\n }\n return violations\n }\n}\n\nexport interface AiTemplateJobAdmissionDeps {\n readCollection?: typeof readAiTemplateCollection\n}\n\n/**\n * A template job is admitted with inputs that say what the page is for, for\n * a site of the job's own org that holds the collection it names, with a\n * template to spare.\n */\nexport function createAiTemplateJobAdmission(deps: AiTemplateJobAdmissionDeps = {}): AiJobAdmission {\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n return async (context) => {\n const inputs = parseAiTemplateJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n return aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'template',\n org: context.org,\n ownCheck: async (hostId) =>\n inputs.collectionId && !(await readCollection(context.firestore, hostId, inputs.collectionId))\n ? { status: 404, error: 'That content collection is not on this site' }\n : aiJobPlanCreationsRefusal('template', context, hostId),\n })\n }\n}\n\nexport interface AiJobTemplateStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n readCollection?: typeof readAiTemplateCollection\n /** The starter examples block; built from the starters once per process otherwise. */\n examples?: () => AiSystemBlock\n}\n\n/**\n * The binding tokens the confirmed plan gives the template, against the\n * template a COPY actually produced (AGL-3024); `null` when the copy keeps\n * the plan, or when the plan promised no token to keep.\n *\n * The same hole as `aiCopiedLayoutReview` and `aiCopiedPageReview`, on the\n * third kind that can start from a duplicate. The generate path already holds\n * a built template to `aiPlanTemplateTokenViolations`; the copy path reached\n * none of it, so a plan reading \"duplicate it and bind the author's photo\"\n * got the duplicate and reported Done.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedTemplateReview(\n firestore: FirebaseFirestore.Firestore,\n input: {\n hostId: string\n id: string\n name: string\n plan: AiJobPlan | null\n definition: AiTemplateSubjectDefinition\n },\n): Promise<AiJobReview | null> {\n const { tokens, unreadable } = aiPlannedTemplateTokens(input.plan, input.definition)\n if (!tokens.length && !unreadable.length) return null\n const stored = await readAiTemplateDraftNodes(firestore, {\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the template the plan names, and this job could not read the copy back to check it shows what the plan gives it. Open the template and check it before you apply it.`,\n findings: [],\n }\n }\n const violations = aiPlanTemplateTokenViolations(input.plan, input.definition, {\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the template the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\nexport function createAiJobTemplateStep(deps: AiJobTemplateStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n const examples = deps.examples ?? aiTemplateExamplesSystemBlock\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a template job names no site, and its admission refuses one')\n const inputs = parseAiTemplateJobInputs(job.inputs)\n if (typeof inputs === 'string') {\n throw new Error(`a template job's inputs no longer read as they were admitted: ${inputs}`)\n }\n const definition = AI_TEMPLATE_SUBJECT_DEFINITIONS[inputs.subject]\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null): AiJobOutput => ({\n resource: 'template',\n id: draft.id,\n versionId: null,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.template') ?? aiModelForStep('job.template')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'template')\n const written = await readAiDraft(firestore, { kind: 'template', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot, collection] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n inputs.collectionId\n ? readCollection(firestore, hostId, inputs.collectionId)\n : Promise.resolve(null),\n ])\n if (inputs.collectionId && !collection) {\n throw new Error(`collection ${inputs.collectionId} left site ${hostId} after its template job was admitted`)\n }\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'template')\n const name = aiTemplateDraftName(definition, collection, creation?.name)\n\n // The plan starts from a copy of a template the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead, and one that cannot keep\n // the plan's tokens is built FROM (AGL-3143 §14): copying it could only end\n // in the review below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.templates.find((template) => template.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedTemplateReview(firestore, { hostId, id: source.id, name: source.name, plan, definition })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort\n ? aiCopySourceLines(\n 'template',\n source.name,\n (await readAiTemplateDraftNodes(firestore, { hostId, id: source.id })) as Record<string, unknown> | null,\n )\n : []\n if (creation?.duplicateOf && source && !sourceShort) {\n const copy = await duplicate('template', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so nothing else can.\n const review = await aiCopiedTemplateReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n definition,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: null, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'template', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('template', {\n step: 'job.template',\n model,\n instructions: [...AI_JOB_TEMPLATE_INSTRUCTIONS, examples()],\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobTemplatePrompt(job, definition, collection, plan, name, sourceLines),\n },\n ],\n tool: aiDoctrineTreeTool('template'),\n maxTokens: AI_JOB_TEMPLATE_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.template'].thinking ? { thinking: AI_ROUTING_TABLE['job.template'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.template'].effort ? { effort: AI_ROUTING_TABLE['job.template'].effort } : {}),\n context: { bindingTokens: aiTemplateAddressTokens(definition) },\n // Rule 8's binding check, and the confirmed plan kept in both halves: a\n // token the plan promised that the template does not bind (AGL-3143\n // §11), and a component the plan reuses that it does not place. The\n // layout and component doors hold their builds to that reuse; the\n // template door did not, so it could drop a promised component and\n // still answer `Done` (AGL-3024).\n extend: (tree) => [\n ...aiTemplateBindingCheck(definition)(tree),\n ...aiPlanTemplateTokenViolations(plan, definition, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ...aiPlanReuseViolations(inventory, plan, aiPlacedComponentIds(tree), 'template'),\n ],\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'template',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n slug: aiTemplateDraftSlug(definition, collection),\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its template was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n return { ...spent, outputs: [output(draft, result.value.load)] }\n }\n}\n\n/** The template step, building the components and forms its plan creates before the template (AGL-3143 §15). */\nexport const runAiJobTemplateStep = aiBuildingPlanCreations('template', 'job.template', createAiJobTemplateStep())\n\n/** Registers the template step and the check a template job passes before it is created or resumed. */\nexport function registerAiTemplateJob(): void {\n registerAiJobStep('template', runAiJobTemplateStep, {\n minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS,\n minimumMsFor: aiPlanCreationsRunMinimumMs('template'),\n })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["hostCollectionKind","normalizeScreenSlug","duplicateResource","CANVAS_ROOT_ELEMENT_ID","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","isAiAddressToken","isAiUnreadToken","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiTemplateDraftNodes","writeAiDraft","aiConfirmedPlan","aiCopySourceLines","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlannedTemplateTokens","aiPlanTemplateTokenViolations","aiBuildingPlanCreations","aiJobPlanCreationsRefusal","aiPlanCreationsRunMinimumMs","aiJobStepBudget","registerAiJobStep","AI_JOB_TEMPLATE_MAX_TOKENS","maxTokens","AI_JOB_TEMPLATE_STEP_BUDGET","tier","AI_JOB_TEMPLATE_STEP_MINIMUM_MS","minimumMs","AI_JOB_TEMPLATE_INSTRUCTIONS","text","join","readAiTemplateCollection","firestore","hostId","collectionId","snapshot","collection","doc","get","exists","data","value","trim","slug","id","name","aiTemplateDraftName","definition","planned","defaultName","aiTemplateDraftSlug","subject","aiJobTemplatePrompt","job","plan","source","of","noun","titleToken","tokens","map","entry","token","label","blocks","length","BLOCK_SUBJECTS","Map","flatMap","block","stringsIn","out","push","Array","isArray","inner","Object","values","isPageTitle","node","componentId","element","props","String","printedTokens","roles","propRoles","entries","key","replace","unread","filter","whole","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","printed","printedAt","titleBound","ancestors","rootId","strays","has","add","shown","owner","inCard","some","ancestor","includes","violations","size","rule","code","message","slice","nodeIds","sourceIds","first","displayName","createAiTemplateJobAdmission","deps","readCollection","context","inputs","status","error","orgId","kind","org","ownCheck","aiCopiedTemplateReview","input","unreadable","stored","reason","findings","createAiJobTemplateStep","readInventory","duplicate","examples","now","signal","modelFor","orgSnapshot","Error","output","draft","load","resource","versionId","hostSubdomain","model","draftId","written","outputs","inventory","Promise","all","resolve","creation","duplicateOf","templates","find","template","undefined","sourceReview","sourceShort","Boolean","sourceLines","copy","sourceId","uid","createdBy","attemptKey","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob","minimumMsFor"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-template-step.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 { throwIfAiJobCanceled } from './ai-job-cancel'\nimport { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\nimport { normalizeScreenSlug } from '@aglyn/aglyn/app-utils/screen-route'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport {\n AI_TEMPLATE_SUBJECT_DEFINITIONS,\n AI_TEMPLATE_SUBJECTS,\n aiBindingTokensIn,\n aiTemplateAddressTokens,\n isAiAddressToken,\n isAiUnreadToken,\n parseAiTemplateJobInputs,\n type AiTemplateSubjectDefinition,\n} from '../model/ai-template-subjects'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n aiDoctrineTreeTool,\n runValidatedGeneration,\n type AiValidatedTree,\n} from '../runtime/ai-doctrine'\nimport {\n walkTree,\n type AiDoctrineNode,\n type AiDoctrineViolation,\n} from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport { AI_PALETTE } from '../runtime/ai-palette.generated'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { aiTemplateExamplesSystemBlock } from '../runtime/ai-template-examples'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiTemplateDraftNodes,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiCopySourceLines,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiModelNodeIds,\n aiPlacedComponentIds,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiPlanReuseViolations,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport { aiPlannedTemplateTokens, aiPlanTemplateTokenViolations } from './ai-job-plan-conformance'\nimport {\n aiBuildingPlanCreations,\n aiJobPlanCreationsRefusal,\n aiPlanCreationsRunMinimumMs,\n} from './ai-job-plan-creations'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The template step (AGL-2909): the generation step of a `template` job, run\n * once a member confirmed the job's plan.\n *\n * It builds ONE page template — the page a site renders once per record: a\n * content collection's entry, a product, an author — through\n * `runValidatedGeneration('template', …)`. Everything that differs per\n * record is a binding token from the subject's vocabulary\n * (`model/ai-template-subjects.ts`), which is the list the besigner's insert\n * picker offers for that page; a link or media prop may hold one of those\n * tokens whole, which the palette validator admits only because this door\n * names them. The door adds one check of its own through `extend` (rule 8):\n * no token the page does not fill, an h1 bound to the subject's title, no\n * block that fills itself only on another subject's page, and no address,\n * slug or timestamp printed in its copy.\n *\n * The model is shown the platform's starter pages as examples, in a cached\n * block (`runtime/ai-template-examples.ts`), and writes against them.\n *\n * The template lands in the site's library as a new draft\n * (`ai-job-drafts.ts`), inert until a member uses it: bound to no\n * collection, set as no product or author page, served nowhere. A plan that\n * starts from a copy of a template the site has gets that copy instead.\n */\n\n/** The longest answer a template may run to; the tree is held to the template budget either way. */\nexport const AI_JOB_TEMPLATE_MAX_TOKENS = AI_ROUTING_TABLE['job.template'].maxTokens\n\n/**\n * The template step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.template` is served from, fitted to what a beat can give a step. It\n * runs on the job beat, never at an inline door, and asks for no more of the\n * ceiling than fits that time on the model it runs.\n */\nexport const AI_JOB_TEMPLATE_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.template'],\n maxTokens: AI_JOB_TEMPLATE_MAX_TOKENS,\n})\n\n/** The least time one template step needs before it starts. */\nexport const AI_JOB_TEMPLATE_STEP_MINIMUM_MS = AI_JOB_TEMPLATE_STEP_BUDGET.minimumMs\n\n/** The template step's own instructions, cached after the doctrine and before the examples. */\nexport const AI_JOB_TEMPLATE_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n 'You build one page template: the page a website renders once for each record it keeps, such as each entry of a content collection, each product or each author. Answer with submit_template: the whole page as one flat node map.',\n 'Everything that differs from one record to the next is a binding token, never typed copy. The request names what the page is for, the tokens that page fills and the blocks that fill themselves there; bind only those tokens.',\n 'The page’s one h1 holds the title token the request names. Copy that reads the same on every record, such as a section heading, is written in the site’s voice.',\n 'A link or media prop holds exactly one token from the request’s link and media tokens, such as an Image whose src is {{entry.coverImage}}; its alt text may combine words and tokens. Copy never prints a link or media token, a slug or a timestamp.',\n 'A template carries no third-party player. It plays a video token only when the confirmed plan lists it, in a Video (video) whose src is that token and whose poster is the cover image token.',\n 'The page renders inside the site’s layout, so it carries no header, navigation or footer of its own.',\n ].join('\\n'),\n },\n]\n\n/** A content collection an entry template is for, as the prompt and the draft name it. */\nexport interface AiTemplateCollection {\n id: string\n name: string\n slug: string\n}\n\n/** The content collection by that id on the site, or `null` when there is none. */\nexport async function readAiTemplateCollection(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n collectionId: string,\n): Promise<AiTemplateCollection | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .doc(collectionId)\n .get()\n if (!snapshot.exists) return null\n const data = (snapshot.data() ?? {}) as Record<string, unknown>\n if (hostCollectionKind(data) !== 'content') return null\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const slug = text(data['slug'])\n return {\n id: collectionId,\n name: text(data['displayName']) || text(data['name']) || slug || collectionId,\n slug,\n }\n}\n\n/** The template's name: the plan's, else the collection's or the subject's. */\nexport function aiTemplateDraftName(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n planned: string | null | undefined,\n): string {\n return planned || (collection ? `${collection.name} entry template` : definition.defaultName)\n}\n\n/** The address Use template offers for the page it creates. */\nexport function aiTemplateDraftSlug(\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n): string | null {\n return (\n normalizeScreenSlug(\n collection\n ? `${collection.slug || collection.id}-entry-template`\n : `${definition.subject}-page-template`,\n ) ?? null\n )\n}\n\n/** The template job as the generation's user turn, with the template a short copy would have started from (AGL-3143 §14). */\nexport function aiJobTemplatePrompt(\n job: Pick<AiJob, 'brief'>,\n definition: AiTemplateSubjectDefinition,\n collection: AiTemplateCollection | null,\n plan: AiJobPlan | null,\n name: string,\n source: readonly string[] = [],\n): string {\n const of = collection\n ? ` of the \"${collection.name}\" collection${collection.slug ? ` (/${collection.slug})` : ''}`\n : ''\n return [\n `Template name: ${name}`,\n `The page is for ${definition.noun}${of}.`,\n `Title token: ${definition.titleToken}`,\n `Tokens this page fills: ${definition.tokens.map((entry) => `${entry.token} ${entry.label}`).join('; ')}`,\n `Link and media tokens: ${aiTemplateAddressTokens(definition).join(', ')}`,\n definition.blocks.length\n ? `Blocks that fill themselves on this page: ${definition.blocks.join(', ')}`\n : 'No block fills itself on this page: bind the tokens.',\n aiJobBriefLine(job),\n ...aiPlanReferenceLines(plan),\n ...source,\n ].join('\\n')\n}\n\n/** Which subject's page each self-filling block belongs to. */\nconst BLOCK_SUBJECTS = new Map<string, AiTemplateSubjectDefinition>(\n AI_TEMPLATE_SUBJECTS.flatMap((subject) =>\n AI_TEMPLATE_SUBJECT_DEFINITIONS[subject].blocks.map(\n (block) => [block, AI_TEMPLATE_SUBJECT_DEFINITIONS[subject]] as const,\n ),\n ),\n)\n\n/** Every string a prop value holds, however deeply. */\nfunction stringsIn(value: unknown, out: string[] = []): string[] {\n if (typeof value === 'string') out.push(value)\n else if (Array.isArray(value)) for (const inner of value) stringsIn(inner, out)\n else if (value && typeof value === 'object') {\n for (const inner of Object.values(value as Record<string, unknown>)) stringsIn(inner, out)\n }\n return out\n}\n\n/** The page's title: a Typography that renders as an h1. */\nfunction isPageTitle(node: AiDoctrineNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n const element = typeof node.props?.['component'] === 'string' ? String(node.props['component']) : ''\n return (element || String(node.props?.['variant'] ?? '')) === 'h1'\n}\n\n/**\n * The tokens a prop prints for a reader that no reader reads: a link or\n * picture token, a slug or a timestamp, in copy (AGL-3143 §16). Copy is a\n * prop the palette gives the text role, read as words where it holds more\n * than one token or is the element's children. A block handed one token\n * whole reads it as a value, as Entry Meta's avatar takes the author's\n * portrait and a Video's publication date takes the timestamp, and a\n * markdown link's target is followed, not read.\n */\nfunction printedTokens(node: AiDoctrineNode): string[] {\n const roles = AI_PALETTE[node.componentId]?.propRoles ?? {}\n return Object.entries(node.props ?? {}).flatMap(([key, value]) => {\n if (typeof value !== 'string' || roles[key] !== 'text') return []\n const tokens = aiBindingTokensIn(value.replace(/\\]\\([^)]*\\)/g, ']'))\n const unread = tokens.filter((token) => isAiAddressToken(token) || isAiUnreadToken(token))\n const whole = tokens.length === 1 && value.trim() === tokens[0]\n return unread.length && (key === 'children' || !whole) ? unread : []\n })\n}\n\n/**\n * The template door's check on a tree the doctrine admitted (rule 8): every\n * token is one the page fills, the h1 binds the subject's title, no block\n * that fills itself on another subject's page is placed — except inside a\n * Collection Entries card, where each fills from the card's own entry — and\n * no copy prints an address, a slug or a timestamp.\n */\nexport function aiTemplateBindingCheck(\n definition: AiTemplateSubjectDefinition,\n): (tree: AiValidatedTree) => AiDoctrineViolation[] {\n const allowed = new Set(definition.tokens.map((entry) => entry.token))\n return (tree) => {\n const nodes = tree.nodes as unknown as Record<string, AiDoctrineNode>\n const unknown = new Set<string>()\n const unknownAt: string[] = []\n const foreign: Array<{ id: string; componentId: string; owner: AiTemplateSubjectDefinition }> = []\n const titles: string[] = []\n const printed = new Set<string>()\n const printedAt: string[] = []\n let titleBound = false\n for (const { id, node, ancestors } of walkTree({ rootId: tree.rootId, nodes })) {\n const tokens = stringsIn(node.props).flatMap(aiBindingTokensIn)\n const strays = tokens.filter((token) => !allowed.has(token))\n if (strays.length) {\n for (const token of strays) unknown.add(token)\n unknownAt.push(id)\n }\n const shown = printedTokens(node).filter((token) => allowed.has(token))\n if (shown.length) {\n for (const token of shown) printed.add(token)\n printedAt.push(id)\n }\n const owner = BLOCK_SUBJECTS.get(node.componentId)\n const inCard = ancestors.some((ancestor) => nodes[ancestor]?.componentId === 'collectionEntries')\n if (owner && owner.subject !== definition.subject && !inCard) {\n foreign.push({ id, componentId: node.componentId, owner })\n }\n if (isPageTitle(node)) {\n titles.push(id)\n if (tokens.includes(definition.titleToken)) titleBound = true\n }\n }\n const violations: AiDoctrineViolation[] = []\n if (unknown.size) {\n violations.push({\n rule: 8,\n code: 'unknown-binding',\n message: `This binds ${[...unknown].slice(0, 3).join(', ')}, which ${definition.noun}’s page does not fill. Bind only the tokens listed for this page.`,\n nodeIds: aiModelNodeIds(unknownAt, tree.sourceIds),\n })\n }\n if (printed.size) {\n violations.push({\n rule: 8,\n code: 'printed-token',\n message: `This prints ${[...printed].slice(0, 3).join(', ')} in the page's copy, where a reader sees an address, a slug or a timestamp. A link or picture token goes whole in a link or media prop, and a slug or a timestamp stays off the page.`,\n nodeIds: aiModelNodeIds(printedAt, tree.sourceIds),\n })\n }\n // The doctrine already refuses a page with no h1 or with several.\n if (titles.length === 1 && !titleBound) {\n violations.push({\n rule: 8,\n code: 'typed-title',\n message: `The page title is typed, so every record would show the same one. Put ${definition.titleToken} in the h1.`,\n nodeIds: aiModelNodeIds(titles, tree.sourceIds),\n })\n }\n if (foreign.length) {\n const [first] = foreign\n violations.push({\n rule: 8,\n code: 'foreign-block',\n message: `The ${AI_PALETTE[first.componentId]?.displayName ?? first.componentId} block fills itself only on ${first.owner.noun}’s page, and this template is for ${definition.noun}. Remove it.`,\n nodeIds: aiModelNodeIds(\n foreign.map((entry) => entry.id),\n tree.sourceIds,\n ),\n })\n }\n return violations\n }\n}\n\nexport interface AiTemplateJobAdmissionDeps {\n readCollection?: typeof readAiTemplateCollection\n}\n\n/**\n * A template job is admitted with inputs that say what the page is for, for\n * a site of the job's own org that holds the collection it names, with a\n * template to spare.\n */\nexport function createAiTemplateJobAdmission(deps: AiTemplateJobAdmissionDeps = {}): AiJobAdmission {\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n return async (context) => {\n const inputs = parseAiTemplateJobInputs(context.inputs)\n if (typeof inputs === 'string') return { status: 400, error: inputs }\n return aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'template',\n org: context.org,\n ownCheck: async (hostId) =>\n inputs.collectionId && !(await readCollection(context.firestore, hostId, inputs.collectionId))\n ? { status: 404, error: 'That content collection is not on this site' }\n : aiJobPlanCreationsRefusal('template', context, hostId),\n })\n }\n}\n\nexport interface AiJobTemplateStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n readCollection?: typeof readAiTemplateCollection\n /** The starter examples block; built from the starters once per process otherwise. */\n examples?: () => AiSystemBlock\n}\n\n/**\n * The binding tokens the confirmed plan gives the template, against the\n * template a COPY actually produced (AGL-3024); `null` when the copy keeps\n * the plan, or when the plan promised no token to keep.\n *\n * The same hole as `aiCopiedLayoutReview` and `aiCopiedPageReview`, on the\n * third kind that can start from a duplicate. The generate path already holds\n * a built template to `aiPlanTemplateTokenViolations`; the copy path reached\n * none of it, so a plan reading \"duplicate it and bind the author's photo\"\n * got the duplicate and reported Done.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedTemplateReview(\n firestore: FirebaseFirestore.Firestore,\n input: {\n hostId: string\n id: string\n name: string\n plan: AiJobPlan | null\n definition: AiTemplateSubjectDefinition\n },\n): Promise<AiJobReview | null> {\n const { tokens, unreadable } = aiPlannedTemplateTokens(input.plan, input.definition)\n if (!tokens.length && !unreadable.length) return null\n const stored = await readAiTemplateDraftNodes(firestore, {\n hostId: input.hostId,\n id: input.id,\n })\n const nodes = (stored ?? {}) as unknown as Record<string, AiDoctrineNode>\n if (!nodes[CANVAS_ROOT_ELEMENT_ID]) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the template the plan names, and this job could not read the copy back to check it shows what the plan gives it. Open the template and check it before you apply it.`,\n findings: [],\n }\n }\n const violations = aiPlanTemplateTokenViolations(input.plan, input.definition, {\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes,\n })\n if (!violations.length) return null\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the template the plan names, and a copy is all it is: ${violations[0].message}`,\n violations,\n })\n}\n\nexport function createAiJobTemplateStep(deps: AiJobTemplateStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n const readCollection = deps.readCollection ?? readAiTemplateCollection\n const examples = deps.examples ?? aiTemplateExamplesSystemBlock\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a template job names no site, and its admission refuses one')\n const inputs = parseAiTemplateJobInputs(job.inputs)\n if (typeof inputs === 'string') {\n throw new Error(`a template job's inputs no longer read as they were admitted: ${inputs}`)\n }\n const definition = AI_TEMPLATE_SUBJECT_DEFINITIONS[inputs.subject]\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null): AiJobOutput => ({\n resource: 'template',\n id: draft.id,\n versionId: null,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.template') ?? aiModelForStep('job.template')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'template')\n const written = await readAiDraft(firestore, { kind: 'template', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot, collection] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n inputs.collectionId\n ? readCollection(firestore, hostId, inputs.collectionId)\n : Promise.resolve(null),\n ])\n if (inputs.collectionId && !collection) {\n throw new Error(`collection ${inputs.collectionId} left site ${hostId} after its template job was admitted`)\n }\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'template')\n const name = aiTemplateDraftName(definition, collection, creation?.name)\n\n // The plan starts from a copy of a template the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead, and one that cannot keep\n // the plan's tokens is built FROM (AGL-3143 §14): copying it could only end\n // in the review below. A source this step cannot read still copies.\n const source = creation?.duplicateOf\n ? inventory.templates.find((template) => template.id === creation.duplicateOf)\n : undefined\n const sourceReview = source\n ? await aiCopiedTemplateReview(firestore, { hostId, id: source.id, name: source.name, plan, definition })\n : null\n const sourceShort = Boolean(sourceReview?.findings.length)\n const sourceLines =\n source && sourceShort\n ? aiCopySourceLines(\n 'template',\n source.name,\n (await readAiTemplateDraftNodes(firestore, { hostId, id: source.id })) as Record<string, unknown> | null,\n )\n : []\n if (creation?.duplicateOf && source && !sourceShort) {\n const copy = await duplicate('template', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so nothing else can.\n const review = await aiCopiedTemplateReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n definition,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [output({ id: copy.id, versionId: null, name: copy.name, hostSubdomain })],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'template', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n const result = await runValidatedGeneration('template', {\n step: 'job.template',\n model,\n instructions: [...AI_JOB_TEMPLATE_INSTRUCTIONS, examples()],\n inventory,\n messages: [\n {\n role: 'user',\n content: aiJobTemplatePrompt(job, definition, collection, plan, name, sourceLines),\n },\n ],\n tool: aiDoctrineTreeTool('template'),\n maxTokens: AI_JOB_TEMPLATE_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.template'].thinking ? { thinking: AI_ROUTING_TABLE['job.template'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.template'].effort ? { effort: AI_ROUTING_TABLE['job.template'].effort } : {}),\n context: { bindingTokens: aiTemplateAddressTokens(definition) },\n // Rule 8's binding check, and the confirmed plan kept in both halves: a\n // token the plan promised that the template does not bind (AGL-3143\n // §11), and a component the plan reuses that it does not place. The\n // layout and component doors hold their builds to that reuse; the\n // template door did not, so it could drop a promised component and\n // still answer `Done` (AGL-3024).\n extend: (tree) => [\n ...aiTemplateBindingCheck(definition)(tree),\n ...aiPlanTemplateTokenViolations(plan, definition, {\n rootId: tree.rootId,\n nodes: tree.nodes as unknown as Record<string, AiDoctrineNode>,\n }),\n ...aiPlanReuseViolations(inventory, plan, aiPlacedComponentIds(tree), 'template'),\n ],\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n // Never written once the job is canceled (AGL-3616).\n throwIfAiJobCanceled(signal)\n const draft = await writeAiDraft(firestore, {\n kind: 'template',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n slug: aiTemplateDraftSlug(definition, collection),\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its template was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n return { ...spent, outputs: [output(draft, result.value.load)] }\n }\n}\n\n/** The template step, building the components and forms its plan creates before the template (AGL-3143 §15). */\nexport const runAiJobTemplateStep = aiBuildingPlanCreations('template', 'job.template', createAiJobTemplateStep())\n\n/** Registers the template step and the check a template job passes before it is created or resumed. */\nexport function registerAiTemplateJob(): void {\n registerAiJobStep('template', runAiJobTemplateStep, {\n minimumMs: AI_JOB_TEMPLATE_STEP_MINIMUM_MS,\n minimumMsFor: aiPlanCreationsRunMinimumMs('template'),\n })\n registerAiJobAdmission('template', createAiTemplateJobAdmission())\n}\n"],"names":["throwIfAiJobCanceled","hostCollectionKind","normalizeScreenSlug","duplicateResource","CANVAS_ROOT_ELEMENT_ID","AI_TEMPLATE_SUBJECT_DEFINITIONS","AI_TEMPLATE_SUBJECTS","aiBindingTokensIn","aiTemplateAddressTokens","isAiAddressToken","isAiUnreadToken","parseAiTemplateJobInputs","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","aiDoctrineTreeTool","runValidatedGeneration","walkTree","AI_PALETTE","aiTemplateExamplesSystemBlock","readSiteInventory","registerAiJobAdmission","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiTemplateDraftNodes","writeAiDraft","aiConfirmedPlan","aiCopySourceLines","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiModelNodeIds","aiPlacedComponentIds","aiPlanCreation","aiPlanReferenceLines","aiPlanReuseViolations","aiUnspentOutcome","aiPlannedTemplateTokens","aiPlanTemplateTokenViolations","aiBuildingPlanCreations","aiJobPlanCreationsRefusal","aiPlanCreationsRunMinimumMs","aiJobStepBudget","registerAiJobStep","AI_JOB_TEMPLATE_MAX_TOKENS","maxTokens","AI_JOB_TEMPLATE_STEP_BUDGET","tier","AI_JOB_TEMPLATE_STEP_MINIMUM_MS","minimumMs","AI_JOB_TEMPLATE_INSTRUCTIONS","text","join","readAiTemplateCollection","firestore","hostId","collectionId","snapshot","collection","doc","get","exists","data","value","trim","slug","id","name","aiTemplateDraftName","definition","planned","defaultName","aiTemplateDraftSlug","subject","aiJobTemplatePrompt","job","plan","source","of","noun","titleToken","tokens","map","entry","token","label","blocks","length","BLOCK_SUBJECTS","Map","flatMap","block","stringsIn","out","push","Array","isArray","inner","Object","values","isPageTitle","node","componentId","element","props","String","printedTokens","roles","propRoles","entries","key","replace","unread","filter","whole","aiTemplateBindingCheck","allowed","Set","tree","nodes","unknown","unknownAt","foreign","titles","printed","printedAt","titleBound","ancestors","rootId","strays","has","add","shown","owner","inCard","some","ancestor","includes","violations","size","rule","code","message","slice","nodeIds","sourceIds","first","displayName","createAiTemplateJobAdmission","deps","readCollection","context","inputs","status","error","orgId","kind","org","ownCheck","aiCopiedTemplateReview","input","unreadable","stored","reason","findings","createAiJobTemplateStep","readInventory","duplicate","examples","now","signal","modelFor","orgSnapshot","Error","output","draft","load","resource","versionId","hostSubdomain","model","draftId","written","outputs","inventory","Promise","all","resolve","creation","duplicateOf","templates","find","template","undefined","sourceReview","sourceShort","Boolean","sourceLines","copy","sourceId","uid","createdBy","attemptKey","ok","review","allowance","result","step","instructions","messages","role","content","tool","thinking","effort","bindingTokens","extend","spent","refused","runAiJobTemplateStep","registerAiTemplateJob","minimumMsFor"],"mappings":"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"}
@@ -99,6 +99,14 @@ export interface AiJobStepOutcome {
99
99
  * a bounded number of times a day.
100
100
  */
101
101
  uncredited?: boolean;
102
+ /**
103
+ * The `failure` is the workspace's own limit, not a fault of ours: the
104
+ * month's Free AI credits are spent (a 429 from the kind's admission, asked
105
+ * when the plan is kept). The machine fails the job and gives the step's
106
+ * credits back exactly as for our failure, but announces it as NOT ours, so
107
+ * no staff alert is raised for a customer reaching their allowance.
108
+ */
109
+ customerLimit?: boolean;
102
110
  /**
103
111
  * What a guided site start put live on its last pass (AGL-3596), kept on
104
112
  * the job by the machine in the write that records the pass: a step never
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-step.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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobItemFailure,\n AiJobItemLedger,\n AiJobItemStatus,\n AiJobSitePublish,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiOverlayCopyRequested } from '../model/ai-overlay-copy'\nimport { runAiOverlayCopy } from './ai-job-overlay-copy'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n /**\n * The answer was refused by the platform's own checks, not the model's\n * (AGL-3594): the machine meters the step as any other and then gives the\n * job's credits back to the meters it was charged to (`refundJobCredits`),\n * a bounded number of times a day.\n */\n uncredited?: boolean\n /**\n * What a guided site start put live on its last pass (AGL-3596), kept on\n * the job by the machine in the write that records the pass: a step never\n * writes the job document itself.\n */\n sitePublish?: AiJobSitePublish\n /**\n * A `build` pass's unit and what it came to (AGL-3616), which the machine\n * writes to the job's item ledger in the transaction that records the\n * pass's spend. An item's failure is never the step's: it is a row, and\n * the build goes on.\n */\n item?: AiJobItemOutcome\n /** A `build`'s ledger to start from, written by the machine in the same transaction (AGL-3616). */\n items?: AiJobItemLedger[]\n}\n\n/** What one `build` pass came to for the unit it ran (AGL-3616). */\nexport interface AiJobItemOutcome {\n slot: string\n /** `running` while the unit asks for another pass of its own. */\n status: AiJobItemStatus\n failure?: AiJobItemFailure | null\n /** Customer-safe: what the person should know about the item. */\n note?: string | null\n degradedBy?: string[]\n /** The ids of the outputs this pass produced for the item. */\n outputs?: string[]\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n 'the brief did not give you; where a detail is missing, leave a ' +\n 'clearly marked placeholder in square brackets.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n // Overlay copy (AGL-3603): the same one request, answered as fields.\n if (aiOverlayCopyRequested(job.inputs)) {\n return runAiOverlayCopy({\n job,\n model,\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n }\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.text'].effort ? { effort: AI_ROUTING_TABLE['job.text'].effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runAiRequest","aiJobStepBudget","aiOverlayCopyRequested","runAiOverlayCopy","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","result","system","messages","role","content","thinking","effort","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-step.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 type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n AiJobItemFailure,\n AiJobItemLedger,\n AiJobItemStatus,\n AiJobSitePublish,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiOverlayCopyRequested } from '../model/ai-overlay-copy'\nimport { runAiOverlayCopy } from './ai-job-overlay-copy'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n /**\n * The answer was refused by the platform's own checks, not the model's\n * (AGL-3594): the machine meters the step as any other and then gives the\n * job's credits back to the meters it was charged to (`refundJobCredits`),\n * a bounded number of times a day.\n */\n uncredited?: boolean\n /**\n * The `failure` is the workspace's own limit, not a fault of ours: the\n * month's Free AI credits are spent (a 429 from the kind's admission, asked\n * when the plan is kept). The machine fails the job and gives the step's\n * credits back exactly as for our failure, but announces it as NOT ours, so\n * no staff alert is raised for a customer reaching their allowance.\n */\n customerLimit?: boolean\n /**\n * What a guided site start put live on its last pass (AGL-3596), kept on\n * the job by the machine in the write that records the pass: a step never\n * writes the job document itself.\n */\n sitePublish?: AiJobSitePublish\n /**\n * A `build` pass's unit and what it came to (AGL-3616), which the machine\n * writes to the job's item ledger in the transaction that records the\n * pass's spend. An item's failure is never the step's: it is a row, and\n * the build goes on.\n */\n item?: AiJobItemOutcome\n /** A `build`'s ledger to start from, written by the machine in the same transaction (AGL-3616). */\n items?: AiJobItemLedger[]\n}\n\n/** What one `build` pass came to for the unit it ran (AGL-3616). */\nexport interface AiJobItemOutcome {\n slot: string\n /** `running` while the unit asks for another pass of its own. */\n status: AiJobItemStatus\n failure?: AiJobItemFailure | null\n /** Customer-safe: what the person should know about the item. */\n note?: string | null\n degradedBy?: string[]\n /** The ids of the outputs this pass produced for the item. */\n outputs?: string[]\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n 'the brief did not give you; where a detail is missing, leave a ' +\n 'clearly marked placeholder in square brackets.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n // Overlay copy (AGL-3603): the same one request, answered as fields.\n if (aiOverlayCopyRequested(job.inputs)) {\n return runAiOverlayCopy({\n job,\n model,\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n }\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.text'].effort ? { effort: AI_ROUTING_TABLE['job.text'].effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runAiRequest","aiJobStepBudget","aiOverlayCopyRequested","runAiOverlayCopy","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","result","system","messages","role","content","thinking","effort","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import { AI_JOB_PLAN_STEP, type AiJob, type AiJobApplied, type AiJobItemLedger, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobSitePublish, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
18
+ import type { AiJobCreditsConfirmation } from '../model/ai-credit-estimate';
18
19
  import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
19
20
  import { type AiActivityActor } from '../activity/ai-activity';
20
21
  import { type AiEffort, type AiUsage } from '../runtime/ai-runtime';
@@ -256,6 +257,8 @@ export interface CreateAiJobInput {
256
257
  createdBy: string;
257
258
  /** The creator's address, for the activity row; the document stores the uid only. */
258
259
  createdByEmail?: string | null;
260
+ /** A Free start the creator chose to build past what was left (AGL-3722). */
261
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
259
262
  }
260
263
  /**
261
264
  * A new job, `queued`, with its step plan laid out and nothing run.
@@ -442,21 +445,36 @@ export declare function failAiJob(firestore: Firestore, orgId: string, jobId: st
442
445
  * tries again; its attempt is not counted, because nothing was attempted.
443
446
  */
444
447
  export declare function markAiJobNeedsInput(firestore: Firestore, orgId: string, jobId: string, reason: string, now?: Date): Promise<AiJob>;
448
+ /** What a cancel did (AGL-3616). */
449
+ export interface AiJobCancelResult {
450
+ job: AiJob;
451
+ /** The cancel was new: it ended the job, or asked the step in flight to stop. */
452
+ changed: boolean;
453
+ }
445
454
  /**
446
- * Cancel. Idempotent — a terminal job is returned unchanged — and it does
447
- * not wait for a step in flight: the runner holding the lease finishes its
448
- * provider call, records what it cost, and finds the job canceled when it
449
- * goes to complete it.
455
+ * Cancel (AGL-2904, AGL-3616). One transaction decides it against whatever
456
+ * else commits, so a cancel racing a finish resolves to whichever committed
457
+ * first: a job already `done`, `failed` or `canceled` is returned unchanged.
458
+ *
459
+ * - **No step in flight** (queued, parked for a person or the meter, or a
460
+ * lease that ran out): the job ends `canceled` in this write. It holds
461
+ * nothing, its unbuilt items are skipped, and nothing it built is touched.
462
+ * - **A step in flight** (a live lease): the cancel is recorded as
463
+ * `cancelRequested` and the job keeps its status until the runner holding
464
+ * the lease comes back. The runner watches for it and aborts its provider
465
+ * call; whatever the step had spent is metered, and the machine's next
466
+ * write ends the job `canceled` (`recordStep`, or the finish or failure it
467
+ * was about to write, which a pending cancel turns into the cancel).
468
+ *
469
+ * Either way a canceled job is never failed: no `ai.jobFailed` alert, no
470
+ * failure notice, no give-back (what ran stays charged), no publish.
450
471
  *
451
- * The activity row is written only when the cancel changed something: a
452
- * second cancel of the same job is not a second act. `actor` is the member
453
- * who canceled; a cancel with nobody named is recorded as nobody's rather
454
- * than as the creator's.
472
+ * The activity row is written once, when the cancel is first asked: a second
473
+ * cancel of the same job is not a second act. `actor` is the member who
474
+ * canceled; a cancel with nobody named is recorded as nobody's rather than
475
+ * as the creator's.
455
476
  */
456
- export declare function cancelAiJob(firestore: Firestore, orgId: string, jobId: string, now?: Date, actor?: AiActivityActor | null): Promise<{
457
- job: AiJob;
458
- changed: boolean;
459
- }>;
477
+ export declare function cancelAiJob(firestore: Firestore, orgId: string, jobId: string, now?: Date, actor?: AiActivityActor | null): Promise<AiJobCancelResult>;
460
478
  /**
461
479
  * The kinds settled item by item (AGL-3616): a build, and a site scaffold,
462
480
  * which is the build's preset. Their step writes an item ledger; a failed
@@ -497,7 +515,10 @@ export declare function settleAiBuildJob(firestore: Firestore, orgId: string, jo
497
515
  * `changed: false` for a job that is not a finished build, or has nothing to
498
516
  * try again.
499
517
  */
500
- export declare function retryAiBuildJob(firestore: Firestore, orgId: string, jobId: string, now?: Date): Promise<{
518
+ export declare function retryAiBuildJob(firestore: Firestore, orgId: string, jobId: string, now?: Date, options?: {
519
+ /** A Free Try again the person chose to run past what was left (AGL-3722). */
520
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
521
+ }): Promise<{
501
522
  job: AiJob;
502
523
  changed: boolean;
503
524
  retried: string[];
@@ -523,6 +544,17 @@ export declare function resumeAiJob(firestore: Firestore, orgId: string, jobId:
523
544
  * pass put its pages live.
524
545
  */
525
546
  publishConfirmed?: boolean;
547
+ /**
548
+ * A Free build or start the person chose to build past what was left
549
+ * (AGL-3722), kept on the job: who, when, what was left.
550
+ */
551
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
552
+ /**
553
+ * The smaller first build the person chose instead (AGL-3722): its plan
554
+ * — the home page and what it needs — is the one confirmed, in place of
555
+ * the plan the job proposed. Only while that plan waits for confirmation.
556
+ */
557
+ reducePlan?: (plan: AiJobPlan) => AiJobPlan | null;
526
558
  }): Promise<{
527
559
  job: AiJob;
528
560
  changed: boolean;
@@ -559,6 +591,8 @@ export declare function aiJobRefusalText(org: Partial<AglynOrgBilling> | null, r
559
591
  export interface RunAiJobStepOptions {
560
592
  /** Who is claiming: a request id or a beat id. */
561
593
  owner: string;
594
+ /** How often the running step re-reads its job for a cancel (AGL-3616); specs shorten it. */
595
+ cancelPollMs?: number;
562
596
  now?: Date;
563
597
  /**
564
598
  * A reservation the caller already holds for THIS step — the console
@@ -620,6 +654,14 @@ export type AiJobStepRun =
620
654
  | {
621
655
  outcome: 'paused';
622
656
  job: AiJob;
657
+ }
658
+ /**
659
+ * A person canceled the job while this step held it (AGL-3616): the step
660
+ * finished or aborted, what it spent is recorded, and the job is canceled.
661
+ */
662
+ | {
663
+ outcome: 'canceled';
664
+ job: AiJob;
623
665
  };
624
666
  /** A step outcome that carries no tokens and no cost: the provider was never reached. */
625
667
  export declare function aiJobStepSpentNothing(outcome: Pick<AiJobStepOutcome, 'usage' | 'estCostUsd'>): boolean;