@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227

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 (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -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} 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'\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\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 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","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} 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'\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\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 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","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":"AAAA;;;;;;;;;;;;;;;CAeC;AASD,SAASA,aAAa,QAAyB,0BAAsB;AACrE,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,0BAAsB;AACvE,SAASC,YAAY,QAA2C,2BAAuB;AAEvF,SAASC,eAAe,QAAQ,qBAAiB;AAEjD;;;;;;;;;;;;;CAaC,GAED,gFAAgF,GAChF,OAAO,SAASC;IACd,OAAOH,eAAe;AACxB;AAEA,4DAA4D,GAC5D,OAAO,MAAMI,yBAAyBL,gBAAgB,CAAC,WAAW,CAACM,SAAS,CAAA;AAE5E;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0BJ,gBAAgB;IACrDK,MAAMT,aAAa,CAAC,WAAW;IAC/BO,WAAWD;IACXI,UAAU;IACVC,SAAS;AACX,GAAE;AAEF,yDAAyD,GACzD,OAAO,MAAMC,8BAA8BJ,wBAAwBK,SAAS,CAAA;AAE5E,+EAA+E,GAC/E,OAAO,MAAMC,yBAAyB,KAAK;AA+E3C;;;CAGC,GACD,OAAO,MAAMC,qBAAsC;IACjD;QACEC,MACE,sEACA,sEACA,yEACA,0EACA,wEACA,wEACA,oEACA;QACFC,iBAAiB;IACnB;CACD,CAAA;AAED,8EAA8E,GAC9E,OAAO,SAASC,gBAAgBC,GAAoC;QAE9CA,aAEXA;IAHT,MAAMC,QAAQD,IAAIC,KAAK,CAACC,KAAK,CAAC,GAAGP;IACjC,MAAMQ,OAAO,SAAOH,cAAAA,IAAII,MAAM,qBAAVJ,WAAY,CAAC,OAAO,MAAK,WAAWA,IAAII,MAAM,CAAC,OAAO,GAAG;IAC7E,MAAMC,WACJ,SAAOL,eAAAA,IAAII,MAAM,qBAAVJ,YAAY,CAAC,WAAW,MAAK,WAAWA,IAAII,MAAM,CAAC,WAAW,GAAG;IAC1E,MAAME,QAAQ;QAAC,CAAC,OAAO,EAAEL,OAAO;KAAC;IACjC,IAAIE,MAAMG,MAAMC,IAAI,CAAC,CAAC,MAAM,EAAEJ,MAAM;IACpC,IAAIE,UAAUC,MAAMC,IAAI,CAAC,CAAC,UAAU,EAAEF,UAAU;IAChD,OAAOC,MAAME,IAAI,CAAC;AACpB;AAEA,OAAO,MAAMC,mBAAoC,OAAO,EAAET,GAAG,EAAEU,MAAM,EAAEC,QAAQ,EAAE;cAiC7DX;IAhClB,MAAMY,gBAAQD,4BAAAA,SAAW,8BAAezB;IACxC,MAAM2B,SAAS,MAAM7B,aAAa;QAChC4B;QACAE,QAAQlB;QACRmB,UAAU;YAAC;gBAAEC,MAAM;gBAAQC,SAASlB,gBAAgBC;YAAK;SAAE;QAC3D,0EAA0E;QAC1E,kCAAkC;QAClCZ,WAAWC,wBAAwBD,SAAS,CAACwB;OACzC9B,gBAAgB,CAAC,WAAW,CAACoC,QAAQ,GAAG;QAAEA,UAAUpC,gBAAgB,CAAC,WAAW,CAACoC,QAAQ;IAAC,IAAI,CAAC,GAC/FpC,gBAAgB,CAAC,WAAW,CAACqC,MAAM,GAAG;QAAEA,QAAQrC,gBAAgB,CAAC,WAAW,CAACqC,MAAM;IAAC,IAAI,CAAC;QAC7FC,QAAQ;OACJV,SAAS;QAAEA;IAAO,IAAI,CAAC;IAE7B,IAAIG,OAAOQ,IAAI,KAAK,WAAW;QAC7B,OAAO;YACLC,SAAS,EAAE;YACXC,OAAOV,OAAOU,KAAK;YACnBC,YAAYX,OAAOW,UAAU;YAC7BZ;YACAa,YAAYZ,OAAOY,UAAU;YAC7BC,SAAS;QACX;IACF;IACA,MAAM7B,OAAOgB,OAAOhB,IAAI,CAAC8B,IAAI;IAC7B,OAAO;QACLL,SAASzB,OACL;YACE;gBACE+B,UAAU;gBACV,6DAA6D;gBAC7D,sDAAsD;gBACtDC,IAAI;gBACJC,MAAM,GAAE9B,cAAAA,IAAI8B,MAAM,YAAV9B,cAAc;gBACtB+B,OAAO;gBACPlC;YACF;SACD,GACD,EAAE;QACN0B,OAAOV,OAAOU,KAAK;QACnBC,YAAYX,OAAOW,UAAU;QAC7BZ;QACAa,YAAYZ,OAAOY,UAAU;IAC/B;AACF,EAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications';
18
+ import { type AiJobTransitionListener } from './ai-jobs';
19
+ /**
20
+ * The person who started an AI job is told when it needs them, when it
21
+ * finishes and when it stops (AGL-3593), through the console's notifications
22
+ * — the bell, and the generic notification email for a person who switched
23
+ * that channel on for the category. No other channel: there is no push.
24
+ *
25
+ * The machine calls this where the job's state changes, once per change
26
+ * (`registerAiJobTransitionListener`): the request that ran a step inline,
27
+ * or the beat that ran it with nobody watching — which is exactly when a
28
+ * person needs telling. The recipient is the job's creator, whose job it is
29
+ * and who alone confirms its plan; a teammate is not told about someone
30
+ * else's draft.
31
+ *
32
+ * `notifyUsers` never throws, and honors the person's per-category,
33
+ * per-workspace and per-site preferences.
34
+ */
35
+ export declare function aiJobTransitionNotifier(notify?: typeof notifyUsers): AiJobTransitionListener;
36
+ /** Registers the notifier the jobs machine tells about each change. */
37
+ export declare function registerAiJobsNotify(): void;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { notifyUsers } from "@aglyn/tenant-data-admin/server/notifications";
17
+ import { aiJobNotice } from "../model/ai-job-notice.js";
18
+ import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
19
+ import { registerAiJobTransitionListener } from "./ai-jobs.js";
20
+ /**
21
+ * The person who started an AI job is told when it needs them, when it
22
+ * finishes and when it stops (AGL-3593), through the console's notifications
23
+ * — the bell, and the generic notification email for a person who switched
24
+ * that channel on for the category. No other channel: there is no push.
25
+ *
26
+ * The machine calls this where the job's state changes, once per change
27
+ * (`registerAiJobTransitionListener`): the request that ran a step inline,
28
+ * or the beat that ran it with nobody watching — which is exactly when a
29
+ * person needs telling. The recipient is the job's creator, whose job it is
30
+ * and who alone confirms its plan; a teammate is not told about someone
31
+ * else's draft.
32
+ *
33
+ * `notifyUsers` never throws, and honors the person's per-category,
34
+ * per-workspace and per-site preferences.
35
+ */ export function aiJobTransitionNotifier(notify = notifyUsers) {
36
+ return async ({ job, to })=>{
37
+ var _job_review;
38
+ if (!job.createdBy) return;
39
+ // A guided site start's plan is confirmed for it the moment it is kept
40
+ // (AGL-3594): there is nothing for the person to confirm.
41
+ if (to === 'needs-review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' && aiJobAutoConfirms(job)) return;
42
+ await notify([
43
+ job.createdBy
44
+ ], aiJobNotice(job, to));
45
+ };
46
+ }
47
+ /** Registers the notifier the jobs machine tells about each change. */ export function registerAiJobsNotify() {
48
+ registerAiJobTransitionListener(aiJobTransitionNotifier());
49
+ }
50
+
51
+ //# sourceMappingURL=ai-jobs-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { aiJobNotice } from '../model/ai-job-notice'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n await notify([job.createdBy], aiJobNotice(job, to))\n }\n}\n\n/** Registers the notifier the jobs machine tells about each change. */\nexport function registerAiJobsNotify(): void {\n registerAiJobTransitionListener(aiJobTransitionNotifier())\n}\n"],"names":["notifyUsers","aiJobNotice","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","job","to","createdBy","review","reason","registerAiJobsNotify"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,gDAA+C;AAC3E,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,+BAA+B,QAAsC,eAAW;AAEzF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBACdC,SAA6BL,WAAW;IAExC,OAAO,OAAO,EAAEM,GAAG,EAAEC,EAAE,EAAE;YAIMD;QAH7B,IAAI,CAACA,IAAIE,SAAS,EAAE;QACpB,uEAAuE;QACvE,0DAA0D;QAC1D,IAAID,OAAO,kBAAkBD,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAUR,kBAAkBI,MAAM;QACtF,MAAMD,OAAO;YAACC,IAAIE,SAAS;SAAC,EAAEP,YAAYK,KAAKC;IACjD;AACF;AAEA,qEAAqE,GACrE,OAAO,SAASI;IACdR,gCAAgCC;AAClC"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
17
+ import { AI_JOB_PLAN_STEP, type AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
18
18
  import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
19
19
  import { type AiActivityActor } from '../activity/ai-activity';
20
20
  import { type AiEffort, type AiUsage } from '../runtime/ai-runtime';
@@ -107,8 +107,7 @@ export declare const AI_JOB_NOT_AVAILABLE_COPY = "This kind of AI job is not ava
107
107
  export declare const AI_JOB_REFUSED_COPY = "The model declined this brief. Try rephrasing it.";
108
108
  type Firestore = FirebaseFirestore.Firestore;
109
109
  export declare function isAiJobTerminal(status: AiJobStatus): boolean;
110
- /** The step a planned kind runs first (AGL-2935). */
111
- export declare const AI_JOB_PLAN_STEP = "plan";
110
+ export { AI_JOB_PLAN_STEP };
112
111
  /**
113
112
  * The kinds that build site structure, and so plan before they generate
114
113
  * (AGL-2935): the plan their first step proposes is what their generation
@@ -196,6 +195,26 @@ export type AiJobPauseReader = (input: {
196
195
  * runs every job, as a process with no lockdown carrier would.
197
196
  */
198
197
  export declare function registerAiJobPauseReader(reader: AiJobPauseReader | null): void;
198
+ /**
199
+ * A change a person is told about: the job's plan waits for them (or a step
200
+ * stopped for their decision), the job finished, or it stopped.
201
+ */
202
+ export type AiJobTransition = 'needs-review' | 'done' | 'failed';
203
+ export type AiJobTransitionListener = (input: {
204
+ job: AiJob;
205
+ to: AiJobTransition;
206
+ }) => Promise<void> | void;
207
+ /**
208
+ * The listener told each time a job ENTERS `needs_review`, `done` or `failed`
209
+ * — once per entry, from the write that made it, so a stream that re-reads
210
+ * the job or a beat that sees it again tells nobody twice. Registered by its
211
+ * own module (`ai-jobs-notify.ts`) from the console's server surface, as the
212
+ * pause reader is, so this machine never loads the notification fan-out.
213
+ * `null` unregisters it.
214
+ */
215
+ export declare function registerAiJobTransitionListener(listener: AiJobTransitionListener | null): void;
216
+ /** Whether a transition listener is registered: what a surface that runs jobs must have. */
217
+ export declare function aiJobTransitionListenerRegistered(): boolean;
199
218
  /** Whether a pause reader is registered: what a surface that runs jobs must have. */
200
219
  export declare function aiJobPauseReaderRegistered(): boolean;
201
220
  /** The runner for one step of one job: the plan step by its name, else the kind's own. */
@@ -230,9 +249,17 @@ export declare function createAiJob(firestore: Firestore, input: CreateAiJobInpu
230
249
  export declare function getAiJob(firestore: Firestore, orgId: string, jobId: string): Promise<AiJob | null>;
231
250
  export interface ListAiJobsOptions {
232
251
  status?: AiJobStatus;
252
+ /**
253
+ * Any of these statuses (AGL-3593): the console's indicator asks for every
254
+ * job not yet settled in one read. Ignored when `status` is given.
255
+ */
256
+ statuses?: readonly AiJobStatus[];
233
257
  limit?: number;
234
258
  }
235
- /** Newest first. Needs the (status, createdAt) index when filtered. */
259
+ /**
260
+ * Newest first. Needs the (status, createdAt) index when filtered, which
261
+ * answers an `in` over statuses as it answers one status.
262
+ */
236
263
  export declare function listAiJobs(firestore: Firestore, orgId: string, options?: ListAiJobsOptions): Promise<AiJob[]>;
237
264
  /** The wire form. `running` is the lease, read against `now`. */
238
265
  export declare function aiJobSummary(job: AiJob, now?: Date): AiJobSummary;
@@ -23,7 +23,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
23
23
  import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
24
24
  import { checkEntitlement } from "@aglyn/aglyn/server";
25
25
  import { assistCreditsFromUsd, assistFreeTasteRefusalText, assistHardCapRefusalText, assistRefusedByHardCap } from "../usage/assist-credits.js";
26
- import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
26
+ import { AI_JOB_PLAN_STEP, AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
27
27
  import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
28
28
  import { aiOverageReservationRefusal } from "../billing/ai-overage-gate.js";
29
29
  import { aiAllotmentRefusalText } from "../model/ai-allotments.js";
@@ -39,7 +39,10 @@ import { aiMintJobDraftIds } from "./ai-job-draft-ids.js";
39
39
  import { AI_JOB_TEXT_STEP_MINIMUM_MS, runAiJobTextStep } from "./ai-job-text-step.js";
40
40
  import { AI_JOB_SEO_STEP_MINIMUM_MS } from "./ai-job-seo-budget.js";
41
41
  import { AI_JOB_THEME_STEP_MINIMUM_MS } from "./ai-job-theme-budget.js";
42
- import { assistExchangeExpiry, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
42
+ import { assistExchangeExpiry, assistUsageDay, assistUsageMonth, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
43
+ import { aiJobRefundCredits, refundJobCredits } from "../usage/assist-job-refund.js";
44
+ import { freeAssistAccount } from "../usage/assist-free-taste.js";
45
+ import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
43
46
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
44
47
  /**
45
48
  * AI generation jobs — the Firestore state machine (AGL-2904).
@@ -134,7 +137,9 @@ export function isAiJobTerminal(status) {
134
137
  return AI_JOB_TERMINAL_STATUSES.includes(status);
135
138
  }
136
139
  // ── The step runner registry ──────────────────────────────────────────────
137
- /** The step a planned kind runs first (AGL-2935). */ export const AI_JOB_PLAN_STEP = 'plan';
140
+ // The step a planned kind runs first (AGL-2935), named in the model so the
141
+ // console reads a job's phase off its steps without loading this machine.
142
+ export { AI_JOB_PLAN_STEP };
138
143
  /**
139
144
  * The kinds that build site structure, and so plan before they generate
140
145
  * (AGL-2935): the plan their first step proposes is what their generation
@@ -232,6 +237,36 @@ let pauseReader = null;
232
237
  */ export function registerAiJobPauseReader(reader) {
233
238
  pauseReader = reader;
234
239
  }
240
+ let transitionListener = null;
241
+ /**
242
+ * The listener told each time a job ENTERS `needs_review`, `done` or `failed`
243
+ * — once per entry, from the write that made it, so a stream that re-reads
244
+ * the job or a beat that sees it again tells nobody twice. Registered by its
245
+ * own module (`ai-jobs-notify.ts`) from the console's server surface, as the
246
+ * pause reader is, so this machine never loads the notification fan-out.
247
+ * `null` unregisters it.
248
+ */ export function registerAiJobTransitionListener(listener) {
249
+ transitionListener = listener;
250
+ }
251
+ /** Whether a transition listener is registered: what a surface that runs jobs must have. */ export function aiJobTransitionListenerRegistered() {
252
+ return transitionListener !== null;
253
+ }
254
+ /** Tells the listener; a listener that throws never fails the write it follows. */ async function announceAiJobTransition(job, to) {
255
+ if (!transitionListener) return;
256
+ try {
257
+ await transitionListener({
258
+ job,
259
+ to
260
+ });
261
+ } catch (error) {
262
+ console.error('ai job transition listener failed', {
263
+ orgId: job.orgId,
264
+ jobId: job.$id,
265
+ to,
266
+ error
267
+ });
268
+ }
269
+ }
235
270
  /** Whether a pause reader is registered: what a surface that runs jobs must have. */ export function aiJobPauseReaderRegistered() {
236
271
  return pauseReader !== null;
237
272
  }
@@ -378,16 +413,23 @@ function jobFrom(snapshot) {
378
413
  export async function getAiJob(firestore, orgId, jobId) {
379
414
  return jobFrom(await jobsCollection(firestore, orgId).doc(jobId).get());
380
415
  }
381
- /** Newest first. Needs the (status, createdAt) index when filtered. */ export async function listAiJobs(firestore, orgId, options = {}) {
416
+ /**
417
+ * Newest first. Needs the (status, createdAt) index when filtered, which
418
+ * answers an `in` over statuses as it answers one status.
419
+ */ export async function listAiJobs(firestore, orgId, options = {}) {
382
420
  var _options_limit;
421
+ var _options_statuses;
383
422
  const limit = Math.min(Math.max(1, Math.floor((_options_limit = options.limit) != null ? _options_limit : 20)), 100);
384
423
  let query = jobsCollection(firestore, orgId);
385
424
  if (options.status) query = query.where('status', '==', options.status);
425
+ else if ((_options_statuses = options.statuses) == null ? void 0 : _options_statuses.length) query = query.where('status', 'in', [
426
+ ...options.statuses
427
+ ]);
386
428
  const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
387
429
  return snapshot.docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
388
430
  }
389
431
  /** The wire form. `running` is the lease, read against `now`. */ export function aiJobSummary(job, now = new Date()) {
390
- var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
432
+ var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _job_refundedCredits, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
391
433
  var _job_lease, _job_inputs;
392
434
  const leaseUntil = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
393
435
  return {
@@ -412,6 +454,7 @@ export async function getAiJob(firestore, orgId, jobId) {
412
454
  outputs: (_job_outputs = job.outputs) != null ? _job_outputs : [],
413
455
  creditsReserved: (_job_creditsReserved = job.creditsReserved) != null ? _job_creditsReserved : 0,
414
456
  creditsSpent: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
457
+ refundedCredits: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0,
415
458
  createdBy: job.createdBy,
416
459
  createdAt: (_toIso = toIso(job.createdAt)) != null ? _toIso : new Date(0).toISOString(),
417
460
  updatedAt: (_toIso1 = toIso(job.updatedAt)) != null ? _toIso1 : new Date(0).toISOString(),
@@ -605,7 +648,7 @@ function nextStepIndex(job) {
605
648
  * in the same transaction, so no beat can claim the next step in between.
606
649
  */ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
607
650
  const ref = jobsCollection(firestore, orgId).doc(jobId);
608
- return firestore.runTransaction(async (tx)=>{
651
+ const recorded = await firestore.runTransaction(async (tx)=>{
609
652
  var _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1;
610
653
  var _job_lease;
611
654
  const job = jobFrom(await tx.get(ref));
@@ -654,9 +697,16 @@ function nextStepIndex(job) {
654
697
  });
655
698
  return {
656
699
  job: _extends({}, job, patch),
657
- remaining
700
+ remaining,
701
+ // Entered `needs_review` in this write, rather than parked again.
702
+ parked: parksForReview && job.status !== 'needs_review'
658
703
  };
659
704
  });
705
+ if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
706
+ return {
707
+ job: recorded.job,
708
+ remaining: recorded.remaining
709
+ };
660
710
  }
661
711
  async function transition(firestore, orgId, jobId, apply) {
662
712
  const ref = jobsCollection(firestore, orgId).doc(jobId);
@@ -678,13 +728,14 @@ async function transition(firestore, orgId, jobId, apply) {
678
728
  });
679
729
  }
680
730
  /** Every step done: the job is `done` and holds nothing. */ export async function completeAiJob(firestore, orgId, jobId, now = new Date()) {
681
- const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
731
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
682
732
  status: 'done',
683
733
  creditsReserved: 0,
684
734
  lease: null,
685
735
  error: null,
686
736
  updatedAt: now
687
737
  });
738
+ if (changed) await announceAiJobTransition(job, 'done');
688
739
  return job;
689
740
  }
690
741
  /**
@@ -699,7 +750,7 @@ async function transition(firestore, orgId, jobId, apply) {
699
750
  stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
700
751
  error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
701
752
  });
702
- const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
753
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
703
754
  status: 'failed',
704
755
  error: message,
705
756
  creditsReserved: 0,
@@ -711,6 +762,7 @@ async function transition(firestore, orgId, jobId, apply) {
711
762
  }) : step),
712
763
  updatedAt: now
713
764
  });
765
+ if (changed) await announceAiJobTransition(job, 'failed');
714
766
  return job;
715
767
  }
716
768
  /**
@@ -760,6 +812,57 @@ async function transition(firestore, orgId, jobId, apply) {
760
812
  }
761
813
  return result;
762
814
  }
815
+ /**
816
+ * Gives back what a job spent when it failed on our side (AGL-3594), and
817
+ * records the give-back on the job — `refundedCredits`, `refundReason` and
818
+ * the count its next key is numbered by. Everything the job spent and has not
819
+ * given back where it delivered nothing; the failing step's own spend where
820
+ * it delivered drafts, which it keeps. `assist-job-refund.ts` has the rule
821
+ * and the bound. Never throws: a give-back that cannot be made leaves the job
822
+ * as the failure left it, metered as every other step.
823
+ */ async function refundOurFailure(firestore, orgId, job, input) {
824
+ try {
825
+ var _job_outputs, _job_creditsSpent, _job_refundedCredits, _data, _job_refunds;
826
+ const delivered = ((_job_outputs = job.outputs) != null ? _job_outputs : []).some((output)=>output.resource !== 'seo');
827
+ const credits = aiJobRefundCredits({
828
+ delivered,
829
+ jobCredits: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
830
+ stepCredits: input.stepCredits,
831
+ alreadyRefunded: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0
832
+ });
833
+ if (credits <= 0) return job;
834
+ const free = input.free !== undefined ? input.free : freeAssistAccount((_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {});
835
+ const ordinal = (_job_refunds = job.refunds) != null ? _job_refunds : 0;
836
+ const refund = await refundJobCredits(firestore, {
837
+ orgId,
838
+ free,
839
+ jobId: job.$id,
840
+ ordinal,
841
+ credits,
842
+ month: assistUsageMonth(input.now),
843
+ day: assistUsageDay(input.now),
844
+ reason: input.reason
845
+ });
846
+ if (refund.status !== 'returned') return job;
847
+ const { job: refunded } = await transition(firestore, orgId, job.$id, (current)=>{
848
+ var _current_refundedCredits, _current_refunds;
849
+ return {
850
+ refundedCredits: ((_current_refundedCredits = current.refundedCredits) != null ? _current_refundedCredits : 0) + credits,
851
+ refundReason: input.reason,
852
+ refunds: ((_current_refunds = current.refunds) != null ? _current_refunds : 0) + 1,
853
+ updatedAt: input.now
854
+ };
855
+ });
856
+ return refunded;
857
+ } catch (error) {
858
+ console.error('ai job refund failed', {
859
+ orgId,
860
+ jobId: job.$id,
861
+ error
862
+ });
863
+ return job;
864
+ }
865
+ }
763
866
  /**
764
867
  * Resume a job that stopped for a person (AGL-2935): confirm its plan, or
765
868
  * try again the step whose answer broke a building rule. One transaction,
@@ -938,22 +1041,32 @@ async function transition(firestore, orgId, jobId, apply) {
938
1041
  const runner = aiJobRunnerForStep(job.kind, step.name);
939
1042
  if (!runner) {
940
1043
  await releaseHeld();
1044
+ const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
1045
+ stepIndex,
1046
+ error: `no runner registered for kind ${job.kind}`
1047
+ }, now);
941
1048
  return {
942
1049
  outcome: 'failed',
943
- job: await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
944
- stepIndex,
945
- error: `no runner registered for kind ${job.kind}`
946
- }, now)
1050
+ job: await refundOurFailure(firestore, orgId, failed, {
1051
+ reason: 'unavailable',
1052
+ stepCredits: 0,
1053
+ now
1054
+ })
947
1055
  };
948
1056
  }
949
1057
  if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
950
1058
  await releaseHeld();
1059
+ const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1060
+ stepIndex,
1061
+ error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
1062
+ }, now);
951
1063
  return {
952
1064
  outcome: 'failed',
953
- job: await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
954
- stepIndex,
955
- error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
956
- }, now)
1065
+ job: await refundOurFailure(firestore, orgId, failed, {
1066
+ reason: 'timeout',
1067
+ stepCredits: 0,
1068
+ now
1069
+ })
957
1070
  };
958
1071
  }
959
1072
  const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
@@ -1058,6 +1171,7 @@ async function transition(firestore, orgId, jobId, apply) {
1058
1171
  modelFor
1059
1172
  });
1060
1173
  } catch (error) {
1174
+ var _reservation_free;
1061
1175
  // The provider refused the request or the budget ended before it
1062
1176
  // answered: no usage came back, so nothing is metered and the message
1063
1177
  // goes back to the org. The provider's own words are already in the
@@ -1078,12 +1192,18 @@ async function transition(firestore, orgId, jobId, apply) {
1078
1192
  job: requeued
1079
1193
  };
1080
1194
  }
1195
+ const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1196
+ stepIndex,
1197
+ error
1198
+ }, now);
1081
1199
  return {
1082
1200
  outcome: 'failed',
1083
- job: await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1084
- stepIndex,
1085
- error
1086
- }, now)
1201
+ job: await refundOurFailure(firestore, orgId, failed, {
1202
+ reason: 'provider',
1203
+ stepCredits: 0,
1204
+ free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
1205
+ now
1206
+ })
1087
1207
  };
1088
1208
  }
1089
1209
  const latencyMs = Math.max(0, Date.now() - runStarted);
@@ -1092,6 +1212,7 @@ async function transition(firestore, orgId, jobId, apply) {
1092
1212
  // back and nothing is metered, exactly as for a request the provider
1093
1213
  // refused.
1094
1214
  if (outcome.failure && aiJobStepSpentNothing(outcome)) {
1215
+ var _reservation_free1;
1095
1216
  await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError)=>console.error('ai job release failed', {
1096
1217
  orgId,
1097
1218
  jobId,
@@ -1102,12 +1223,18 @@ async function transition(firestore, orgId, jobId, apply) {
1102
1223
  creditsSpent: 0,
1103
1224
  error: outcome.failure
1104
1225
  }, now);
1226
+ const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
1227
+ stepIndex,
1228
+ error: `step failure before the provider: ${outcome.failure}`
1229
+ }, now);
1105
1230
  return {
1106
1231
  outcome: 'failed',
1107
- job: await failAiJob(firestore, orgId, jobId, outcome.failure, {
1108
- stepIndex,
1109
- error: `step failure before the provider: ${outcome.failure}`
1110
- }, now)
1232
+ job: await refundOurFailure(firestore, orgId, failed, {
1233
+ reason: 'step-failure',
1234
+ stepCredits: 0,
1235
+ free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null,
1236
+ now
1237
+ })
1111
1238
  };
1112
1239
  }
1113
1240
  const tokens = {
@@ -1132,7 +1259,7 @@ async function transition(firestore, orgId, jobId, apply) {
1132
1259
  // Tokens were spent, so the bill is written first and its failure is a
1133
1260
  // log line, never a failed job — the same rule the chat route keeps.
1134
1261
  try {
1135
- var _job_hostId1, _reservation_free;
1262
+ var _job_hostId1, _reservation_free2;
1136
1263
  await recordAssistCost(firestore, orgId, {
1137
1264
  route: 'ai/jobs',
1138
1265
  hostId: (_job_hostId1 = job.hostId) != null ? _job_hostId1 : null,
@@ -1145,7 +1272,7 @@ async function transition(firestore, orgId, jobId, apply) {
1145
1272
  // The account this job drew on, as the reservation decided it
1146
1273
  // (AGL-2925): a Free job lands on the owner's allowance and the
1147
1274
  // platform's day; a paid one on neither.
1148
- free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
1275
+ free: (_reservation_free2 = reservation.free) != null ? _reservation_free2 : null,
1149
1276
  // The step is the creator's spend, under the job's kind (AGL-2928).
1150
1277
  uid: job.createdBy,
1151
1278
  kind: job.kind
@@ -1163,6 +1290,7 @@ async function transition(firestore, orgId, jobId, apply) {
1163
1290
  // model's answer (AGL-2938), fails the job with its own sentence. The
1164
1291
  // tokens are already on the bill above.
1165
1292
  if (outcome.refused || outcome.failure) {
1293
+ var _reservation_free3;
1166
1294
  const message = outcome.refused ? AI_JOB_REFUSED_COPY : outcome.failure;
1167
1295
  await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
1168
1296
  status: 'failed',
@@ -1170,12 +1298,19 @@ async function transition(firestore, orgId, jobId, apply) {
1170
1298
  error: message,
1171
1299
  tokens
1172
1300
  }, now);
1301
+ const failed = await failAiJob(firestore, orgId, jobId, message, {
1302
+ stepIndex,
1303
+ error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1304
+ }, now);
1305
+ // A model declining the brief is not our failure; a step's own is (AGL-3594).
1173
1306
  return {
1174
1307
  outcome: 'failed',
1175
- job: await failAiJob(firestore, orgId, jobId, message, {
1176
- stepIndex,
1177
- error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1178
- }, now)
1308
+ job: outcome.refused ? failed : await refundOurFailure(firestore, orgId, failed, {
1309
+ reason: 'step-failure',
1310
+ stepCredits: credits,
1311
+ free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null,
1312
+ now
1313
+ })
1179
1314
  };
1180
1315
  }
1181
1316
  // A step that stopped for a person parks the job in the same write that
@@ -1245,7 +1380,30 @@ async function transition(firestore, orgId, jobId, apply) {
1245
1380
  }
1246
1381
  });
1247
1382
  }
1383
+ if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
1384
+ // A guided site start confirms its own plan (AGL-3594): the resume
1385
+ // door's confirmation, by the job's creator, and the build queued.
1386
+ const resumed = await resumeAiJob(firestore, orgId, jobId, {
1387
+ uid: job.createdBy
1388
+ }, now);
1389
+ await writeAiJobAudit(firestore, {
1390
+ action: 'ai.job.resume',
1391
+ actorUid: job.createdBy,
1392
+ orgId,
1393
+ jobId,
1394
+ after: {
1395
+ status: resumed.job.status,
1396
+ confirmedPlan: true,
1397
+ autoConfirm: true
1398
+ }
1399
+ });
1400
+ return {
1401
+ outcome: 'done',
1402
+ job: resumed.job
1403
+ };
1404
+ }
1248
1405
  if (review && recorded.job.status === 'needs_review') {
1406
+ var _reservation_free4;
1249
1407
  // Nobody parked the job but its own step, so the row names no actor, as
1250
1408
  // the meter's park does.
1251
1409
  await logAiJobNeedsInput(orgId, {
@@ -1255,9 +1413,15 @@ async function transition(firestore, orgId, jobId, apply) {
1255
1413
  kind: job.kind,
1256
1414
  reason: review.reason
1257
1415
  });
1416
+ // A plan the plan rules still refused is our failure (AGL-3594).
1258
1417
  return {
1259
1418
  outcome: 'needs_review',
1260
- job: recorded.job
1419
+ job: outcome.uncredited ? await refundOurFailure(firestore, orgId, recorded.job, {
1420
+ reason: 'plan-refused',
1421
+ stepCredits: credits,
1422
+ free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
1423
+ now
1424
+ }) : recorded.job
1261
1425
  };
1262
1426
  }
1263
1427
  if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {