@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.252
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.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.js +15 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +1 -1
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +20 -2
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +11 -0
- package/src/lib/components/ai-jobs-store.js +22 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.js +7 -4
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
- package/src/lib/jobs/ai-job-drafts.js +46 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +6 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
- package/src/lib/jobs/ai-job-form-reuse.js +136 -0
- package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-form-step.js +63 -11
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +6 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-plan-step.js +39 -1
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +5 -3
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
- package/src/lib/jobs/ai-job-site-content.js +7 -5
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.js +6 -3
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
- package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
- package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
- package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
- package/src/lib/model/ai-build-plan.d.ts +7 -3
- package/src/lib/model/ai-build-plan.js +11 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +32 -18
- package/src/lib/model/ai-credit-estimate.js +36 -18
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +30 -0
- package/src/lib/model/ai-job-activity.js +35 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +5 -3
- package/src/lib/model/ai-media-credits.js +7 -5
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-site-job.js +6 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +5 -0
- package/src/lib/model/ai-site-store-pages.js +2 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/plugin-config.js +2 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
- package/src/lib/providers/catalog.d.ts +24 -17
- package/src/lib/providers/catalog.js +63 -30
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.d.ts +1 -1
- package/src/lib/providers/model-choice.js +39 -30
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.d.ts +20 -1
- package/src/lib/providers/routing.js +47 -8
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +37 -1
- package/src/lib/runtime/ai-dev-env.js +59 -6
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +8 -0
- package/src/lib/runtime/ai-eval-live.js +34 -8
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +4 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +17 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/seo-fields.js +4 -3
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-assist.js +3 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/ai-media-svg.js +4 -0
- package/src/lib/server/ai-media-svg.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +2 -5
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n page: { median: 30, p90: 40 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n theme: { median: 4, p90: 8 },\n plan: { median: 21, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 29, p90: 49 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 4, p90: 4 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** When the table above was read, and from when its jobs ran. */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/** What a whole Free two-page site job measured at, in credits, in the same window. */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 148, high: 160 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 40 (p90 78), a 5-section 46 (p90 90).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 16, p90: 30 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-credit-estimate.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\n/**\n * WHAT A JOB REALLY COSTS, NEXT TO WHAT IT CAN COST (AGL-3722).\n *\n * Every job's cost used to be quoted at its CEILING: each pass at the\n * `AI_SITE_PASS_CREDITS` (50) a pass is reserved against. A Free Assist\n * build of two pages, a quote form and a layout was quoted 600 credits and\n * refused with 164 left, when builds like it measure about a fifth of that.\n *\n * So a job is now quoted as a RANGE: what it is likely to cost (the sum of\n * the measured medians of what it builds), what it costs nine times in ten\n * (the measured p90s), and its ceiling (the old figure, unchanged). The\n * meter still charges actual use, and the reservation each pass takes is\n * still the ceiling's 50 — only the words and the Free admission move:\n * a Free job is admitted outright when its p90 fits what is left, and asked\n * to be confirmed when it does not.\n *\n * This module is pure: the console, the doors and the specs read one table.\n */\n\n/** One measured cost: the median and the 90th percentile, in credits. */\nexport interface AiMeasuredCredits {\n median: number\n p90: number\n}\n\n/**\n * MEASURED IN PRODUCTION, credits per unit, over every `done` AI job since\n * 2026-10-01 (read 2026-10-09; a whole Free site job came to 148–160):\n *\n * page item 30 / 40 (a page of about `AI_MEASURED_PAGE_SECTIONS` sections, its listing included)\n * form 29 / 31\n * layout 23 / 24 (header and footer, one answer)\n * theme 4 / 8 (a site start's look)\n * plan step 21 / 70\n *\n * THE FAST TIER'S PART, REPRICED (AGL-3781). A site start's look, a Free\n * start's plan and every page's listing run on the fast tier, measured above\n * on Claude Haiku 4.5 at its cost. They run on Claude Haiku 5.5 at the\n * platform's markup now, so those parts are repriced from what production\n * sent, at the new rates and about 30% more tokens: over October 1–14 a look\n * 4 / 7 → 1 / 2, a Free start's plan 15 / 22 → 3 / 5, a listing 3 / 5 → 1 / 1\n * (2 less on every page), and a whole two-page Free site 148 → 121 at the\n * median. The other rows ran on the balanced tier and do not move.\n *\n * HOW TO RE-MEASURE: a read-only collection-group read of `aiJobs` in\n * production where `status == 'done'` and `createdAt` is in the window; group\n * each ledger row's (`items[]`) `creditsSpent` by its `op` (the site start's\n * look is the `theme` row), take the plan step's `steps[].creditsSpent` for\n * `plan`, and each job's `creditsSpent` for the whole-site band. Put each\n * group's median and 90th percentile here, and the dates in\n * `AI_MEASURED_AT`. `ai-credit-estimate.spec.ts` holds the table, the page\n * scaling and the measured whole-site band together, so a re-measure that\n * moves one moves the spec with it.\n */\nexport const AI_MEASURED_UNIT_CREDITS = {\n // 30 / 40 with its listing at Haiku 4.5's cost.\n page: { median: 28, p90: 38 },\n form: { median: 29, p90: 31 },\n layout: { median: 23, p90: 24 },\n // 4 / 8 on Haiku 4.5.\n theme: { median: 1, p90: 2 },\n // 21 / 70: the median is a Free start's plan, on the fast tier; the p90 a\n // plan on the balanced tier, which does not move.\n plan: { median: 4, p90: 70 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * MEASURED IN PRODUCTION, credits per unit of a PAID site start, whose plan\n * and parts run on the paid routing (Sonnet) rather than a Free start's\n * (Haiku) the table above is mostly made of: every `done` paid site job\n * since 2026-10-01, read 2026-10-12 (6 jobs; products from the 4 stores; AGL-3739).\n *\n * plan step 81 / 86 (a Free start's plan: 20 / 42 on Haiku 4.5, 3 / 5 repriced)\n * page item 29 / 49 (about `AI_MEASURED_PAGE_SECTIONS` sections; 27 / 47 with its listing repriced)\n * form 32 / 39\n * layout 29 / 29\n * theme 4 / 4 (1 / 1 repriced)\n * email 49 / 51\n * products 64 / 66 (a store's first products, one answer)\n *\n * A 5-page store with its welcome email was quoted about 277 off the table\n * above: its plan at a Free start's 21 where paid plans run about 81, and its\n * first products not counted at all where they run about 64. Those stores\n * came to 413-448; this table quotes one at about 404.\n *\n * Re-measure as the table above says, keeping only jobs whose plan step's\n * `tokens.model` is the paid routing's.\n */\nexport const AI_MEASURED_PAID_SITE_CREDITS = {\n plan: { median: 81, p90: 86 },\n page: { median: 27, p90: 47 },\n form: { median: 32, p90: 39 },\n layout: { median: 29, p90: 29 },\n theme: { median: 1, p90: 1 },\n email: { median: 49, p90: 51 },\n products: { median: 64, p90: 66 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** When the table above was read, and from when its jobs ran. */\nexport const AI_MEASURED_AT = { read: '2026-10-09', since: '2026-10-01' } as const\n\n/**\n * What a whole Free two-page site job measured at, in credits, in the same\n * window: 148–160 with its plan, look and listings at Haiku 4.5's cost, 27\n * less at the median with them repriced (AGL-3781).\n */\nexport const AI_MEASURED_FREE_SITE_CREDITS = { low: 121, high: 133 } as const\n\n/** The sections the measured page item is about: a page is scaled around it. */\nexport const AI_MEASURED_PAGE_SECTIONS = 5\n\n/**\n * What a page of this many sections is likely to cost: the measured page\n * scaled by its sections around the ~5-section median — about 6 credits a\n * section, 8 at the p90.\n */\nexport function aiMeasuredPageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { median, p90 } = AI_MEASURED_UNIT_CREDITS.page\n return {\n median: Math.ceil((median * count) / AI_MEASURED_PAGE_SECTIONS),\n p90: Math.ceil((p90 * count) / AI_MEASURED_PAGE_SECTIONS),\n }\n}\n\n/**\n * What a page costs when it is written ON ITS OWN — a page an Assist build or\n * a page job writes, rather than one of a site start's pages (AGL-3722).\n *\n * The table's page item is a site start's: its pages are written one after\n * another inside one job, the later ones reading the prompt the first one\n * paid to write, so a 2-section second page measured 19–28. A page written on\n * its own pays that prompt itself and reads the site it lands on, and costs\n * what a site start's FIRST page costs: a 5-section home page measured 39–78\n * (median 40, p90 78), standalone page jobs' writing step 82–105 for a\n * 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting\n * a build's pages at the site start's scaled median told a Free workspace\n * \"about 48 credits\" for two 4-section pages (AGL-3722, 2026-10-10).\n *\n * So it is priced as a fixed cost — the prompt, the site, the listing — and a\n * cost per section: a 4-section page about 38 (p90 76), a 5-section 44 (p90 88).\n * The fixed part was 16 / 30 with its listing at Haiku 4.5's cost (AGL-3781).\n */\nexport const AI_MEASURED_STANDALONE_PAGE = {\n fixed: { median: 14, p90: 28 },\n perSection: { median: 6, p90: 12 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/** What a page of this many sections written on its own is likely to cost. */\nexport function aiMeasuredStandalonePageCredits(sections: number): AiMeasuredCredits {\n const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS))\n const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE\n return {\n median: fixed.median + perSection.median * count,\n p90: fixed.p90 + perSection.p90 * count,\n }\n}\n\n/**\n * What a creation costs when it is written ON ITS OWN (AGL-3722), where it\n * measured apart from a site start's: an Assist build's layout came to 24–28\n * (2026-10-09/10), against a site start's 23/24 written in its warm prompt.\n * A form measured the same either way (25–30 on its own, 29/31 in a site\n * start), so it keeps the table's figure.\n */\nexport const AI_MEASURED_STANDALONE_CREATION_CREDITS = {\n layout: { median: 26, p90: 30 },\n} as const satisfies Record<string, AiMeasuredCredits>\n\n/**\n * What one pass of a kind with no measurement of its own is likely to cost\n * (a component, an email design, a template, an item another runner\n * builds), written on its own: a component job's writing step measured\n * 52–65, a form job's 37, a layout job's 63, and the Assist build of\n * 2026-10-09 a welcome email at 46 and a products step at 60. Until\n * 2026-10-10 this was a site start's form (29/31), which is written inside a\n * site job's warm prompt and is the cheapest single answer there is.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = { median: 50, p90: 65 }\n\n/** A job's cost as it is quoted: likely, nine times in ten, and at its worst. */\nexport interface AiCreditRange {\n /** The sum of the measured medians: what the job is likely to spend. */\n likely: number\n /** The sum of the measured p90s: what a Free job is admitted on. */\n p90: number\n /** Every pass at its reserve: what it can spend at most. */\n ceiling: number\n}\n\nexport const AI_CREDIT_RANGE_ZERO: AiCreditRange = { likely: 0, p90: 0, ceiling: 0 }\n\n/** Two ranges added. */\nexport function aiCreditRangeAdd(a: AiCreditRange, b: AiCreditRange): AiCreditRange {\n return { likely: a.likely + b.likely, p90: a.p90 + b.p90, ceiling: a.ceiling + b.ceiling }\n}\n\n/** A measured cost as a range with its own ceiling, the likely and p90 never above it. */\nexport function aiCreditRangeOf(measured: AiMeasuredCredits, ceiling: number): AiCreditRange {\n const top = Math.max(0, Math.ceil(ceiling))\n return { likely: Math.min(measured.median, top), p90: Math.min(measured.p90, top), ceiling: top }\n}\n\n/** A range whose likely and p90 are each at least the last, and at most the ceiling. */\nexport function aiCreditRangeOrdered(range: AiCreditRange): AiCreditRange {\n const ceiling = Math.max(0, Math.ceil(range.ceiling))\n const likely = Math.min(Math.max(0, Math.ceil(range.likely)), ceiling)\n const p90 = Math.min(Math.max(likely, Math.ceil(range.p90)), ceiling)\n return { likely, p90, ceiling }\n}\n\nconst n = (value: number) => value.toLocaleString('en-US')\n\n/**\n * The range as every surface quotes it: \"About 106 credits (up to 650)\".\n * A job whose likely figure is its ceiling says one number.\n */\nexport function aiCreditRangeText(range: AiCreditRange): string {\n if (range.likely >= range.ceiling) return `About ${n(range.ceiling)} credits`\n return `About ${n(range.likely)} credits (up to ${n(range.ceiling)})`\n}\n\n/**\n * Whether a Free job of this range needs the person's go-ahead before it\n * starts (AGL-3722): its p90 is more than what is left. A job whose p90\n * fits starts with no prompt; one that does not starts only when confirmed,\n * builds as much as what is left pays for, and pauses with Resume.\n */\nexport function aiCreditsNeedConfirm(range: Pick<AiCreditRange, 'p90'>, left: number): boolean {\n return Number.isFinite(left) && range.p90 > Math.max(0, Math.floor(left))\n}\n\n/** The smaller first build a Free job that does not fit is offered (AGL-3722). */\nexport interface AiCreditsSmaller {\n /** \"Build the home page first\". */\n label: string\n likely: number\n p90: number\n ceiling: number\n}\n\n/**\n * What a Free job that does not fit what is left says before it starts, and\n * the choices it offers: build what fits, a smaller first build, or Upgrade.\n * The door that refuses an unconfirmed start answers with it (status 409,\n * `code: AI_CREDITS_CONFIRM_CODE`), and the plan card and the guided start\n * draw it from the same figures.\n */\nexport interface AiCreditsPrompt extends AiCreditRange {\n left: number\n resetsOn: string\n smaller: AiCreditsSmaller | null\n}\n\n/** The `code` a door answers an unconfirmed Free start with. */\nexport const AI_CREDITS_CONFIRM_CODE = 'credits-confirm'\n\n/** \"Build what fits\": the go-ahead for a Free job past what is left. */\nexport const AI_CREDITS_BUILD_WHAT_FITS = 'Build what fits'\n\n/** The smaller first build's button: \"Build the home page first (about 64 credits)\". */\nexport function aiCreditsSmallerText(smaller: Pick<AiCreditsSmaller, 'label' | 'likely'>): string {\n return `${smaller.label} (about ${n(smaller.likely)} credits)`\n}\n\n/** Which way on a prompt offers, in the order it offers them. */\nexport type AiCreditsChoiceKey = 'fits' | 'smaller' | 'upgrade'\n\n/** One way on, as its option button reads it: a short label and the figure under it (AGL-3722). */\nexport interface AiCreditsChoice {\n key: AiCreditsChoiceKey\n label: string\n detail: string\n /** The one choice drawn as the primary button. */\n recommended: boolean\n}\n\nconst credit = (count: number) => `${n(count)} ${count === 1 ? 'credit' : 'credits'}`\n\n/**\n * The prompt's ways on, as its three option buttons read them (Zach,\n * 2026-10-10: \"more appealing and consistent\"): build what fits, the smaller\n * first build when there is one, and Upgrade when it can be offered — each a\n * short sentence-case label with its figure under it. The smaller build is\n * the recommended one when its p90 fits what is left, since it then finishes;\n * otherwise building what fits is.\n */\nexport function aiCreditsChoices(\n prompt: Pick<AiCreditsPrompt, 'left' | 'smaller'>,\n offers: { smaller: boolean; upgrade: boolean },\n): AiCreditsChoice[] {\n const left = Math.max(0, Math.floor(prompt.left))\n const smaller = offers.smaller ? prompt.smaller : null\n const smallerFits = smaller !== null && smaller.p90 <= left\n const choices: AiCreditsChoice[] = [\n {\n key: 'fits',\n label: AI_CREDITS_BUILD_WHAT_FITS,\n detail: `Uses your ${credit(left)} left, then pauses`,\n recommended: !smallerFits,\n },\n ]\n if (smaller) {\n choices.push({\n key: 'smaller',\n label: smaller.label,\n detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,\n recommended: smallerFits,\n })\n }\n if (offers.upgrade) choices.push({ key: 'upgrade', label: 'Upgrade', detail: 'Get more credits', recommended: false })\n return choices\n}\n\n/** A reset day as a person reads it: \"November 1\". */\nfunction resetLabel(resetsOn: string): string {\n const at = new Date(`${resetsOn}T00:00:00.000Z`)\n return Number.isFinite(at.getTime())\n ? at.toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' })\n : 'the first of next month'\n}\n\n/**\n * The plain sentence (Zach, 2026-10-09): \"This build is about N credits (up\n * to M). You have L left, so it will build as much as it can and pause when\n * your credits run out. You can upgrade or resume when they renew.\"\n */\nexport function aiCreditsPromptText(prompt: AiCreditsPrompt, noun: 'build' | 'site' = 'build'): string {\n const size =\n prompt.likely >= prompt.ceiling\n ? `about ${n(prompt.ceiling)} credits`\n : `about ${n(prompt.likely)} credits (up to ${n(prompt.ceiling)})`\n const left = Math.max(0, Math.floor(prompt.left))\n return (\n `This ${noun} is ${size}. You have ${n(left)} left, so it will build as much as it can and pause ` +\n `when your credits run out. You can upgrade or resume when they renew on ${resetLabel(prompt.resetsOn)}.`\n )\n}\n\n/**\n * The record a confirmed Free start keeps (AGL-3722): who said go, when,\n * what was left then and the range they were shown.\n */\nexport interface AiJobCreditsConfirmation extends AiCreditRange {\n by: string\n at: Date | string\n left: number\n}\n\n/** The confirmation a door keeps on the job for the prompt the person confirmed, or `null` when there was none. */\nexport function aiCreditsConfirmation(\n prompt: AiCreditsPrompt | null,\n by: string,\n at: Date,\n): AiJobCreditsConfirmation | null {\n if (!prompt) return null\n return { by, at, left: prompt.left, likely: prompt.likely, p90: prompt.p90, ceiling: prompt.ceiling }\n}\n\n/** The prompt a range needs against what is left, or `null` when it fits. */\nexport function aiCreditsPromptFor(\n range: AiCreditRange,\n credits: { left: number; resetsOn: string } | null | undefined,\n smaller: AiCreditsSmaller | null = null,\n): AiCreditsPrompt | null {\n if (!credits || !aiCreditsNeedConfirm(range, credits.left)) return null\n return { ...range, left: Math.max(0, Math.floor(credits.left)), resetsOn: credits.resetsOn, smaller }\n}\n"],"names":["AI_MEASURED_UNIT_CREDITS","page","median","p90","form","layout","theme","plan","AI_MEASURED_PAID_SITE_CREDITS","email","products","AI_MEASURED_AT","read","since","AI_MEASURED_FREE_SITE_CREDITS","low","high","AI_MEASURED_PAGE_SECTIONS","aiMeasuredPageCredits","sections","count","Math","max","floor","Number","isFinite","ceil","AI_MEASURED_STANDALONE_PAGE","fixed","perSection","aiMeasuredStandalonePageCredits","AI_MEASURED_STANDALONE_CREATION_CREDITS","AI_MEASURED_PASS_CREDITS","AI_CREDIT_RANGE_ZERO","likely","ceiling","aiCreditRangeAdd","a","b","aiCreditRangeOf","measured","top","min","aiCreditRangeOrdered","range","n","value","toLocaleString","aiCreditRangeText","aiCreditsNeedConfirm","left","AI_CREDITS_CONFIRM_CODE","AI_CREDITS_BUILD_WHAT_FITS","aiCreditsSmallerText","smaller","label","credit","aiCreditsChoices","prompt","offers","smallerFits","choices","key","detail","recommended","push","upgrade","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","noun","size","aiCreditsConfirmation","by","aiCreditsPromptFor","credits"],"mappings":"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"}
|
|
@@ -132,3 +132,33 @@ export declare function aiJobsHeaderChips(counts: AiJobActivityCounts): Array<{
|
|
|
132
132
|
}>;
|
|
133
133
|
/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */
|
|
134
134
|
export declare function aiJobKindNoun(kind: AiJobKind): string;
|
|
135
|
+
/**
|
|
136
|
+
* The job kinds that build a site's pages (AGL-3765): a page, a guided start
|
|
137
|
+
* and a build from the Assist chat. Two at once on one site each plan against
|
|
138
|
+
* the site as it was, and race for what its plan holds one of.
|
|
139
|
+
*/
|
|
140
|
+
export declare const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[];
|
|
141
|
+
/**
|
|
142
|
+
* How long a plan waiting for its confirmation still counts as a build on its
|
|
143
|
+
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
144
|
+
* days ago.
|
|
145
|
+
*/
|
|
146
|
+
export declare const AI_SITE_BUILD_PLAN_FRESH_MS: number;
|
|
147
|
+
/** What a door that starts a page says while another is being built on the site (AGL-3765). */
|
|
148
|
+
export declare const AI_SITE_BUILD_IN_FLIGHT_COPY = "A page is already being built. Wait for it, or start another anyway.";
|
|
149
|
+
/**
|
|
150
|
+
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
151
|
+
* building, or a plan that waits for its confirmation and moved in the last
|
|
152
|
+
* {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns
|
|
153
|
+
* with it and still starts the job: two builds can share a site, they just
|
|
154
|
+
* cannot both make what its plan holds only one of.
|
|
155
|
+
*/
|
|
156
|
+
export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
157
|
+
id: string;
|
|
158
|
+
kind: AiJobKind | string;
|
|
159
|
+
hostId?: string | null;
|
|
160
|
+
}>(jobs: readonly T[], input: {
|
|
161
|
+
hostId: string | null | undefined;
|
|
162
|
+
exceptJobId?: string | null;
|
|
163
|
+
now?: number;
|
|
164
|
+
}): T | null;
|
|
@@ -169,5 +169,40 @@ export function aiJobsActivity(jobs) {
|
|
|
169
169
|
var _KIND_NOUNS_kind;
|
|
170
170
|
return (_KIND_NOUNS_kind = KIND_NOUNS[kind]) != null ? _KIND_NOUNS_kind : 'AI job';
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The job kinds that build a site's pages (AGL-3765): a page, a guided start
|
|
174
|
+
* and a build from the Assist chat. Two at once on one site each plan against
|
|
175
|
+
* the site as it was, and race for what its plan holds one of.
|
|
176
|
+
*/ export const AI_PAGE_BUILDING_JOB_KINDS = [
|
|
177
|
+
'page',
|
|
178
|
+
'site',
|
|
179
|
+
'build'
|
|
180
|
+
];
|
|
181
|
+
/**
|
|
182
|
+
* How long a plan waiting for its confirmation still counts as a build on its
|
|
183
|
+
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
184
|
+
* days ago.
|
|
185
|
+
*/ export const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60000;
|
|
186
|
+
/** What a door that starts a page says while another is being built on the site (AGL-3765). */ export const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.';
|
|
187
|
+
/**
|
|
188
|
+
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
189
|
+
* building, or a plan that waits for its confirmation and moved in the last
|
|
190
|
+
* {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns
|
|
191
|
+
* with it and still starts the job: two builds can share a site, they just
|
|
192
|
+
* cannot both make what its plan holds only one of.
|
|
193
|
+
*/ export function aiSiteBuildInFlight(jobs, input) {
|
|
194
|
+
var _input_now, _jobs_find;
|
|
195
|
+
if (!input.hostId) return null;
|
|
196
|
+
const now = (_input_now = input.now) != null ? _input_now : Date.now();
|
|
197
|
+
return (_jobs_find = jobs.find((job)=>{
|
|
198
|
+
if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false;
|
|
199
|
+
if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind)) return false;
|
|
200
|
+
const phase = aiJobPhase(job);
|
|
201
|
+
if (phase === 'queued' || phase === 'planning' || phase === 'building') return true;
|
|
202
|
+
if (phase !== 'plan-ready') return false;
|
|
203
|
+
const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN;
|
|
204
|
+
return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS;
|
|
205
|
+
})) != null ? _jobs_find : null;
|
|
206
|
+
}
|
|
172
207
|
|
|
173
208
|
//# sourceMappingURL=ai-job-activity.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","aiJobKindNoun","kind"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n\n/**\n * The job kinds that build a site's pages (AGL-3765): a page, a guided start\n * and a build from the Assist chat. Two at once on one site each plan against\n * the site as it was, and race for what its plan holds one of.\n */\nexport const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[] = ['page', 'site', 'build']\n\n/**\n * How long a plan waiting for its confirmation still counts as a build on its\n * way (AGL-3765): one the person is reading now, not one left unconfirmed\n * days ago.\n */\nexport const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60_000\n\n/** What a door that starts a page says while another is being built on the site (AGL-3765). */\nexport const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.'\n\n/**\n * Another job building pages on this site (AGL-3765): queued, planning or\n * building, or a plan that waits for its confirmation and moved in the last\n * {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns\n * with it and still starts the job: two builds can share a site, they just\n * cannot both make what its plan holds only one of.\n */\nexport function aiSiteBuildInFlight<\n T extends AiJobPhaseSource & { id: string; kind: AiJobKind | string; hostId?: string | null },\n>(\n jobs: readonly T[],\n input: { hostId: string | null | undefined; exceptJobId?: string | null; now?: number },\n): T | null {\n if (!input.hostId) return null\n const now = input.now ?? Date.now()\n return (\n jobs.find((job) => {\n if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false\n if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind as AiJobKind)) return false\n const phase = aiJobPhase(job)\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return true\n if (phase !== 'plan-ready') return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS\n }) ?? null\n )\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","aiJobKindNoun","kind","AI_PAGE_BUILDING_JOB_KINDS","AI_SITE_BUILD_PLAN_FRESH_MS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiSiteBuildInFlight","input","hostId","id","exceptJobId","includes"],"mappings":"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"}
|
|
@@ -38,9 +38,11 @@ export declare const AI_MEDIA_PHOTO_NOMINAL_USAGE: AiUsage & {
|
|
|
38
38
|
images: number;
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
|
-
* An illustration's nominal tokens
|
|
42
|
-
*
|
|
43
|
-
* the description, and a few
|
|
41
|
+
* An illustration's nominal tokens, as the fast tier's Claude Haiku 5.5 counts
|
|
42
|
+
* them (about 30% more than Claude Haiku 4.5 did for the same text, AGL-3781):
|
|
43
|
+
* about 2,700 of instructions, the tool and the description, and a few
|
|
44
|
+
* thousand of SVG. The instructions cache on that model, but an illustration
|
|
45
|
+
* asked once writes them, so they are counted here at the full input rate.
|
|
44
46
|
*/
|
|
45
47
|
export declare const AI_MEDIA_SVG_NOMINAL_USAGE: AiUsage;
|
|
46
48
|
/**
|
|
@@ -42,12 +42,14 @@ import { assistCreditsFromUsd } from "../usage/assist-credits.js";
|
|
|
42
42
|
images: 1
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
|
-
* An illustration's nominal tokens
|
|
46
|
-
*
|
|
47
|
-
* the description, and a few
|
|
45
|
+
* An illustration's nominal tokens, as the fast tier's Claude Haiku 5.5 counts
|
|
46
|
+
* them (about 30% more than Claude Haiku 4.5 did for the same text, AGL-3781):
|
|
47
|
+
* about 2,700 of instructions, the tool and the description, and a few
|
|
48
|
+
* thousand of SVG. The instructions cache on that model, but an illustration
|
|
49
|
+
* asked once writes them, so they are counted here at the full input rate.
|
|
48
50
|
*/ export const AI_MEDIA_SVG_NOMINAL_USAGE = {
|
|
49
|
-
inputTokens:
|
|
50
|
-
outputTokens:
|
|
51
|
+
inputTokens: 3250,
|
|
52
|
+
outputTokens: 3900,
|
|
51
53
|
cacheReadTokens: 0,
|
|
52
54
|
cacheWriteTokens: 0
|
|
53
55
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-media-credits.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 { AiUsage } from '../providers/contract'\nimport {\n AI_IMAGE_DEFAULT_MODEL,\n AI_MODEL_CATALOG,\n estimateAiBilledUsd,\n} from '../providers/catalog'\nimport type { AiImageMode, AiImageSize } from '../providers/image-contract'\nimport { assistCreditsFromUsd } from '../usage/assist-credits'\n\n/**\n * What one picture in Media is expected to draw (AGL-3602), in credits: the\n * estimate the dialog shows before anything is spent, and the figure the\n * door holds a walled band to. Browser-safe, so both read the same number.\n *\n * Each is an ESTIMATE at nominal usage, priced at the billed rates:\n *\n * - a photo is one picture at its per-picture rate for the size the plan\n * makes (512 px on Free, 1K on a paid plan), plus a prompt and the\n * thinking Google's model does before it draws;\n * - an illustration is the text provider's answer — the instructions, the\n * description, and a few thousand tokens of SVG.\n *\n * What is actually charged is what was spent, measured: a picture held back\n * or not stored is never charged, and an illustration that needed its second\n * attempt costs about twice its estimate.\n */\n\n/** A photo's nominal tokens beside its picture. */\nexport const AI_MEDIA_PHOTO_NOMINAL_USAGE: AiUsage & { images: number } = {\n inputTokens: 600,\n outputTokens: 1_500,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n images: 1,\n}\n\n/**\n * An illustration's nominal tokens
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-media-credits.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 { AiUsage } from '../providers/contract'\nimport {\n AI_IMAGE_DEFAULT_MODEL,\n AI_MODEL_CATALOG,\n estimateAiBilledUsd,\n} from '../providers/catalog'\nimport type { AiImageMode, AiImageSize } from '../providers/image-contract'\nimport { assistCreditsFromUsd } from '../usage/assist-credits'\n\n/**\n * What one picture in Media is expected to draw (AGL-3602), in credits: the\n * estimate the dialog shows before anything is spent, and the figure the\n * door holds a walled band to. Browser-safe, so both read the same number.\n *\n * Each is an ESTIMATE at nominal usage, priced at the billed rates:\n *\n * - a photo is one picture at its per-picture rate for the size the plan\n * makes (512 px on Free, 1K on a paid plan), plus a prompt and the\n * thinking Google's model does before it draws;\n * - an illustration is the text provider's answer — the instructions, the\n * description, and a few thousand tokens of SVG.\n *\n * What is actually charged is what was spent, measured: a picture held back\n * or not stored is never charged, and an illustration that needed its second\n * attempt costs about twice its estimate.\n */\n\n/** A photo's nominal tokens beside its picture. */\nexport const AI_MEDIA_PHOTO_NOMINAL_USAGE: AiUsage & { images: number } = {\n inputTokens: 600,\n outputTokens: 1_500,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n images: 1,\n}\n\n/**\n * An illustration's nominal tokens, as the fast tier's Claude Haiku 5.5 counts\n * them (about 30% more than Claude Haiku 4.5 did for the same text, AGL-3781):\n * about 2,700 of instructions, the tool and the description, and a few\n * thousand of SVG. The instructions cache on that model, but an illustration\n * asked once writes them, so they are counted here at the full input rate.\n */\nexport const AI_MEDIA_SVG_NOMINAL_USAGE: AiUsage = {\n inputTokens: 3_250,\n outputTokens: 3_900,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\n/**\n * The model an illustration is estimated at where the server's choice is\n * not known: the catalog's first fast-tier row, which is what the default\n * text provider serves illustrations on.\n */\nexport const AI_MEDIA_SVG_ESTIMATE_MODEL =\n AI_MODEL_CATALOG.find((entry) => entry.tier === 'fast')?.id ?? AI_MODEL_CATALOG[0].id\n\n/**\n * The credits one picture is expected to draw in `mode`, on `model`, at\n * `size` for a photo (1K when absent; an illustration has no size).\n */\nexport function aiMediaCreditsPerPicture(\n mode: AiImageMode,\n model?: string,\n size?: AiImageSize,\n): number {\n return mode === 'photo'\n ? assistCreditsFromUsd(\n estimateAiBilledUsd(\n { ...AI_MEDIA_PHOTO_NOMINAL_USAGE, ...(size ? { imageSize: size } : {}) },\n model ?? AI_IMAGE_DEFAULT_MODEL,\n ),\n )\n : assistCreditsFromUsd(estimateAiBilledUsd(AI_MEDIA_SVG_NOMINAL_USAGE, model ?? AI_MEDIA_SVG_ESTIMATE_MODEL))\n}\n"],"names":["AI_MODEL_CATALOG","AI_IMAGE_DEFAULT_MODEL","estimateAiBilledUsd","assistCreditsFromUsd","AI_MEDIA_PHOTO_NOMINAL_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","images","AI_MEDIA_SVG_NOMINAL_USAGE","AI_MEDIA_SVG_ESTIMATE_MODEL","find","entry","tier","id","aiMediaCreditsPerPicture","mode","model","size","imageSize"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;;IA0DCA;AAvDF,SACEC,sBAAsB,EACtBD,gBAAgB,EAChBE,mBAAmB,QACd,0BAAsB;AAE7B,SAASC,oBAAoB,QAAQ,6BAAyB;AAE9D;;;;;;;;;;;;;;;;CAgBC,GAED,iDAAiD,GACjD,OAAO,MAAMC,+BAA6D;IACxEC,aAAa;IACbC,cAAc;IACdC,iBAAiB;IACjBC,kBAAkB;IAClBC,QAAQ;AACV,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,6BAAsC;IACjDL,aAAa;IACbC,cAAc;IACdC,iBAAiB;IACjBC,kBAAkB;AACpB,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAMG,uCACXX,yBAAAA,iBAAiBY,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,KAAK,4BAAhDd,uBAAyDe,EAAE,mBAAIf,gBAAgB,CAAC,EAAE,CAACe,EAAE,CAAA;AAEvF;;;CAGC,GACD,OAAO,SAASC,yBACdC,IAAiB,EACjBC,KAAc,EACdC,IAAkB;IAElB,OAAOF,SAAS,UACZd,qBACED,oBACE,aAAKE,8BAAkCe,OAAO;QAAEC,WAAWD;IAAK,IAAI,CAAC,IACrED,gBAAAA,QAASjB,2BAGbE,qBAAqBD,oBAAoBQ,4BAA4BQ,gBAAAA,QAASP;AACpF"}
|
|
@@ -85,14 +85,17 @@ import { AI_CREDIT_RANGE_ZERO, AI_MEASURED_PAID_SITE_CREDITS, AI_MEASURED_PASS_C
|
|
|
85
85
|
* stand, priced as each field says. The spec fails when a figure it derives
|
|
86
86
|
* moves and this does not.
|
|
87
87
|
*/ export const AI_FREE_SITE_WORST_CASE_CREDITS = {
|
|
88
|
-
plan
|
|
89
|
-
|
|
88
|
+
// The plan, the look and a listing run on the fast tier, billed at the
|
|
89
|
+
// platform's markup on Claude Haiku 5.5 since AGL-3781 (36, 7 and 3 credits
|
|
90
|
+
// at Haiku 4.5's cost before).
|
|
91
|
+
plan: 5,
|
|
92
|
+
look: 2,
|
|
90
93
|
layout: 23,
|
|
91
94
|
form: 29,
|
|
92
95
|
page: 43,
|
|
93
96
|
section: 4,
|
|
94
97
|
retry: 64,
|
|
95
|
-
listing:
|
|
98
|
+
listing: 1
|
|
96
99
|
};
|
|
97
100
|
/**
|
|
98
101
|
* The most sections a Free site plans across its pages (AGL-3660), whatever
|