@aglyn/plugins-ai 1.0.0-beta.235 → 1.0.0-beta.237

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 (98) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +37 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +67 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  6. package/src/lib/components/ai-job-plan.component.js +37 -12
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-job-requests.d.ts +10 -0
  9. package/src/lib/components/ai-job-requests.js +12 -4
  10. package/src/lib/components/ai-job-requests.js.map +1 -1
  11. package/src/lib/components/ai-site-build-page.component.js +26 -7
  12. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  13. package/src/lib/components/ai-site-start-card.component.js +68 -22
  14. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  15. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  16. package/src/lib/components/assist-build-card.component.js +58 -7
  17. package/src/lib/components/assist-build-card.component.js.map +1 -1
  18. package/src/lib/components/assist-jobs-drawer.component.js +54 -6
  19. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  20. package/src/lib/components/assist-panel.component.js +2 -1
  21. package/src/lib/components/assist-panel.component.js.map +1 -1
  22. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  23. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  24. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  25. package/src/lib/jobs/ai-job-build-step.js +50 -34
  26. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-layout-language.js +1 -1
  28. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-language.js +4 -0
  30. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  31. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  32. package/src/lib/jobs/ai-job-page-step.js +34 -3
  33. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  34. package/src/lib/jobs/ai-job-plan-step.d.ts +23 -0
  35. package/src/lib/jobs/ai-job-plan-step.js +75 -15
  36. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-site-content.d.ts +34 -3
  38. package/src/lib/jobs/ai-job-site-content.js +132 -32
  39. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  40. package/src/lib/jobs/ai-job-site-step.js +45 -15
  41. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  43. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  44. package/src/lib/jobs/ai-jobs.d.ts +18 -1
  45. package/src/lib/jobs/ai-jobs.js +29 -13
  46. package/src/lib/jobs/ai-jobs.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +105 -18
  48. package/src/lib/jobs/ai-layout-stock-photos.js +356 -114
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/layout-language/ai-layout-compiler.d.ts +27 -0
  51. package/src/lib/layout-language/ai-layout-compiler.js +78 -11
  52. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  53. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  54. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-language.js +1 -1
  56. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-listings.d.ts +37 -1
  58. package/src/lib/layout-language/ai-layout-listings.js +66 -11
  59. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-pictures.d.ts +2 -1
  61. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  62. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  63. package/src/lib/model/ai-build-job.d.ts +31 -0
  64. package/src/lib/model/ai-build-job.js +86 -1
  65. package/src/lib/model/ai-build-job.js.map +1 -1
  66. package/src/lib/model/ai-build-progress.d.ts +8 -0
  67. package/src/lib/model/ai-build-progress.js +33 -0
  68. package/src/lib/model/ai-build-progress.js.map +1 -1
  69. package/src/lib/model/ai-credit-estimate.d.ts +182 -0
  70. package/src/lib/model/ai-credit-estimate.js +190 -0
  71. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  72. package/src/lib/model/ai-jobs.types.d.ts +15 -0
  73. package/src/lib/model/ai-jobs.types.js.map +1 -1
  74. package/src/lib/model/ai-site-job.d.ts +59 -22
  75. package/src/lib/model/ai-site-job.js +104 -23
  76. package/src/lib/model/ai-site-job.js.map +1 -1
  77. package/src/lib/model/ai-site-kinds.js +3 -3
  78. package/src/lib/model/ai-site-kinds.js.map +1 -1
  79. package/src/lib/runtime/ai-doctrine-validators.js +10 -1
  80. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  81. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  82. package/src/lib/runtime/ai-node-tree.js +26 -0
  83. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  84. package/src/lib/runtime/ai-palette.generated.js +539 -7
  85. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  86. package/src/lib/server/ai-jobs-resume.js +93 -35
  87. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  88. package/src/lib/server/ai-jobs-route.js +45 -4
  89. package/src/lib/server/ai-jobs-route.js.map +1 -1
  90. package/src/lib/server/assist-chat.js +4 -1
  91. package/src/lib/server/assist-chat.js.map +1 -1
  92. package/src/lib/server/assist-edit.js +3 -1
  93. package/src/lib/server/assist-edit.js.map +1 -1
  94. package/src/lib/server/assist-music.d.ts +36 -0
  95. package/src/lib/server/assist-music.js +36 -0
  96. package/src/lib/server/assist-music.js.map +1 -0
  97. package/src/lib/server.js +6 -0
  98. package/src/lib/server.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 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.
