@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.239
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-credits-prompt.component.d.ts +11 -4
- package/src/lib/components/ai-credits-prompt.component.js +84 -34
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- package/src/lib/components/ai-job-links.js +8 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +12 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +12 -0
- package/src/lib/components/ai-jobs-store.js +15 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +22 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
- package/src/lib/components/ai-usage-meter-refresh.js +63 -0
- package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
- package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +12 -16
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +91 -51
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
- package/src/lib/components/use-ai-usage-meter.js +8 -3
- package/src/lib/components/use-ai-usage-meter.js.map +1 -1
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
- package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +33 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +7 -2
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
- package/src/lib/jobs/ai-job-form-dataset.js +161 -0
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
- package/src/lib/jobs/ai-job-form-step.js +41 -10
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
- package/src/lib/jobs/ai-job-page-language.js +45 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
- package/src/lib/jobs/ai-job-plan-step.js +90 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
- package/src/lib/jobs/ai-job-site-content.js +66 -7
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
- package/src/lib/jobs/ai-job-site-datasets.js +499 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
- package/src/lib/jobs/ai-job-site-step.js +152 -30
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +37 -12
- package/src/lib/jobs/ai-jobs.js +184 -28
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
- package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
- package/src/lib/layout-language/ai-layout-compiler.js +397 -9
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +17 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +44 -3
- package/src/lib/layout-language/ai-layout-listings.js +118 -50
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +11 -1
- package/src/lib/model/ai-build-job.js +35 -6
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +23 -3
- package/src/lib/model/ai-build-plan.js +47 -6
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +2 -2
- package/src/lib/model/ai-build-progress.js +6 -4
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +69 -2
- package/src/lib/model/ai-credit-estimate.js +91 -3
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +38 -4
- package/src/lib/model/ai-site-job.js +50 -14
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +34 -6
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
- package/src/lib/runtime/ai-dataset-generation.js +127 -0
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +77 -1
- package/src/lib/runtime/ai-doctrine-validators.js +280 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +10 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +2 -0
- package/src/lib/runtime/ai-node-tree.js +16 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +53 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
- package/src/lib/tools/ai-dataset-tool.js +231 -0
- package/src/lib/tools/ai-dataset-tool.js.map +1 -0
- package/src/lib/usage/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +30 -4
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -93,12 +93,64 @@
|
|
|
93
93
|
p90: Math.ceil(p90 * count / AI_MEASURED_PAGE_SECTIONS)
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* What a page costs when it is written ON ITS OWN — a page an Assist build or
|
|
98
|
+
* a page job writes, rather than one of a site start's pages (AGL-3722).
|
|
99
|
+
*
|
|
100
|
+
* The table's page item is a site start's: its pages are written one after
|
|
101
|
+
* another inside one job, the later ones reading the prompt the first one
|
|
102
|
+
* paid to write, so a 2-section second page measured 19–28. A page written on
|
|
103
|
+
* its own pays that prompt itself and reads the site it lands on, and costs
|
|
104
|
+
* what a site start's FIRST page costs: a 5-section home page measured 39–78
|
|
105
|
+
* (median 40, p90 78), standalone page jobs' writing step 82–105 for a
|
|
106
|
+
* 2-section page, and the Assist build pages of 2026-10-09 22–89 each. Quoting
|
|
107
|
+
* a build's pages at the site start's scaled median told a Free workspace
|
|
108
|
+
* "about 48 credits" for two 4-section pages (AGL-3722, 2026-10-10).
|
|
109
|
+
*
|
|
110
|
+
* So it is priced as a fixed cost — the prompt, the site, the listing — and a
|
|
111
|
+
* cost per section: a 4-section page about 40 (p90 78), a 5-section 46 (p90 90).
|
|
112
|
+
*/ export const AI_MEASURED_STANDALONE_PAGE = {
|
|
113
|
+
fixed: {
|
|
114
|
+
median: 16,
|
|
115
|
+
p90: 30
|
|
116
|
+
},
|
|
117
|
+
perSection: {
|
|
118
|
+
median: 6,
|
|
119
|
+
p90: 12
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/** What a page of this many sections written on its own is likely to cost. */ export function aiMeasuredStandalonePageCredits(sections) {
|
|
123
|
+
const count = Math.max(1, Math.floor(Number.isFinite(sections) ? sections : AI_MEASURED_PAGE_SECTIONS));
|
|
124
|
+
const { fixed, perSection } = AI_MEASURED_STANDALONE_PAGE;
|
|
125
|
+
return {
|
|
126
|
+
median: fixed.median + perSection.median * count,
|
|
127
|
+
p90: fixed.p90 + perSection.p90 * count
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* What a creation costs when it is written ON ITS OWN (AGL-3722), where it
|
|
132
|
+
* measured apart from a site start's: an Assist build's layout came to 24–28
|
|
133
|
+
* (2026-10-09/10), against a site start's 23/24 written in its warm prompt.
|
|
134
|
+
* A form measured the same either way (25–30 on its own, 29/31 in a site
|
|
135
|
+
* start), so it keeps the table's figure.
|
|
136
|
+
*/ export const AI_MEASURED_STANDALONE_CREATION_CREDITS = {
|
|
137
|
+
layout: {
|
|
138
|
+
median: 26,
|
|
139
|
+
p90: 30
|
|
140
|
+
}
|
|
141
|
+
};
|
|
96
142
|
/**
|
|
97
143
|
* What one pass of a kind with no measurement of its own is likely to cost
|
|
98
144
|
* (a component, an email design, a template, an item another runner
|
|
99
|
-
* builds): a
|
|
100
|
-
*
|
|
101
|
-
|
|
145
|
+
* builds), written on its own: a component job's writing step measured
|
|
146
|
+
* 52–65, a form job's 37, a layout job's 63, and the Assist build of
|
|
147
|
+
* 2026-10-09 a welcome email at 46 and a products step at 60. Until
|
|
148
|
+
* 2026-10-10 this was a site start's form (29/31), which is written inside a
|
|
149
|
+
* site job's warm prompt and is the cheapest single answer there is.
|
|
150
|
+
*/ export const AI_MEASURED_PASS_CREDITS = {
|
|
151
|
+
median: 50,
|
|
152
|
+
p90: 65
|
|
153
|
+
};
|
|
102
154
|
export const AI_CREDIT_RANGE_ZERO = {
|
|
103
155
|
likely: 0,
|
|
104
156
|
p90: 0,
|
|
@@ -150,6 +202,42 @@ const n = (value)=>value.toLocaleString('en-US');
|
|
|
150
202
|
/** The smaller first build's button: "Build the home page first (about 64 credits)". */ export function aiCreditsSmallerText(smaller) {
|
|
151
203
|
return `${smaller.label} (about ${n(smaller.likely)} credits)`;
|
|
152
204
|
}
|
|
205
|
+
const credit = (count)=>`${n(count)} ${count === 1 ? 'credit' : 'credits'}`;
|
|
206
|
+
/**
|
|
207
|
+
* The prompt's ways on, as its three option buttons read them (Zach,
|
|
208
|
+
* 2026-10-10: "more appealing and consistent"): build what fits, the smaller
|
|
209
|
+
* first build when there is one, and Upgrade when it can be offered — each a
|
|
210
|
+
* short sentence-case label with its figure under it. The smaller build is
|
|
211
|
+
* the recommended one when its p90 fits what is left, since it then finishes;
|
|
212
|
+
* otherwise building what fits is.
|
|
213
|
+
*/ export function aiCreditsChoices(prompt, offers) {
|
|
214
|
+
const left = Math.max(0, Math.floor(prompt.left));
|
|
215
|
+
const smaller = offers.smaller ? prompt.smaller : null;
|
|
216
|
+
const smallerFits = smaller !== null && smaller.p90 <= left;
|
|
217
|
+
const choices = [
|
|
218
|
+
{
|
|
219
|
+
key: 'fits',
|
|
220
|
+
label: AI_CREDITS_BUILD_WHAT_FITS,
|
|
221
|
+
detail: `Uses your ${credit(left)} left, then pauses`,
|
|
222
|
+
recommended: !smallerFits
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
if (smaller) {
|
|
226
|
+
choices.push({
|
|
227
|
+
key: 'smaller',
|
|
228
|
+
label: smaller.label,
|
|
229
|
+
detail: smallerFits ? `About ${credit(smaller.likely)}, fits what you have left` : `About ${credit(smaller.likely)}`,
|
|
230
|
+
recommended: smallerFits
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (offers.upgrade) choices.push({
|
|
234
|
+
key: 'upgrade',
|
|
235
|
+
label: 'Upgrade',
|
|
236
|
+
detail: 'Get more credits',
|
|
237
|
+
recommended: false
|
|
238
|
+
});
|
|
239
|
+
return choices;
|
|
240
|
+
}
|
|
153
241
|
/** A reset day as a person reads it: "November 1". */ function resetLabel(resetsOn) {
|
|
154
242
|
const at = new Date(`${resetsOn}T00:00:00.000Z`);
|
|
155
243
|
return Number.isFinite(at.getTime()) ? at.toLocaleDateString('en-US', {
|
|
@@ -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/** 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 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): a form's, the measured single-answer creation closest to them.\n * A declared stand-in, replaced the day those kinds are measured.\n */\nexport const AI_MEASURED_PASS_CREDITS: AiMeasuredCredits = AI_MEASURED_UNIT_CREDITS.form\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/** 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_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_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","resetLabel","resetsOn","at","Date","getTime","toLocaleDateString","month","day","timeZone","aiCreditsPromptText","prompt","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 * 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/** 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_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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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { AiJobSummary } from './ai-jobs.types';
|
|
18
|
+
/**
|
|
19
|
+
* What every surface that cancels an AI job says (AGL-3616): the job's page,
|
|
20
|
+
* a row of the site's AI jobs list, and the AI jobs list the top-bar chip
|
|
21
|
+
* opens. One wording, so a person reads the same promise wherever they stop
|
|
22
|
+
* a job.
|
|
23
|
+
*/
|
|
24
|
+
/** The confirm dialog's title. */
|
|
25
|
+
export declare const AI_JOB_CANCEL_TITLE = "Cancel this AI job?";
|
|
26
|
+
/** What canceling does, said before it is done. */
|
|
27
|
+
export declare const AI_JOB_CANCEL_CONFIRM_COPY = "Stop building. Pages already built stay as drafts; you won\u2019t be charged for steps that haven\u2019t run.";
|
|
28
|
+
/** While the step in flight finishes or stops. */
|
|
29
|
+
export declare const AI_JOB_CANCELING_COPY = "Stopping. The step in progress finishes or stops first, and nothing after it runs.";
|
|
30
|
+
/** What a canceled job's page says under its heading. */
|
|
31
|
+
export declare const AI_JOB_CANCELED_LEDE = "You canceled this job. Anything it already built stays as an unpublished draft, and nothing was published.";
|
|
32
|
+
/**
|
|
33
|
+
* Why a job cannot be canceled, or `null` when it can: one that is done,
|
|
34
|
+
* failed or already canceled has nothing left to stop, and one already
|
|
35
|
+
* stopping needs no second cancel. The button stays, disabled, with this as
|
|
36
|
+
* its reason.
|
|
37
|
+
*/
|
|
38
|
+
export declare function aiJobCancelBlockedReason(job: Pick<AiJobSummary, 'status'> & Partial<Pick<AiJobSummary, 'cancelRequested'>>): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* What a canceled job cost (AGL-3616): the credits its finished steps used,
|
|
41
|
+
* which stay charged, less anything given back. "This job used 42 credits
|
|
42
|
+
* before it stopped." — or that it used none.
|
|
43
|
+
*/
|
|
44
|
+
export declare function aiJobCanceledCreditsCopy(job: Partial<Pick<AiJobSummary, 'creditsSpent' | 'refundedCredits'>>): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* What every surface that cancels an AI job says (AGL-3616): the job's page,
|
|
18
|
+
* a row of the site's AI jobs list, and the AI jobs list the top-bar chip
|
|
19
|
+
* opens. One wording, so a person reads the same promise wherever they stop
|
|
20
|
+
* a job.
|
|
21
|
+
*/ /** The confirm dialog's title. */ export const AI_JOB_CANCEL_TITLE = 'Cancel this AI job?';
|
|
22
|
+
/** What canceling does, said before it is done. */ export const AI_JOB_CANCEL_CONFIRM_COPY = 'Stop building. Pages already built stay as drafts; you won’t be charged for steps that haven’t run.';
|
|
23
|
+
/** While the step in flight finishes or stops. */ export const AI_JOB_CANCELING_COPY = 'Stopping. The step in progress finishes or stops first, and nothing after it runs.';
|
|
24
|
+
/** What a canceled job's page says under its heading. */ export const AI_JOB_CANCELED_LEDE = 'You canceled this job. Anything it already built stays as an unpublished draft, and nothing was published.';
|
|
25
|
+
/**
|
|
26
|
+
* Why a job cannot be canceled, or `null` when it can: one that is done,
|
|
27
|
+
* failed or already canceled has nothing left to stop, and one already
|
|
28
|
+
* stopping needs no second cancel. The button stays, disabled, with this as
|
|
29
|
+
* its reason.
|
|
30
|
+
*/ export function aiJobCancelBlockedReason(job) {
|
|
31
|
+
switch(job.status){
|
|
32
|
+
case 'done':
|
|
33
|
+
return 'This job already finished.';
|
|
34
|
+
case 'failed':
|
|
35
|
+
return 'This job already stopped.';
|
|
36
|
+
case 'canceled':
|
|
37
|
+
return 'This job was canceled.';
|
|
38
|
+
default:
|
|
39
|
+
return job.cancelRequested ? AI_JOB_CANCELING_COPY : null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What a canceled job cost (AGL-3616): the credits its finished steps used,
|
|
44
|
+
* which stay charged, less anything given back. "This job used 42 credits
|
|
45
|
+
* before it stopped." — or that it used none.
|
|
46
|
+
*/ export function aiJobCanceledCreditsCopy(job) {
|
|
47
|
+
var _job_creditsSpent, _job_refundedCredits;
|
|
48
|
+
const net = Math.max(0, Math.floor(((_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0) - ((_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0)));
|
|
49
|
+
if (net === 0) return 'This job used no credits before it stopped.';
|
|
50
|
+
return `This job used ${net} ${net === 1 ? 'credit' : 'credits'} before it stopped.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=ai-job-cancel-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-cancel-copy.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 { AiJobSummary } from './ai-jobs.types'\n\n/**\n * What every surface that cancels an AI job says (AGL-3616): the job's page,\n * a row of the site's AI jobs list, and the AI jobs list the top-bar chip\n * opens. One wording, so a person reads the same promise wherever they stop\n * a job.\n */\n\n/** The confirm dialog's title. */\nexport const AI_JOB_CANCEL_TITLE = 'Cancel this AI job?'\n\n/** What canceling does, said before it is done. */\nexport const AI_JOB_CANCEL_CONFIRM_COPY =\n 'Stop building. Pages already built stay as drafts; you won’t be charged for steps that haven’t run.'\n\n/** While the step in flight finishes or stops. */\nexport const AI_JOB_CANCELING_COPY =\n 'Stopping. The step in progress finishes or stops first, and nothing after it runs.'\n\n/** What a canceled job's page says under its heading. */\nexport const AI_JOB_CANCELED_LEDE =\n 'You canceled this job. Anything it already built stays as an unpublished draft, and nothing was published.'\n\n/**\n * Why a job cannot be canceled, or `null` when it can: one that is done,\n * failed or already canceled has nothing left to stop, and one already\n * stopping needs no second cancel. The button stays, disabled, with this as\n * its reason.\n */\nexport function aiJobCancelBlockedReason(\n job: Pick<AiJobSummary, 'status'> & Partial<Pick<AiJobSummary, 'cancelRequested'>>,\n): string | null {\n switch (job.status) {\n case 'done':\n return 'This job already finished.'\n case 'failed':\n return 'This job already stopped.'\n case 'canceled':\n return 'This job was canceled.'\n default:\n return job.cancelRequested ? AI_JOB_CANCELING_COPY : null\n }\n}\n\n/**\n * What a canceled job cost (AGL-3616): the credits its finished steps used,\n * which stay charged, less anything given back. \"This job used 42 credits\n * before it stopped.\" — or that it used none.\n */\nexport function aiJobCanceledCreditsCopy(\n job: Partial<Pick<AiJobSummary, 'creditsSpent' | 'refundedCredits'>>,\n): string {\n const net = Math.max(0, Math.floor((job.creditsSpent ?? 0) - (job.refundedCredits ?? 0)))\n if (net === 0) return 'This job used no credits before it stopped.'\n return `This job used ${net} ${net === 1 ? 'credit' : 'credits'} before it stopped.`\n}\n"],"names":["AI_JOB_CANCEL_TITLE","AI_JOB_CANCEL_CONFIRM_COPY","AI_JOB_CANCELING_COPY","AI_JOB_CANCELED_LEDE","aiJobCancelBlockedReason","job","status","cancelRequested","aiJobCanceledCreditsCopy","net","Math","max","floor","creditsSpent","refundedCredits"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;CAKC,GAED,gCAAgC,GAChC,OAAO,MAAMA,sBAAsB,sBAAqB;AAExD,iDAAiD,GACjD,OAAO,MAAMC,6BACX,sGAAqG;AAEvG,gDAAgD,GAChD,OAAO,MAAMC,wBACX,qFAAoF;AAEtF,uDAAuD,GACvD,OAAO,MAAMC,uBACX,6GAA4G;AAE9G;;;;;CAKC,GACD,OAAO,SAASC,yBACdC,GAAkF;IAElF,OAAQA,IAAIC,MAAM;QAChB,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAOD,IAAIE,eAAe,GAAGL,wBAAwB;IACzD;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASM,yBACdH,GAAoE;QAEhCA,mBAA0BA;IAA9D,MAAMI,MAAMC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAAC,EAACP,oBAAAA,IAAIQ,YAAY,YAAhBR,oBAAoB,OAAMA,uBAAAA,IAAIS,eAAe,YAAnBT,uBAAuB;IACrF,IAAII,QAAQ,GAAG,OAAO;IACtB,OAAO,CAAC,cAAc,EAAEA,IAAI,CAAC,EAAEA,QAAQ,IAAI,WAAW,UAAU,mBAAmB,CAAC;AACtF"}
|
|
@@ -63,7 +63,10 @@ export declare function aiPlanRetryRefusal(input: {
|
|
|
63
63
|
* the job's own record (`aiJobRefundCopy`).
|
|
64
64
|
*/
|
|
65
65
|
export declare const AI_SITE_GUIDED_BUILD_FAILED_COPY = "Something went wrong building your site, and it was not built.";
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* What a person's own cancel said about its credits before AGL-3616, which
|
|
68
|
+
* now names them: `aiJobCanceledCreditsCopy`.
|
|
69
|
+
*/
|
|
67
70
|
export declare const AI_JOB_CANCELED_CREDITS_COPY = "You paid for what was spent up to then.";
|
|
68
71
|
/**
|
|
69
72
|
* What every surface that shows a stopped job says about its credits
|
|
@@ -15,7 +15,8 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
15
15
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
|
-
*/ import {
|
|
18
|
+
*/ import { aiJobCanceledCreditsCopy } from "./ai-job-cancel-copy.js";
|
|
19
|
+
import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
|
|
19
20
|
/**
|
|
20
21
|
* What a member reads when a plan could not be made (AGL-3594): one plain
|
|
21
22
|
* sentence and one thing to do, never a rule number or a validator's words.
|
|
@@ -68,7 +69,10 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
68
69
|
* sentence, never a rule. Whether its credits came back is said beside it, from
|
|
69
70
|
* the job's own record (`aiJobRefundCopy`).
|
|
70
71
|
*/ export const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.';
|
|
71
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* What a person's own cancel said about its credits before AGL-3616, which
|
|
74
|
+
* now names them: `aiJobCanceledCreditsCopy`.
|
|
75
|
+
*/ export const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.';
|
|
72
76
|
/**
|
|
73
77
|
* What every surface that shows a stopped job says about its credits
|
|
74
78
|
* (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),
|
|
@@ -86,7 +90,8 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
86
90
|
const whole = refunded >= Math.floor((_job_creditsSpent1 = job.creditsSpent) != null ? _job_creditsSpent1 : 0);
|
|
87
91
|
return whole ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.` : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`;
|
|
88
92
|
}
|
|
89
|
-
|
|
93
|
+
// A person's own cancel keeps what ran charged, and says how much (AGL-3616).
|
|
94
|
+
if (job.status === 'canceled' && ((_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0) > 0) return aiJobCanceledCreditsCopy(job);
|
|
90
95
|
return null;
|
|
91
96
|
}
|
|
92
97
|
/** The guided start's way back to the starter (AGL-3594). */ export const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/** What a person's own cancel says about its credits: they paid for what ran until then. */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return AI_JOB_CANCELED_CREDITS_COPY\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n\n/**\n * What a customer reads when an answer still broke a building rule after its\n * re-ask (AGL-3596), in place of the doctrine's own sentence. \"Rule 12\n * (Responsive by the theme's breakpoints): Every child of a Grid container…\"\n * is what the re-ask is told and what staff read; shown to the person who\n * asked for a site, it reads as the product breaking. The rule's words stay\n * on the job (`review.detail`, an item's `failure.detail`) for staff.\n */\nexport const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`\n\n/** The same, for a page. */\nexport const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`\n\n/** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */\nconst AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.'\n\n/** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */\nexport function aiIsDoctrineRuleText(text: string | null | undefined): boolean {\n if (!text) return false\n return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\\bRule \\d+ \\(/.test(text)\n}\n\n/**\n * A sentence a customer may read: the doctrine's own words become the plain\n * refusal, a page's when the thing refused is a page; anything else stands.\n */\nexport function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options: { page?: boolean } = {}): T | string {\n if (!aiIsDoctrineRuleText(text)) return text\n return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY\n}\n\n/**\n * A doctrine review as a customer reads it (AGL-3596): a message in the\n * doctrine's own words becomes the plain refusal, and those words move to\n * `detail`, which only staff are shown. A review written before the step did\n * this itself reads the same as one written after.\n */\nexport function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & { detail?: string }>(\n review: T,\n options: { page?: boolean } = {},\n): T {\n if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review\n return { ...review, message: aiCustomerSafeCopy(review.message, options), detail: review.detail ?? review.message }\n}\n\n/**\n * A build item's failure as a customer reads it (AGL-3596): a building rule\n * still broken reads as the plain refusal, a page's for a page, and its\n * checks' own sentence (`detail`) is left off.\n */\nexport function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T {\n const failure = row.failure\n if (!failure) return row\n const { detail: _detail, ...rest } = failure\n const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message)\n if (!doctrine && _detail === undefined) return row\n return {\n ...row,\n failure: {\n ...rest,\n message: doctrine\n ? row.op === 'page'\n ? AI_DOCTRINE_REFUSED_PAGE_COPY\n : AI_DOCTRINE_REFUSED_COPY\n : failure.message,\n },\n }\n}\n"],"names":["PLATFORM_BRAND_NAME","AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason","AI_DOCTRINE_REFUSED_COPY","AI_DOCTRINE_REFUSED_PAGE_COPY","AI_DOCTRINE_REFUSAL_LEAD","aiIsDoctrineRuleText","text","startsWith","test","aiCustomerSafeCopy","options","page","aiCustomerSafeReview","message","detail","aiCustomerSafeItem","row","failure","_detail","rest","doctrine","undefined","op"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 { aiJobCanceledCreditsCopy } from './ai-job-cancel-copy'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/**\n * What a person's own cancel said about its credits before AGL-3616, which\n * now names them: `aiJobCanceledCreditsCopy`.\n */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n // A person's own cancel keeps what ran charged, and says how much (AGL-3616).\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return aiJobCanceledCreditsCopy(job)\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n\n/**\n * What a customer reads when an answer still broke a building rule after its\n * re-ask (AGL-3596), in place of the doctrine's own sentence. \"Rule 12\n * (Responsive by the theme's breakpoints): Every child of a Grid container…\"\n * is what the re-ask is told and what staff read; shown to the person who\n * asked for a site, it reads as the product breaking. The rule's words stay\n * on the job (`review.detail`, an item's `failure.detail`) for staff.\n */\nexport const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`\n\n/** The same, for a page. */\nexport const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`\n\n/** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */\nconst AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.'\n\n/** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */\nexport function aiIsDoctrineRuleText(text: string | null | undefined): boolean {\n if (!text) return false\n return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\\bRule \\d+ \\(/.test(text)\n}\n\n/**\n * A sentence a customer may read: the doctrine's own words become the plain\n * refusal, a page's when the thing refused is a page; anything else stands.\n */\nexport function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options: { page?: boolean } = {}): T | string {\n if (!aiIsDoctrineRuleText(text)) return text\n return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY\n}\n\n/**\n * A doctrine review as a customer reads it (AGL-3596): a message in the\n * doctrine's own words becomes the plain refusal, and those words move to\n * `detail`, which only staff are shown. A review written before the step did\n * this itself reads the same as one written after.\n */\nexport function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & { detail?: string }>(\n review: T,\n options: { page?: boolean } = {},\n): T {\n if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review\n return { ...review, message: aiCustomerSafeCopy(review.message, options), detail: review.detail ?? review.message }\n}\n\n/**\n * A build item's failure as a customer reads it (AGL-3596): a building rule\n * still broken reads as the plain refusal, a page's for a page, and its\n * checks' own sentence (`detail`) is left off.\n */\nexport function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T {\n const failure = row.failure\n if (!failure) return row\n const { detail: _detail, ...rest } = failure\n const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message)\n if (!doctrine && _detail === undefined) return row\n return {\n ...row,\n failure: {\n ...rest,\n message: doctrine\n ? row.op === 'page'\n ? AI_DOCTRINE_REFUSED_PAGE_COPY\n : AI_DOCTRINE_REFUSED_COPY\n : failure.message,\n },\n }\n}\n"],"names":["aiJobCanceledCreditsCopy","PLATFORM_BRAND_NAME","AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason","AI_DOCTRINE_REFUSED_COPY","AI_DOCTRINE_REFUSED_PAGE_COPY","AI_DOCTRINE_REFUSAL_LEAD","aiIsDoctrineRuleText","text","startsWith","test","aiCustomerSafeCopy","options","page","aiCustomerSafeReview","message","detail","aiCustomerSafeItem","row","failure","_detail","rest","doctrine","undefined","op"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,QAAQ,0BAAsB;AAC/D,SAASC,mBAAmB,QAAQ,wCAAuC;AAG3E;;;;;;;;;;;;;;;;CAgBC,GAED,0DAA0D,GAC1D,OAAO,MAAMC,yBAAyB,sBAAqB;AAE3D,qDAAqD,GACrD,SAASC,aAAaC,IAAwB;IAC5C,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAO;IACX;AACF;AAWA;;;;CAIC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;IAC7D,MAAMC,QAAQD,MAAME,QAAQ,GAAG,gDAAgD;IAC/E,IAAIF,MAAMG,KAAK,CAACC,QAAQ,CAACR,yBAAyB;QAChD,OAAO,CAAC,mDAAmD,EAAEK,MAAM,uCAAuC,CAAC;IAC7G;IACA,OAAO,CAAC,8BAA8B,EAAEJ,aAAaG,MAAMF,IAAI,IAAIG,MAAM,YAAY,CAAC;AACxF;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAmBL,KAAmD;IACpF,MAAMM,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACT,MAAMU,WAAW;IACrD,IAAIJ,QAAQN,MAAMW,WAAW,EAAE,OAAO;IACtC,OAAO,CAAC,SAAS,EAAEL,KAAK,IAAI,EAAEA,SAAS,IAAI,WAAW,UAAU,yCAAyC,EAAEN,MAAMW,WAAW,CAAC,uDAAuD,CAAC;AACvL;AAEA;;;;CAIC,GACD,OAAO,MAAMC,mCAAmC,iEAAgE;AAEhH;;;CAGC,GACD,OAAO,MAAMC,+BAA+B,0CAAyC;AAErF;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,GAA8F;QAEtDA,sBAUNA;IAVlC,MAAMb,WAAWK,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,EAACM,uBAAAA,IAAIC,eAAe,YAAnBD,uBAAuB;IAC/D,IAAIb,WAAW,GAAG;YAGqBa;QAFrC,MAAME,UAAU,GAAGf,SAAS,CAAC,EAAEA,aAAa,IAAI,WAAW,WAAW;QACtE,MAAMgB,OAAOhB,aAAa,IAAI,OAAO;QACrC,MAAMiB,QAAQjB,YAAYK,KAAKE,KAAK,EAACM,qBAAAA,IAAIK,YAAY,YAAhBL,qBAAoB;QACzD,OAAOI,QACH,CAAC,4CAA4C,EAAEF,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC,GACjG,CAAC,qEAAqE,EAAED,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC;IAChI;IACA,8EAA8E;IAC9E,IAAIH,IAAIM,MAAM,KAAK,cAAc,EAACN,oBAAAA,IAAIK,YAAY,YAAhBL,oBAAoB,KAAK,GAAG,OAAOrB,yBAAyBqB;IAC9F,OAAO;AACT;AAEA,2DAA2D,GAC3D,OAAO,MAAMO,iCAAiC,+BAA8B;AAE5E;;;;;;CAMC,GACD,OAAO,SAASC,6BACdR,GAA4E;QAMpCA;IAJxC,IAAIA,IAAIjB,IAAI,KAAK,UAAU,CAACiB,IAAIS,MAAM,EAAE,OAAO;IAC/C,2FAA2F;IAC3F,IAAIT,IAAIU,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,QAAQ,KAAK,QAAQ,OAAO;IACpE,IAAIb,IAAIM,MAAM,KAAK,YAAYN,IAAIM,MAAM,KAAK,YAAY,OAAO;IACjE,OAAON,IAAIM,MAAM,KAAK,kBAAkBN,EAAAA,cAAAA,IAAIc,MAAM,qBAAVd,YAAYe,MAAM,MAAK;AACjE;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA2B,GAAGpC,oBAAoB,qFAAqF,CAAC,CAAA;AAErJ,0BAA0B,GAC1B,OAAO,MAAMqC,gCAAgC,GAAGrC,oBAAoB,wFAAwF,CAAC,CAAA;AAE7J,gFAAgF,GAChF,MAAMsC,2BAA2B;AAEjC,4FAA4F,GAC5F,OAAO,SAASC,qBAAqBC,IAA+B;IAClE,IAAI,CAACA,MAAM,OAAO;IAClB,OAAOA,KAAKC,UAAU,CAACH,6BAA6B,gBAAgBI,IAAI,CAACF;AAC3E;AAEA;;;CAGC,GACD,OAAO,SAASG,mBAAwDH,IAAO,EAAEI,UAA8B,CAAC,CAAC;IAC/G,IAAI,CAACL,qBAAqBC,OAAO,OAAOA;IACxC,OAAOI,QAAQC,IAAI,GAAGR,gCAAgCD;AACxD;AAEA;;;;;CAKC,GACD,OAAO,SAASU,qBACdZ,MAAS,EACTU,UAA8B,CAAC,CAAC;QAGkDV;IADlF,IAAIA,OAAOC,MAAM,KAAK,cAAc,CAACI,qBAAqBL,OAAOa,OAAO,GAAG,OAAOb;IAClF,OAAO,aAAKA;QAAQa,SAASJ,mBAAmBT,OAAOa,OAAO,EAAEH;QAAUI,MAAM,GAAEd,iBAAAA,OAAOc,MAAM,YAAbd,iBAAiBA,OAAOa,OAAO;;AACnH;AAEA;;;;CAIC,GACD,OAAO,SAASE,mBAAsEC,GAAM;IAC1F,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,IAAI,CAACA,SAAS,OAAOD;IACrB,MAAM,EAAEF,QAAQI,OAAO,EAAW,GAAGD,SAATE,wCAASF;;;IACrC,MAAMG,WAAWH,QAAQhB,MAAM,KAAK,sBAAsBI,qBAAqBY,QAAQJ,OAAO;IAC9F,IAAI,CAACO,YAAYF,YAAYG,WAAW,OAAOL;IAC/C,OAAO,aACFA;QACHC,SAAS,aACJE;YACHN,SAASO,WACLJ,IAAIM,EAAE,KAAK,SACTnB,gCACAD,2BACFe,QAAQJ,OAAO;;;AAGzB"}
|
|
@@ -468,6 +468,20 @@ export interface AiJob {
|
|
|
468
468
|
* what was left and the range they were shown; absent on every job that fit.
|
|
469
469
|
*/
|
|
470
470
|
creditsConfirmed?: AiJobCreditsConfirmation | null;
|
|
471
|
+
/**
|
|
472
|
+
* A person asked to stop the job (AGL-3616): written only by the cancel
|
|
473
|
+
* door, in the transaction that decides it. A job no step holds ends
|
|
474
|
+
* `canceled` in that same write; a job whose step holds a live lease keeps
|
|
475
|
+
* running until the step finishes or aborts, and the machine ends it
|
|
476
|
+
* `canceled` at its next write. Absent on a job nobody canceled.
|
|
477
|
+
*/
|
|
478
|
+
cancelRequested?: AiJobCancelRequest | null;
|
|
479
|
+
}
|
|
480
|
+
/** Who asked to cancel a job, and when (AGL-3616). */
|
|
481
|
+
export interface AiJobCancelRequest {
|
|
482
|
+
at: ITimestamp;
|
|
483
|
+
/** The member or staff account that asked; `null` when nobody was named. */
|
|
484
|
+
by: string | null;
|
|
471
485
|
}
|
|
472
486
|
/**
|
|
473
487
|
* A guided site start's publish (AGL-3596): the pages that went live, with
|
|
@@ -583,4 +597,10 @@ export interface AiJobSummary {
|
|
|
583
597
|
publishAsked?: boolean;
|
|
584
598
|
/** A `build` job's planning spend and how it settled (AGL-3616). */
|
|
585
599
|
orchestration?: AiJobOrchestration;
|
|
600
|
+
/**
|
|
601
|
+
* A cancel was asked and the job has not ended yet (AGL-3616): the step in
|
|
602
|
+
* flight is finishing or aborting, and nothing after it runs. Absent once
|
|
603
|
+
* the job is terminal, and on every job nobody canceled.
|
|
604
|
+
*/
|
|
605
|
+
cancelRequested?: boolean;
|
|
586
606
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-jobs.types.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 { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type { AiEffort } from '../providers/contract'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiBuildPlan } from './ai-build-plan'\nimport type { AiFreeCreditsLeft } from './ai-site-job'\nimport type { AiJobCreditsConfirmation } from './ai-credit-estimate'\n\n/**\n * AI generation jobs (AGL-2904).\n *\n * A job is one customer brief carried through one or more model steps by a\n * Firestore state machine, so that work longer than a request can survive\n * the request: the console route runs what it can inline, and the platform\n * job beat resumes whatever is left. The document is the whole state — no\n * process holds anything the next beat cannot read back.\n *\n * ── Outputs are drafts, never a publish ──────────────────────────────────\n *\n * Every output a job produces is a NEW draft or a new unpublished version of\n * an existing resource. A job never flips a version pointer, never registers\n * a route and never sends anything; a person opens the draft the job names\n * and publishes it through the door that already exists for that resource.\n * That is what lets a job run with no visitor, no session and no confirm\n * gate — the thing it writes cannot be seen by anyone but the workspace\n * until a member chooses otherwise.\n *\n * ── The lease ────────────────────────────────────────────────────────────\n *\n * A step runs under a lease: an owner id and an `until` instant. Two beats\n * fire a minute apart against a 120 s function timeout, and a console route\n * may still be running the first step when the first beat looks, so the\n * lease is what stops the same step being run — and metered — twice. A\n * claim is a transaction that refuses while another owner's lease is live;\n * an expired lease is simply claimable, which is also how a step abandoned\n * by a frozen process is recovered.\n */\n\n/**\n * What a job produces. `text` and `theme` have runners; every other kind is\n * registered and refuses with \"not available yet\" until its own issue lands,\n * so a kind's presence in the union says the job model accepts it, not that\n * a runner exists.\n */\nexport type AiJobKind =\n | 'page'\n | 'component'\n | 'layout'\n | 'template'\n | 'edit'\n | 'seo'\n | 'site'\n | 'email'\n | 'campaign'\n | 'form'\n | 'experiment'\n | 'insight'\n | 'products'\n | 'crm'\n | 'onboarding'\n | 'workflow'\n | 'logic'\n | 'text'\n | 'theme'\n | 'build'\n\n/** The union as a value, so a route can validate a body against it. */\nexport const AI_JOB_KINDS: readonly AiJobKind[] = [\n 'page',\n 'component',\n 'layout',\n 'template',\n 'edit',\n 'seo',\n 'site',\n 'email',\n 'campaign',\n 'form',\n 'experiment',\n 'insight',\n 'products',\n 'crm',\n 'onboarding',\n 'workflow',\n 'logic',\n 'text',\n 'theme',\n 'build',\n]\n\n/**\n * `queued` — waiting for a runner; `running` — a step holds the lease;\n * `needs_input` — the job stopped for something only the workspace can\n * change (credits, a cap, a switch), and the beat tries it again once that\n * may have changed; `needs_review` — the job stopped for a person's decision\n * (AGL-2935): its plan is ready to confirm, or an answer broke a building\n * rule on its re-ask. No timer can make that decision, so neither the beat\n * nor anything but that person resumes it. The last three are terminal.\n */\nexport type AiJobStatus =\n | 'queued'\n | 'running'\n | 'needs_input'\n | 'needs_review'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport const AI_JOB_TERMINAL_STATUSES: readonly AiJobStatus[] = [\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport type AiJobStepStatus = 'pending' | 'running' | 'done' | 'failed'\n\n/** The step a planned kind runs first (AGL-2935): its plan, before anything is built. */\nexport const AI_JOB_PLAN_STEP = 'plan'\n\nexport interface AiJobStep {\n /** Stable within the job (`plan`, `draft`, `generate`), unique per job. */\n name: string\n status: AiJobStepStatus\n startedAt?: ITimestamp | null\n endedAt?: ITimestamp | null\n /** Credits this step has actually cost, at the plan's credit rate. */\n creditsSpent: number\n /** Customer-safe only; provider detail goes to the server log. */\n error?: string | null\n /**\n * How many times a runner has been handed this step. A retryable provider\n * failure hands the step back as `pending`; the count is what stops a\n * provider outage re-running a step forever.\n */\n attempts?: number\n /**\n * Passes of a step that works through a list (AGL-2910): each one a\n * recorded, metered exchange that asked to continue. Attempts count again\n * from zero after a pass; this count is what bounds the step.\n */\n passes?: number\n /**\n * What the step's model calls cost in tokens and time (AGL-2937), summed\n * over every run that reached the provider. Absent on a step none has.\n */\n tokens?: AiJobStepTokens\n /**\n * The ids the step writes its drafts under, by draft (AGL-3079): minted\n * when the job is created, so every run of the step names the same drafts.\n * Absent on a step that writes none; a step that records none names its\n * drafts by the job's own id.\n */\n draftIds?: Partial<Record<AiJobDraftSlot, string>>\n}\n\n/**\n * A draft a job's kind always writes (AGL-3079): a screen, layout, template,\n * form or component; an email design; a campaign; an automation.\n */\nexport type AiJobDraftSlot =\n | 'screen'\n | 'layout'\n | 'template'\n | 'form'\n | 'component'\n | 'email'\n | 'campaign'\n | 'workflow'\n\n/**\n * A step's measure (AGL-2937): the four token counts the meter prices, the\n * time spent in the runner, and the model and thinking effort of the last\n * run. The machine writes it beside `creditsSpent`, so what a kind of step\n * costs in tokens is read off the job rather than inferred from the month.\n */\nexport interface AiJobStepTokens {\n /** Prompt tokens sent uncached. */\n input: number\n /** Prompt tokens read from the prompt cache. */\n cachedRead: number\n /** Prompt tokens written to the prompt cache. */\n cacheWrite: number\n /** Tokens generated, thinking included. */\n output: number\n /** The model the last run was served by. */\n model: string | null\n /** The thinking effort the last run asked for; `null` when it named none. */\n effort: AiEffort | null\n /** Milliseconds spent in the runner, summed over runs. */\n latencyMs: number\n /** Runs that reached the provider. */\n runs: number\n /**\n * Why the step's latest runs stopped (AGL-3042), oldest first and the last\n * run last: each run's stop reason and the tokens it generated, so a pass\n * cut off at its ceiling reads off the job rather than out of the sums. At\n * most `AI_JOB_STEP_LAST_RUNS` are kept; absent on a step measured before.\n */\n lastRuns?: AiJobStepRunStop[]\n}\n\n/** One run of a step, by why it stopped (AGL-3042). */\nexport interface AiJobStepRunStop {\n /**\n * The stop reason of the run's last model call — `tool_use` or `end_turn`\n * for an answer, `max_tokens` for one cut off at its ceiling, `refusal` —\n * and `null` when the provider named none.\n */\n stopReason: string | null\n /** Tokens the run generated, over every model call it made. */\n output: number\n}\n\nexport type AiJobOutputResource =\n | 'screen'\n | 'reusableComponent'\n | 'layout'\n | 'template'\n | 'form'\n | 'emailScreen'\n | 'campaign'\n | 'product'\n | 'experiment'\n | 'workflow'\n | 'text'\n | 'theme'\n | 'seo'\n | 'insight'\n | 'crm'\n /** A draft another plugin wrote for a build (AGL-3616), named by `draftResource`. */\n | 'draft'\n | 'logic'\n | 'orgAutomation'\n /** A post a guided start wrote into the site's blog (AGL-3676): a content collection entry. */\n | 'entry'\n\n/**\n * One thing a job wrote. Addressed by resource and id so the console can\n * build the \"open draft\" link without knowing what the runner did; `text`\n * has no document of its own, so its body rides on the output itself.\n *\n * A `theme` output is a proposal rather than a document (AGL-2938). A site's\n * theme is fields on its host document, and writing those fields is the save\n * the Theme section's editor performs, which a job never does. So the change\n * set rides on the output as `proposal`, and a person puts it in the editor\n * and saves it there, or does not.\n *\n * An `seo` output is a proposal rather than a document (AGL-2910): search\n * listing values for a page or a product, or a site audit's findings and\n * fixes. A screen's listing is served from the screen document itself, so a\n * job never writes it; the values ride on the output as `proposal`, a person\n * puts them in the editor and saves them there, and an audit's content fixes\n * become new versions only when a person applies them.\n *\n * A `crm` output is a proposal too (AGL-2917): a record's summary and next\n * step, an email draft, or an import's column matches. The CRM's own task\n * form, stage route, composer and import drawer are the writes, and a person\n * makes each of them, or does not.\n *\n * A `logic` output is a proposal too (AGL-3603): a site function or variable\n * the Functions & Variables editor opens unsaved. Saving it is the editor's\n * write, which a person makes, or does not.\n *\n * An `orgAutomation` output is a proposal too (AGL-3603): one of the\n * workspace's automations, which the Org automations editor opens unsaved and\n * switched off. `hostId` is `null`: it belongs to no one site.\n */\nexport interface AiJobOutput {\n resource: AiJobOutputResource\n id: string\n /** The unpublished version the job created, where the resource has versions. */\n versionId?: string | null\n /** `null` for an output that belongs to the org rather than one site. */\n hostId: string | null\n /**\n * The site's subdomain, which is what a console URL names a site by: the\n * `[host]` segment resolves by subdomain, so a link built from `hostId`\n * names no site the console can open.\n */\n hostSubdomain?: string | null\n label: string\n /** The body of a `text` output. Absent on every other resource. */\n text?: string\n /**\n * The change set of an output a person applies rather than opens — a\n * `theme` or `seo` proposal — in the shape its job kind's runner defines.\n */\n proposal?: Record<string, unknown>\n /**\n * What the person decides next about this output, in customer-safe words:\n * a routing the draft could not store, or part of the brief it could not\n * build. Absent when there is nothing to decide.\n */\n note?: string | null\n /**\n * What a first visit is estimated to transfer, as the doctrine measured\n * the generated document (AGL-2935) — the weight the proposal shows before\n * anything is applied. Absent where the output is not a page's document.\n */\n load?: AiLoadEstimate | null\n /**\n * For a `draft` output (AGL-3616): the resource whose owner wrote it, as\n * its writer is registered on the resource-draft seam.\n */\n draftResource?: string\n}\n\nexport interface AiJobLease {\n /** The process that holds the step: a route request id or a beat id. */\n owner: string\n until: ITimestamp\n}\n\n/** Why a job stopped for a person (AGL-2935). */\nexport type AiJobReviewReason =\n /** The plan step proposed a plan; generation waits for it to be confirmed. */\n | 'plan'\n /** An answer broke a building rule, and so did the answer to the re-ask. */\n | 'doctrine'\n /**\n * The site is at an allowance its plan sets for what the step writes, such\n * as its shared layouts or its templates (AGL-2909). Freeing one, or a plan\n * that holds more, is the member's decision; trying again runs the step\n * again.\n */\n | 'limit'\n\n/**\n * A building rule an answer broke, as the job keeps it: the number, the code,\n * the sentence, and where in the answer it was broken (AGL-3078).\n */\nexport interface AiJobRuleFinding {\n rule: number | null\n code: string\n /** Customer-safe. */\n message: string\n /** The nodes the finding names, by the ids the refused answer gave them. Absent when it names none. */\n nodeIds?: string[]\n /** The plan entries or answer fields it names, as `screens[0].sections[2]`. Absent when it names none. */\n paths?: string[]\n}\n\n/**\n * One node of the parts of a refused answer its findings name (AGL-3078), as\n * the review keeps it: the node's place, its element and the names of what it\n * sets, and never its copy. A Grid keeps the values of its layout props as\n * they were written, so a review can tell a shape the rules refuse from one\n * the model was asked to mend and did not.\n */\nexport interface AiJobReviewOutlineNode {\n /** The node's id, as the refused answer wrote it. */\n id: string\n /** How far below a node a finding names it sits: 0 for that node. */\n depth: number\n componentId: string\n /** The props it sets, by name only. */\n props: string[]\n /** The `sx` keys it sets, by name only; absent when it sets none. */\n sx?: string[]\n /** A Grid's layout props, `container`, `size`, `direction`, `spacing` and the rest, as written. */\n grid?: Record<string, string | number | boolean | null>\n /** Its children's element ids, in order. */\n children: string[]\n}\n\nexport interface AiJobReview {\n reason: AiJobReviewReason\n /** Customer-safe: what the person is asked to decide. */\n message: string\n /** The rules the last answer broke; empty for a plan. */\n findings: AiJobRuleFinding[]\n /**\n * The parts of the refused answer its findings name, each node with the\n * nodes below it (AGL-3078); absent when they name none. Kept with the job\n * for diagnosis, and shown to staff.\n */\n outline?: AiJobReviewOutlineNode[]\n /**\n * The checks' own sentence, where `message` was written for the member\n * instead (AGL-3594): rule numbers and all, for staff. Its presence is what\n * tells the drawer the findings are staff reading too.\n */\n detail?: string\n /**\n * Why trying again cannot work yet (AGL-3594) — the Free allowance left is\n * less than the step can cost — or absent when it can. The button is drawn\n * disabled with this beside it.\n */\n retryRefusal?: string\n /**\n * What a Free workspace had left when a build's plan was kept (AGL-3722),\n * so its plan card says, before Confirm, whether the build fits and offers\n * the go-ahead or a smaller first build when it does not. The confirm door\n * reads what is left again; this is what the card shows.\n */\n freeCredits?: AiFreeCreditsLeft\n}\n\nexport type AiJobPlanStatus = 'proposed' | 'confirmed'\n\n/** The plan a job builds from, as the job keeps it once its plan step proposed it. */\nexport interface AiJobPlan extends AiBuildPlan {\n status: AiJobPlanStatus\n /** Names for the inventory ids the plan references, read when it was made. */\n labels: Record<string, string>\n proposedAt: ITimestamp\n confirmedAt: ITimestamp | null\n /** The member who confirmed it. */\n confirmedBy: string | null\n /**\n * A digest of the whole request that produced this plan (AGL-2937): the\n * job's kind, site, brief and scalar inputs, the model that answered, and\n * the prompt as it was rendered — the site inventory included. A later job\n * whose request hashes the same reuses this plan rather than paying for the\n * same answer again. A hash of the request, never of the answer, and no\n * part of the brief is recoverable from it.\n */\n key?: string\n /** The job this plan was reused from, when it was not asked for again. */\n reusedFrom?: string\n}\n\n/**\n * Where one item of a `build` job stands (AGL-3616). A build settles item by\n * item: a failed item gives back its own spend and the rest of the plan still\n * runs, so the job keeps one row per item, written only by the machine's\n * `recordStep` — in the same transaction as the spend it accounts for — and\n * by the resume door's retry.\n */\nexport type AiJobItemStatus = 'pending' | 'running' | 'succeeded' | 'degraded' | 'failed' | 'skipped'\n\n/** Why an item failed: a give-back reason when it was ours, else the model's refusal or a review. */\nexport type AiJobItemFailureReason =\n | 'plan-refused'\n | 'doctrine-refused'\n | 'provider'\n | 'timeout'\n | 'step-failure'\n | 'unavailable'\n | 'refused'\n | 'review'\n\nexport interface AiJobItemFailure {\n /** Whether the failure was on our side: only then is the item's spend given back. */\n ours: boolean\n reason: AiJobItemFailureReason\n /** Customer-safe. */\n message: string\n /**\n * The checks' own sentence behind a `doctrine-refused` failure (AGL-3596),\n * rule numbers and all: kept on the job for staff and debugging, never on\n * the wire a customer reads.\n */\n detail?: string\n}\n\nexport interface AiJobItemLedger {\n /** The unit's slot within the plan: `c0` a creation, `p0` a page, `i0` an item. */\n slot: string\n /** The capability operation that builds it: `page`, `form`, or an owner's op. */\n op: string\n /** What a person reads: the page's title, the creation's or item's name. */\n label: string\n status: AiJobItemStatus\n /** Bumped by Try again; the refund key names it, so each attempt gives back once. */\n attempt: number\n /** What this item's passes cost, every attempt. */\n creditsSpent: number\n /** What this attempt's passes cost so far: what a failure of this attempt gives back. */\n attemptCredits?: number\n /** What was given back for this item's failures. */\n creditsRefunded: number\n /** The key the latest give-back for this item was made under. */\n refundKey?: string\n failure?: AiJobItemFailure | null\n /** The ids of the outputs this item produced. */\n outputs: string[]\n /** The slots whose failure changed how this item was built. */\n degradedBy?: string[]\n /** Customer-safe: what the person should know about this item. */\n note?: string | null\n /**\n * When this attempt settled, as an ISO string (AGL-3596): the moment the\n * next item started, which is what its progress row counts from. Absent\n * while the item is open, and on a ledger written before it was kept.\n */\n settledAt?: string | null\n}\n\n/** Whether a build's own planning was charged or given back (AGL-3616). */\nexport interface AiJobOrchestration {\n /** What the plan step spent. */\n creditsSpent: number\n /** `charged` once anything was delivered; `refunded` when nothing was. */\n settled: 'pending' | 'charged' | 'refunded'\n}\n\nexport interface AiJob {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n status: AiJobStatus\n /** The customer's brief, verbatim. Expires with the document. */\n brief: string\n /** Kind-specific inputs the runner reads (a screen id to edit, a tone). */\n inputs: Record<string, unknown>\n /**\n * The model the creator picked for the job's steps (AGL-2942), or `null`\n * for Auto. Each step honors it only where the plan and the allotment\n * allowlists allow it, and runs on the routing table otherwise.\n */\n model?: string | null\n steps: AiJobStep[]\n outputs: AiJobOutput[]\n /**\n * The nominal credit figure held against the job while steps are\n * outstanding. The meter's real bound is the per-step message reservation\n * and the org's monthly ceiling; this is the number the console shows so\n * a running job is not read as free.\n */\n creditsReserved: number\n creditsSpent: number\n /**\n * Credits the job gave back because it failed on our side (AGL-3594),\n * summed over its give-backs; absent when it gave none. `creditsSpent`\n * stays what ran, so what the job cost is the one less the other.\n */\n refundedCredits?: number\n /** Why the latest give-back was made (`AiJobRefundReason`). */\n refundReason?: string\n /** How many give-backs the job has had: the next one's key. */\n refunds?: number\n createdBy: string\n createdAt: ITimestamp\n updatedAt: ITimestamp\n /** TTL — the brief is verbatim customer text and expires like an exchange. */\n expiresAt: ITimestamp\n /** Customer-safe only. */\n error?: string | null\n lease?: AiJobLease | null\n /** The plan the job builds from, once its plan step proposed one. */\n plan?: AiJobPlan | null\n /** What the person is asked to decide while the job is `needs_review`. */\n review?: AiJobReview | null\n /**\n * What a person applied from the job's outputs (AGL-2910). Written by the\n * door that applied them, never by a step runner, and absent until then.\n */\n applied?: AiJobApplied | null\n /**\n * What a guided site start put live when it finished (AGL-3596): written\n * once by the site step's last pass, never by a person. Absent on every\n * other job, which leaves its pages as drafts.\n */\n sitePublish?: AiJobSitePublish | null\n /** A `build` job's items, one row each (AGL-3616); absent on every other kind. */\n items?: AiJobItemLedger[] | null\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration | null\n /**\n * A Free job started past what was left (AGL-3722): the person chose to\n * build what fits, knowing it pauses where the credits run out. Who, when,\n * what was left and the range they were shown; absent on every job that fit.\n */\n creditsConfirmed?: AiJobCreditsConfirmation | null\n}\n\n/**\n * A guided site start's publish (AGL-3596): the pages that went live, with\n * the address each answers, and the pages that stayed drafts, each with the\n * plain sentence for why. A page that stayed a draft never fails the job.\n */\nexport interface AiJobSitePublish {\n /** The site's live address, `https://<subdomain>.aglyn.app/`; `null` when the site has no subdomain. */\n liveUrl: string | null\n published: Array<{ id: string; label: string; path: string }>\n drafts: Array<{ id: string; label: string; reason: string }>\n}\n\n/**\n * The record of an apply (AGL-2910): what became a new unpublished version,\n * and what waits in its editor for a person to save.\n */\nexport interface AiJobApplied {\n at: ITimestamp\n /** The member who applied. */\n by: string\n /** The new version opened for each resource, by resource id. */\n versions: Record<string, string>\n /** Resource ids whose proposed values wait in their editor, unsaved. */\n staged: string[]\n}\n\n/** The plan on the wire: every instant an ISO string. */\nexport interface AiJobPlanSummary extends AiBuildPlan {\n status: AiJobPlanStatus\n labels: Record<string, string>\n proposedAt: string | null\n confirmedAt: string | null\n confirmedBy: string | null\n key?: string\n reusedFrom?: string\n}\n\n/**\n * The wire form the console reads: every instant an ISO string, the lease\n * reduced to whether a runner holds the job. The brief and the outputs\n * travel as they are — the reader is a member of the org that wrote them.\n */\nexport interface AiJobSummary {\n id: string\n orgId: string\n hostId: string | null\n kind: AiJobKind\n status: AiJobStatus\n brief: string\n /**\n * The batch a door created this job as one of (AGL-2911), read off the\n * job's inputs; `null` for a job created on its own. The console groups a\n * run of jobs by it, which is the one thing it cannot do from a list whose\n * rows carry no inputs — and the only input the wire form carries, because\n * a batch id names nothing the member wrote.\n */\n batch: string | null\n steps: Array<{\n name: string\n status: AiJobStepStatus\n startedAt: string | null\n endedAt: string | null\n creditsSpent: number\n error: string | null\n }>\n outputs: AiJobOutput[]\n creditsReserved: number\n creditsSpent: number\n /** Credits given back for a failure on our side (AGL-3594); 0 when none. */\n refundedCredits?: number\n createdBy: string\n createdAt: string\n updatedAt: string\n error: string | null\n running: boolean\n plan: AiJobPlanSummary | null\n review: AiJobReview | null\n /** What a person applied from the outputs (AGL-2910); instants as ISO strings. */\n applied?: {\n at: string | null\n by: string\n versions: Record<string, string>\n staged: string[]\n } | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: AiJobSitePublish | null\n /**\n * A site job's answers (AGL-3596): the guided start's questions as the person\n * answered them, so a failed start's Try again reopens them filled in. The\n * scalar site inputs only, never anything else a job's inputs carry; absent\n * on every other kind.\n */\n siteInputs?: Record<string, unknown> | null\n /**\n * Whether the machine confirms this job's plan itself (AGL-3594): a guided\n * site start. Its plan parks for the instant the confirmation takes, and a\n * surface reads that park as the build going on (AGL-3596). Absent on every\n * other job.\n */\n autoConfirm?: boolean\n /** A `build` job's items (AGL-3616); absent on every other job. */\n items?: AiJobItemLedger[]\n /**\n * Whether a build's request asked to publish (AGL-3616), so its plan card\n * offers the box that confirms it. Absent on every other job.\n */\n publishAsked?: boolean\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration\n}\n"],"names":["AI_JOB_KINDS","AI_JOB_TERMINAL_STATUSES","AI_JOB_PLAN_STEP"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAmED,qEAAqE,GACrE,OAAO,MAAMA,eAAqC;IAChD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAoBD,OAAO,MAAMC,2BAAmD;IAC9D;IACA;IACA;CACD,CAAA;AAID,uFAAuF,GACvF,OAAO,MAAMC,mBAAmB,OAAM"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-jobs.types.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 { ITimestamp } from '@aglyn/shared-util-timestamp'\nimport type { AiEffort } from '../providers/contract'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiBuildPlan } from './ai-build-plan'\nimport type { AiFreeCreditsLeft } from './ai-site-job'\nimport type { AiJobCreditsConfirmation } from './ai-credit-estimate'\n\n/**\n * AI generation jobs (AGL-2904).\n *\n * A job is one customer brief carried through one or more model steps by a\n * Firestore state machine, so that work longer than a request can survive\n * the request: the console route runs what it can inline, and the platform\n * job beat resumes whatever is left. The document is the whole state — no\n * process holds anything the next beat cannot read back.\n *\n * ── Outputs are drafts, never a publish ──────────────────────────────────\n *\n * Every output a job produces is a NEW draft or a new unpublished version of\n * an existing resource. A job never flips a version pointer, never registers\n * a route and never sends anything; a person opens the draft the job names\n * and publishes it through the door that already exists for that resource.\n * That is what lets a job run with no visitor, no session and no confirm\n * gate — the thing it writes cannot be seen by anyone but the workspace\n * until a member chooses otherwise.\n *\n * ── The lease ────────────────────────────────────────────────────────────\n *\n * A step runs under a lease: an owner id and an `until` instant. Two beats\n * fire a minute apart against a 120 s function timeout, and a console route\n * may still be running the first step when the first beat looks, so the\n * lease is what stops the same step being run — and metered — twice. A\n * claim is a transaction that refuses while another owner's lease is live;\n * an expired lease is simply claimable, which is also how a step abandoned\n * by a frozen process is recovered.\n */\n\n/**\n * What a job produces. `text` and `theme` have runners; every other kind is\n * registered and refuses with \"not available yet\" until its own issue lands,\n * so a kind's presence in the union says the job model accepts it, not that\n * a runner exists.\n */\nexport type AiJobKind =\n | 'page'\n | 'component'\n | 'layout'\n | 'template'\n | 'edit'\n | 'seo'\n | 'site'\n | 'email'\n | 'campaign'\n | 'form'\n | 'experiment'\n | 'insight'\n | 'products'\n | 'crm'\n | 'onboarding'\n | 'workflow'\n | 'logic'\n | 'text'\n | 'theme'\n | 'build'\n\n/** The union as a value, so a route can validate a body against it. */\nexport const AI_JOB_KINDS: readonly AiJobKind[] = [\n 'page',\n 'component',\n 'layout',\n 'template',\n 'edit',\n 'seo',\n 'site',\n 'email',\n 'campaign',\n 'form',\n 'experiment',\n 'insight',\n 'products',\n 'crm',\n 'onboarding',\n 'workflow',\n 'logic',\n 'text',\n 'theme',\n 'build',\n]\n\n/**\n * `queued` — waiting for a runner; `running` — a step holds the lease;\n * `needs_input` — the job stopped for something only the workspace can\n * change (credits, a cap, a switch), and the beat tries it again once that\n * may have changed; `needs_review` — the job stopped for a person's decision\n * (AGL-2935): its plan is ready to confirm, or an answer broke a building\n * rule on its re-ask. No timer can make that decision, so neither the beat\n * nor anything but that person resumes it. The last three are terminal.\n */\nexport type AiJobStatus =\n | 'queued'\n | 'running'\n | 'needs_input'\n | 'needs_review'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport const AI_JOB_TERMINAL_STATUSES: readonly AiJobStatus[] = [\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport type AiJobStepStatus = 'pending' | 'running' | 'done' | 'failed'\n\n/** The step a planned kind runs first (AGL-2935): its plan, before anything is built. */\nexport const AI_JOB_PLAN_STEP = 'plan'\n\nexport interface AiJobStep {\n /** Stable within the job (`plan`, `draft`, `generate`), unique per job. */\n name: string\n status: AiJobStepStatus\n startedAt?: ITimestamp | null\n endedAt?: ITimestamp | null\n /** Credits this step has actually cost, at the plan's credit rate. */\n creditsSpent: number\n /** Customer-safe only; provider detail goes to the server log. */\n error?: string | null\n /**\n * How many times a runner has been handed this step. A retryable provider\n * failure hands the step back as `pending`; the count is what stops a\n * provider outage re-running a step forever.\n */\n attempts?: number\n /**\n * Passes of a step that works through a list (AGL-2910): each one a\n * recorded, metered exchange that asked to continue. Attempts count again\n * from zero after a pass; this count is what bounds the step.\n */\n passes?: number\n /**\n * What the step's model calls cost in tokens and time (AGL-2937), summed\n * over every run that reached the provider. Absent on a step none has.\n */\n tokens?: AiJobStepTokens\n /**\n * The ids the step writes its drafts under, by draft (AGL-3079): minted\n * when the job is created, so every run of the step names the same drafts.\n * Absent on a step that writes none; a step that records none names its\n * drafts by the job's own id.\n */\n draftIds?: Partial<Record<AiJobDraftSlot, string>>\n}\n\n/**\n * A draft a job's kind always writes (AGL-3079): a screen, layout, template,\n * form or component; an email design; a campaign; an automation.\n */\nexport type AiJobDraftSlot =\n | 'screen'\n | 'layout'\n | 'template'\n | 'form'\n | 'component'\n | 'email'\n | 'campaign'\n | 'workflow'\n\n/**\n * A step's measure (AGL-2937): the four token counts the meter prices, the\n * time spent in the runner, and the model and thinking effort of the last\n * run. The machine writes it beside `creditsSpent`, so what a kind of step\n * costs in tokens is read off the job rather than inferred from the month.\n */\nexport interface AiJobStepTokens {\n /** Prompt tokens sent uncached. */\n input: number\n /** Prompt tokens read from the prompt cache. */\n cachedRead: number\n /** Prompt tokens written to the prompt cache. */\n cacheWrite: number\n /** Tokens generated, thinking included. */\n output: number\n /** The model the last run was served by. */\n model: string | null\n /** The thinking effort the last run asked for; `null` when it named none. */\n effort: AiEffort | null\n /** Milliseconds spent in the runner, summed over runs. */\n latencyMs: number\n /** Runs that reached the provider. */\n runs: number\n /**\n * Why the step's latest runs stopped (AGL-3042), oldest first and the last\n * run last: each run's stop reason and the tokens it generated, so a pass\n * cut off at its ceiling reads off the job rather than out of the sums. At\n * most `AI_JOB_STEP_LAST_RUNS` are kept; absent on a step measured before.\n */\n lastRuns?: AiJobStepRunStop[]\n}\n\n/** One run of a step, by why it stopped (AGL-3042). */\nexport interface AiJobStepRunStop {\n /**\n * The stop reason of the run's last model call — `tool_use` or `end_turn`\n * for an answer, `max_tokens` for one cut off at its ceiling, `refusal` —\n * and `null` when the provider named none.\n */\n stopReason: string | null\n /** Tokens the run generated, over every model call it made. */\n output: number\n}\n\nexport type AiJobOutputResource =\n | 'screen'\n | 'reusableComponent'\n | 'layout'\n | 'template'\n | 'form'\n | 'emailScreen'\n | 'campaign'\n | 'product'\n | 'experiment'\n | 'workflow'\n | 'text'\n | 'theme'\n | 'seo'\n | 'insight'\n | 'crm'\n /** A draft another plugin wrote for a build (AGL-3616), named by `draftResource`. */\n | 'draft'\n | 'logic'\n | 'orgAutomation'\n /** A post a guided start wrote into the site's blog (AGL-3676): a content collection entry. */\n | 'entry'\n\n/**\n * One thing a job wrote. Addressed by resource and id so the console can\n * build the \"open draft\" link without knowing what the runner did; `text`\n * has no document of its own, so its body rides on the output itself.\n *\n * A `theme` output is a proposal rather than a document (AGL-2938). A site's\n * theme is fields on its host document, and writing those fields is the save\n * the Theme section's editor performs, which a job never does. So the change\n * set rides on the output as `proposal`, and a person puts it in the editor\n * and saves it there, or does not.\n *\n * An `seo` output is a proposal rather than a document (AGL-2910): search\n * listing values for a page or a product, or a site audit's findings and\n * fixes. A screen's listing is served from the screen document itself, so a\n * job never writes it; the values ride on the output as `proposal`, a person\n * puts them in the editor and saves them there, and an audit's content fixes\n * become new versions only when a person applies them.\n *\n * A `crm` output is a proposal too (AGL-2917): a record's summary and next\n * step, an email draft, or an import's column matches. The CRM's own task\n * form, stage route, composer and import drawer are the writes, and a person\n * makes each of them, or does not.\n *\n * A `logic` output is a proposal too (AGL-3603): a site function or variable\n * the Functions & Variables editor opens unsaved. Saving it is the editor's\n * write, which a person makes, or does not.\n *\n * An `orgAutomation` output is a proposal too (AGL-3603): one of the\n * workspace's automations, which the Org automations editor opens unsaved and\n * switched off. `hostId` is `null`: it belongs to no one site.\n */\nexport interface AiJobOutput {\n resource: AiJobOutputResource\n id: string\n /** The unpublished version the job created, where the resource has versions. */\n versionId?: string | null\n /** `null` for an output that belongs to the org rather than one site. */\n hostId: string | null\n /**\n * The site's subdomain, which is what a console URL names a site by: the\n * `[host]` segment resolves by subdomain, so a link built from `hostId`\n * names no site the console can open.\n */\n hostSubdomain?: string | null\n label: string\n /** The body of a `text` output. Absent on every other resource. */\n text?: string\n /**\n * The change set of an output a person applies rather than opens — a\n * `theme` or `seo` proposal — in the shape its job kind's runner defines.\n */\n proposal?: Record<string, unknown>\n /**\n * What the person decides next about this output, in customer-safe words:\n * a routing the draft could not store, or part of the brief it could not\n * build. Absent when there is nothing to decide.\n */\n note?: string | null\n /**\n * What a first visit is estimated to transfer, as the doctrine measured\n * the generated document (AGL-2935) — the weight the proposal shows before\n * anything is applied. Absent where the output is not a page's document.\n */\n load?: AiLoadEstimate | null\n /**\n * For a `draft` output (AGL-3616): the resource whose owner wrote it, as\n * its writer is registered on the resource-draft seam.\n */\n draftResource?: string\n}\n\nexport interface AiJobLease {\n /** The process that holds the step: a route request id or a beat id. */\n owner: string\n until: ITimestamp\n}\n\n/** Why a job stopped for a person (AGL-2935). */\nexport type AiJobReviewReason =\n /** The plan step proposed a plan; generation waits for it to be confirmed. */\n | 'plan'\n /** An answer broke a building rule, and so did the answer to the re-ask. */\n | 'doctrine'\n /**\n * The site is at an allowance its plan sets for what the step writes, such\n * as its shared layouts or its templates (AGL-2909). Freeing one, or a plan\n * that holds more, is the member's decision; trying again runs the step\n * again.\n */\n | 'limit'\n\n/**\n * A building rule an answer broke, as the job keeps it: the number, the code,\n * the sentence, and where in the answer it was broken (AGL-3078).\n */\nexport interface AiJobRuleFinding {\n rule: number | null\n code: string\n /** Customer-safe. */\n message: string\n /** The nodes the finding names, by the ids the refused answer gave them. Absent when it names none. */\n nodeIds?: string[]\n /** The plan entries or answer fields it names, as `screens[0].sections[2]`. Absent when it names none. */\n paths?: string[]\n}\n\n/**\n * One node of the parts of a refused answer its findings name (AGL-3078), as\n * the review keeps it: the node's place, its element and the names of what it\n * sets, and never its copy. A Grid keeps the values of its layout props as\n * they were written, so a review can tell a shape the rules refuse from one\n * the model was asked to mend and did not.\n */\nexport interface AiJobReviewOutlineNode {\n /** The node's id, as the refused answer wrote it. */\n id: string\n /** How far below a node a finding names it sits: 0 for that node. */\n depth: number\n componentId: string\n /** The props it sets, by name only. */\n props: string[]\n /** The `sx` keys it sets, by name only; absent when it sets none. */\n sx?: string[]\n /** A Grid's layout props, `container`, `size`, `direction`, `spacing` and the rest, as written. */\n grid?: Record<string, string | number | boolean | null>\n /** Its children's element ids, in order. */\n children: string[]\n}\n\nexport interface AiJobReview {\n reason: AiJobReviewReason\n /** Customer-safe: what the person is asked to decide. */\n message: string\n /** The rules the last answer broke; empty for a plan. */\n findings: AiJobRuleFinding[]\n /**\n * The parts of the refused answer its findings name, each node with the\n * nodes below it (AGL-3078); absent when they name none. Kept with the job\n * for diagnosis, and shown to staff.\n */\n outline?: AiJobReviewOutlineNode[]\n /**\n * The checks' own sentence, where `message` was written for the member\n * instead (AGL-3594): rule numbers and all, for staff. Its presence is what\n * tells the drawer the findings are staff reading too.\n */\n detail?: string\n /**\n * Why trying again cannot work yet (AGL-3594) — the Free allowance left is\n * less than the step can cost — or absent when it can. The button is drawn\n * disabled with this beside it.\n */\n retryRefusal?: string\n /**\n * What a Free workspace had left when a build's plan was kept (AGL-3722),\n * so its plan card says, before Confirm, whether the build fits and offers\n * the go-ahead or a smaller first build when it does not. The confirm door\n * reads what is left again; this is what the card shows.\n */\n freeCredits?: AiFreeCreditsLeft\n}\n\nexport type AiJobPlanStatus = 'proposed' | 'confirmed'\n\n/** The plan a job builds from, as the job keeps it once its plan step proposed it. */\nexport interface AiJobPlan extends AiBuildPlan {\n status: AiJobPlanStatus\n /** Names for the inventory ids the plan references, read when it was made. */\n labels: Record<string, string>\n proposedAt: ITimestamp\n confirmedAt: ITimestamp | null\n /** The member who confirmed it. */\n confirmedBy: string | null\n /**\n * A digest of the whole request that produced this plan (AGL-2937): the\n * job's kind, site, brief and scalar inputs, the model that answered, and\n * the prompt as it was rendered — the site inventory included. A later job\n * whose request hashes the same reuses this plan rather than paying for the\n * same answer again. A hash of the request, never of the answer, and no\n * part of the brief is recoverable from it.\n */\n key?: string\n /** The job this plan was reused from, when it was not asked for again. */\n reusedFrom?: string\n}\n\n/**\n * Where one item of a `build` job stands (AGL-3616). A build settles item by\n * item: a failed item gives back its own spend and the rest of the plan still\n * runs, so the job keeps one row per item, written only by the machine's\n * `recordStep` — in the same transaction as the spend it accounts for — and\n * by the resume door's retry.\n */\nexport type AiJobItemStatus = 'pending' | 'running' | 'succeeded' | 'degraded' | 'failed' | 'skipped'\n\n/** Why an item failed: a give-back reason when it was ours, else the model's refusal or a review. */\nexport type AiJobItemFailureReason =\n | 'plan-refused'\n | 'doctrine-refused'\n | 'provider'\n | 'timeout'\n | 'step-failure'\n | 'unavailable'\n | 'refused'\n | 'review'\n\nexport interface AiJobItemFailure {\n /** Whether the failure was on our side: only then is the item's spend given back. */\n ours: boolean\n reason: AiJobItemFailureReason\n /** Customer-safe. */\n message: string\n /**\n * The checks' own sentence behind a `doctrine-refused` failure (AGL-3596),\n * rule numbers and all: kept on the job for staff and debugging, never on\n * the wire a customer reads.\n */\n detail?: string\n}\n\nexport interface AiJobItemLedger {\n /** The unit's slot within the plan: `c0` a creation, `p0` a page, `i0` an item. */\n slot: string\n /** The capability operation that builds it: `page`, `form`, or an owner's op. */\n op: string\n /** What a person reads: the page's title, the creation's or item's name. */\n label: string\n status: AiJobItemStatus\n /** Bumped by Try again; the refund key names it, so each attempt gives back once. */\n attempt: number\n /** What this item's passes cost, every attempt. */\n creditsSpent: number\n /** What this attempt's passes cost so far: what a failure of this attempt gives back. */\n attemptCredits?: number\n /** What was given back for this item's failures. */\n creditsRefunded: number\n /** The key the latest give-back for this item was made under. */\n refundKey?: string\n failure?: AiJobItemFailure | null\n /** The ids of the outputs this item produced. */\n outputs: string[]\n /** The slots whose failure changed how this item was built. */\n degradedBy?: string[]\n /** Customer-safe: what the person should know about this item. */\n note?: string | null\n /**\n * When this attempt settled, as an ISO string (AGL-3596): the moment the\n * next item started, which is what its progress row counts from. Absent\n * while the item is open, and on a ledger written before it was kept.\n */\n settledAt?: string | null\n}\n\n/** Whether a build's own planning was charged or given back (AGL-3616). */\nexport interface AiJobOrchestration {\n /** What the plan step spent. */\n creditsSpent: number\n /** `charged` once anything was delivered; `refunded` when nothing was. */\n settled: 'pending' | 'charged' | 'refunded'\n}\n\nexport interface AiJob {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n status: AiJobStatus\n /** The customer's brief, verbatim. Expires with the document. */\n brief: string\n /** Kind-specific inputs the runner reads (a screen id to edit, a tone). */\n inputs: Record<string, unknown>\n /**\n * The model the creator picked for the job's steps (AGL-2942), or `null`\n * for Auto. Each step honors it only where the plan and the allotment\n * allowlists allow it, and runs on the routing table otherwise.\n */\n model?: string | null\n steps: AiJobStep[]\n outputs: AiJobOutput[]\n /**\n * The nominal credit figure held against the job while steps are\n * outstanding. The meter's real bound is the per-step message reservation\n * and the org's monthly ceiling; this is the number the console shows so\n * a running job is not read as free.\n */\n creditsReserved: number\n creditsSpent: number\n /**\n * Credits the job gave back because it failed on our side (AGL-3594),\n * summed over its give-backs; absent when it gave none. `creditsSpent`\n * stays what ran, so what the job cost is the one less the other.\n */\n refundedCredits?: number\n /** Why the latest give-back was made (`AiJobRefundReason`). */\n refundReason?: string\n /** How many give-backs the job has had: the next one's key. */\n refunds?: number\n createdBy: string\n createdAt: ITimestamp\n updatedAt: ITimestamp\n /** TTL — the brief is verbatim customer text and expires like an exchange. */\n expiresAt: ITimestamp\n /** Customer-safe only. */\n error?: string | null\n lease?: AiJobLease | null\n /** The plan the job builds from, once its plan step proposed one. */\n plan?: AiJobPlan | null\n /** What the person is asked to decide while the job is `needs_review`. */\n review?: AiJobReview | null\n /**\n * What a person applied from the job's outputs (AGL-2910). Written by the\n * door that applied them, never by a step runner, and absent until then.\n */\n applied?: AiJobApplied | null\n /**\n * What a guided site start put live when it finished (AGL-3596): written\n * once by the site step's last pass, never by a person. Absent on every\n * other job, which leaves its pages as drafts.\n */\n sitePublish?: AiJobSitePublish | null\n /** A `build` job's items, one row each (AGL-3616); absent on every other kind. */\n items?: AiJobItemLedger[] | null\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration | null\n /**\n * A Free job started past what was left (AGL-3722): the person chose to\n * build what fits, knowing it pauses where the credits run out. Who, when,\n * what was left and the range they were shown; absent on every job that fit.\n */\n creditsConfirmed?: AiJobCreditsConfirmation | null\n /**\n * A person asked to stop the job (AGL-3616): written only by the cancel\n * door, in the transaction that decides it. A job no step holds ends\n * `canceled` in that same write; a job whose step holds a live lease keeps\n * running until the step finishes or aborts, and the machine ends it\n * `canceled` at its next write. Absent on a job nobody canceled.\n */\n cancelRequested?: AiJobCancelRequest | null\n}\n\n/** Who asked to cancel a job, and when (AGL-3616). */\nexport interface AiJobCancelRequest {\n at: ITimestamp\n /** The member or staff account that asked; `null` when nobody was named. */\n by: string | null\n}\n\n/**\n * A guided site start's publish (AGL-3596): the pages that went live, with\n * the address each answers, and the pages that stayed drafts, each with the\n * plain sentence for why. A page that stayed a draft never fails the job.\n */\nexport interface AiJobSitePublish {\n /** The site's live address, `https://<subdomain>.aglyn.app/`; `null` when the site has no subdomain. */\n liveUrl: string | null\n published: Array<{ id: string; label: string; path: string }>\n drafts: Array<{ id: string; label: string; reason: string }>\n}\n\n/**\n * The record of an apply (AGL-2910): what became a new unpublished version,\n * and what waits in its editor for a person to save.\n */\nexport interface AiJobApplied {\n at: ITimestamp\n /** The member who applied. */\n by: string\n /** The new version opened for each resource, by resource id. */\n versions: Record<string, string>\n /** Resource ids whose proposed values wait in their editor, unsaved. */\n staged: string[]\n}\n\n/** The plan on the wire: every instant an ISO string. */\nexport interface AiJobPlanSummary extends AiBuildPlan {\n status: AiJobPlanStatus\n labels: Record<string, string>\n proposedAt: string | null\n confirmedAt: string | null\n confirmedBy: string | null\n key?: string\n reusedFrom?: string\n}\n\n/**\n * The wire form the console reads: every instant an ISO string, the lease\n * reduced to whether a runner holds the job. The brief and the outputs\n * travel as they are — the reader is a member of the org that wrote them.\n */\nexport interface AiJobSummary {\n id: string\n orgId: string\n hostId: string | null\n kind: AiJobKind\n status: AiJobStatus\n brief: string\n /**\n * The batch a door created this job as one of (AGL-2911), read off the\n * job's inputs; `null` for a job created on its own. The console groups a\n * run of jobs by it, which is the one thing it cannot do from a list whose\n * rows carry no inputs — and the only input the wire form carries, because\n * a batch id names nothing the member wrote.\n */\n batch: string | null\n steps: Array<{\n name: string\n status: AiJobStepStatus\n startedAt: string | null\n endedAt: string | null\n creditsSpent: number\n error: string | null\n }>\n outputs: AiJobOutput[]\n creditsReserved: number\n creditsSpent: number\n /** Credits given back for a failure on our side (AGL-3594); 0 when none. */\n refundedCredits?: number\n createdBy: string\n createdAt: string\n updatedAt: string\n error: string | null\n running: boolean\n plan: AiJobPlanSummary | null\n review: AiJobReview | null\n /** What a person applied from the outputs (AGL-2910); instants as ISO strings. */\n applied?: {\n at: string | null\n by: string\n versions: Record<string, string>\n staged: string[]\n } | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: AiJobSitePublish | null\n /**\n * A site job's answers (AGL-3596): the guided start's questions as the person\n * answered them, so a failed start's Try again reopens them filled in. The\n * scalar site inputs only, never anything else a job's inputs carry; absent\n * on every other kind.\n */\n siteInputs?: Record<string, unknown> | null\n /**\n * Whether the machine confirms this job's plan itself (AGL-3594): a guided\n * site start. Its plan parks for the instant the confirmation takes, and a\n * surface reads that park as the build going on (AGL-3596). Absent on every\n * other job.\n */\n autoConfirm?: boolean\n /** A `build` job's items (AGL-3616); absent on every other job. */\n items?: AiJobItemLedger[]\n /**\n * Whether a build's request asked to publish (AGL-3616), so its plan card\n * offers the box that confirms it. Absent on every other job.\n */\n publishAsked?: boolean\n /** A `build` job's planning spend and how it settled (AGL-3616). */\n orchestration?: AiJobOrchestration\n /**\n * A cancel was asked and the job has not ended yet (AGL-3616): the step in\n * flight is finishing or aborting, and nothing after it runs. Absent once\n * the job is terminal, and on every job nobody canceled.\n */\n cancelRequested?: boolean\n}\n"],"names":["AI_JOB_KINDS","AI_JOB_TERMINAL_STATUSES","AI_JOB_PLAN_STEP"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAmED,qEAAqE,GACrE,OAAO,MAAMA,eAAqC;IAChD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAoBD,OAAO,MAAMC,2BAAmD;IAC9D;IACA;IACA;CACD,CAAA;AAID,uFAAuF,GACvF,OAAO,MAAMC,mBAAmB,OAAM"}
|