@@ -497,7 +500,10 @@ export declare function settleAiBuildJob(firestore: Firestore, orgId: string, jo
497
500
  * `changed: false` for a job that is not a finished build, or has nothing to
498
501
  * try again.
499
502
  */
500
- export declare function retryAiBuildJob(firestore: Firestore, orgId: string, jobId: string, now?: Date): Promise<{
503
+ export declare function retryAiBuildJob(firestore: Firestore, orgId: string, jobId: string, now?: Date, options?: {
504
+ /** A Free Try again the person chose to run past what was left (AGL-3722). */
505
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
506
+ }): Promise<{
501
507
  job: AiJob;
502
508
  changed: boolean;
503
509
  retried: string[];
@@ -523,6 +529,17 @@ export declare function resumeAiJob(firestore: Firestore, orgId: string, jobId:
523
529
  * pass put its pages live.
524
530
  */
525
531
  publishConfirmed?: boolean;
532
+ /**
533
+ * A Free build or start the person chose to build past what was left
534
+ * (AGL-3722), kept on the job: who, when, what was left.
535
+ */
536
+ creditsConfirmed?: AiJobCreditsConfirmation | null;
537
+ /**
538
+ * The smaller first build the person chose instead (AGL-3722): its plan
539
+ * — the home page and what it needs — is the one confirmed, in place of
540
+ * the plan the job proposed. Only while that plan waits for confirmation.
541
+ */
542
+ reducePlan?: (plan: AiJobPlan) => AiJobPlan | null;
526
543
  }): Promise<{
527
544
  job: AiJob;
528
545
  changed: boolean;
@@ -383,7 +383,7 @@ function jobFrom(snapshot) {
383
383
  } : {}));
384
384
  // A job is a resource the console lists and routes to, named as one.
385
385
  const ref = jobsCollection(firestore, input.orgId).doc(createResourceUid());
386
- const data = {
386
+ const data = _extends({
387
387
  orgId: input.orgId,
388
388
  hostId: (_input_hostId = input.hostId) != null ? _input_hostId : null,
389
389
  kind: input.kind,
@@ -403,7 +403,9 @@ function jobFrom(snapshot) {
403
403
  expiresAt: assistExchangeExpiry(now),
404
404
  error: null,
405
405
  lease: null
406
- };
406
+ }, input.creditsConfirmed ? {
407
+ creditsConfirmed: input.creditsConfirmed
408
+ } : {});
407
409
  await ref.set(data);
408
410
  await logAiJobCreated(input.orgId, {
409
411
  uid: input.createdBy,
@@ -970,6 +972,7 @@ async function transition(firestore, orgId, jobId, apply) {
970
972
  * ended meanwhile — a person's cancel, which pays for what ran — is left as it
971
973
  * ended and given nothing back.
972
974
  */ async function failOurFailure(firestore, orgId, jobId, message, detail, refund, now) {
975
+ var _detail_ours;
973
976
  var _current_items;
974
977
  const current = await getAiJob(firestore, orgId, jobId);
975
978
  // A build that got as far as its items settles item by item (AGL-3616):
@@ -996,8 +999,10 @@ async function transition(firestore, orgId, jobId, apply) {
996
999
  now
997
1000
  }));
998
1001
  }
1002
+ // Given back as ours either way; ANNOUNCED as ours unless the step said the
1003
+ // failure is the customer's own limit (`customerLimit`), which pages no one.
999
1004
  return failAiJob(firestore, orgId, jobId, message, _extends({}, detail, {
1000
- ours: true
1005
+ ours: (_detail_ours = detail.ours) != null ? _detail_ours : true
1001
1006
  }), now);
1002
1007
  }
1003
1008
  // ── A build's settlement (AGL-3616) ──────────────────────────────────────
@@ -1222,7 +1227,7 @@ async function transition(firestore, orgId, jobId, apply) {
1222
1227
  * re-asks the plan. Held at what the retried items are estimated to cost.
1223
1228
  * `changed: false` for a job that is not a finished build, or has nothing to
1224
1229
  * try again.
1225
- */ export async function retryAiBuildJob(firestore, orgId, jobId, now = new Date()) {
1230
+ */ export async function retryAiBuildJob(firestore, orgId, jobId, now = new Date(), options = {}) {
1226
1231
  let retried = [];
1227
1232
  const result = await transition(firestore, orgId, jobId, (current)=>{
1228
1233
  var _current_plan, _current_items;
@@ -1232,7 +1237,7 @@ async function transition(firestore, orgId, jobId, apply) {
1232
1237
  const next = aiBuildRetryLedger(current.items, current.kind === 'site' ? aiLedgerUnits(current.items) : aiBuildUnits(plan));
1233
1238
  if (!next.ledger.some((row)=>row.status === 'pending')) return null;
1234
1239
  retried = next.retried;
1235
- return {
1240
+ return _extends({
1236
1241
  status: 'queued',
1237
1242
  items: next.ledger,
1238
1243
  steps: current.steps.map((step)=>step.name === AI_JOB_PLAN_STEP ? step : _extends({}, step, {
@@ -1247,9 +1252,12 @@ async function transition(firestore, orgId, jobId, apply) {
1247
1252
  lease: null,
1248
1253
  creditsReserved: Math.max(AI_JOB_STEP_RESERVE_CREDITS, current.kind === 'site' ? next.retried.length * AI_JOB_STEP_RESERVE_CREDITS : aiBuildCreditEstimate(plan, {
1249
1254
  slots: new Set(next.retried)
1250
- })),
1255
+ }))
1256
+ }, options.creditsConfirmed ? {
1257
+ creditsConfirmed: options.creditsConfirmed
1258
+ } : {}, {
1251
1259
  updatedAt: now
1252
- };
1260
+ });
1253
1261
  });
1254
1262
  return _extends({}, result, {
1255
1263
  retried: result.changed ? retried : []
@@ -1268,6 +1276,7 @@ async function transition(firestore, orgId, jobId, apply) {
1268
1276
  * (AGL-3031), creations included, where that is more.
1269
1277
  */ export async function resumeAiJob(firestore, orgId, jobId, actor, now = new Date(), options = {}) {
1270
1278
  return transition(firestore, orgId, jobId, (current)=>{
1279
+ var _options_reducePlan;
1271
1280
  var _current_review, _current_inputs;
1272
1281
  // A job the meter paused (AGL-3660) carries on from the step it paused
1273
1282
  // on: that step is already pending, so it is queued again with nothing
@@ -1287,7 +1296,8 @@ async function transition(firestore, orgId, jobId, apply) {
1287
1296
  }) : step);
1288
1297
  const outstanding = steps.filter((step)=>step.status === 'pending').length;
1289
1298
  const pending = outstanding > 0;
1290
- const confirming = ((_current_review = current.review) == null ? void 0 : _current_review.reason) === 'plan' && current.plan ? current.plan : null;
1299
+ const proposed = ((_current_review = current.review) == null ? void 0 : _current_review.reason) === 'plan' && current.plan ? current.plan : null;
1300
+ const confirming = proposed && options.reducePlan ? (_options_reducePlan = options.reducePlan(proposed)) != null ? _options_reducePlan : proposed : proposed;
1291
1301
  const confirmed = confirming ? {
1292
1302
  plan: _extends({}, confirming, {
1293
1303
  status: 'confirmed',
@@ -1305,7 +1315,9 @@ async function transition(firestore, orgId, jobId, apply) {
1305
1315
  steps,
1306
1316
  review: null,
1307
1317
  error: null
1308
- }, confirmed, publish, {
1318
+ }, confirmed, publish, options.creditsConfirmed ? {
1319
+ creditsConfirmed: options.creditsConfirmed
1320
+ } : {}, {
1309
1321
  creditsReserved: pending ? Math.max(outstanding * AI_JOB_STEP_RESERVE_CREDITS, confirming ? aiJobCreditEstimate(current.kind, confirming) : 0) : 0,
1310
1322
  updatedAt: now
1311
1323
  });
@@ -1632,10 +1644,12 @@ async function transition(firestore, orgId, jobId, apply) {
1632
1644
  }, now);
1633
1645
  return {
1634
1646
  outcome: 'failed',
1635
- job: await failOurFailure(firestore, orgId, jobId, outcome.failure, {
1647
+ job: await failOurFailure(firestore, orgId, jobId, outcome.failure, _extends({
1636
1648
  stepIndex,
1637
1649
  error: `step failure before the provider: ${outcome.failure}`
1638
- }, {
1650
+ }, outcome.customerLimit ? {
1651
+ ours: false
1652
+ } : {}), {
1639
1653
  reason: 'step-failure',
1640
1654
  stepCredits: 0,
1641
1655
  free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null
@@ -1703,10 +1717,12 @@ async function transition(firestore, orgId, jobId, apply) {
1703
1717
  error: message,
1704
1718
  tokens
1705
1719
  }, now);
1706
- const detail = {
1720
+ const detail = _extends({
1707
1721
  stepIndex,
1708
1722
  error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1709
- };
1723
+ }, outcome.customerLimit ? {
1724
+ ours: false
1725
+ } : {});
1710
1726
  // A model declining the brief is not our failure; a step's own is (AGL-3594).
1711
1727
  return {
1712
1728
  outcome: 'failed',