@aglyn/plugins-ai 1.0.0-beta.195 → 1.0.0-beta.196

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.195",
3
+ "version": "1.0.0-beta.196",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.195",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.195",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.195",
31
- "@aglyn/shared-data-types": "1.0.0-beta.195",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.195",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.195",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.195",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.195",
36
- "@aglyn/shared-util-email": "1.0.0-beta.195",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.195",
38
- "@aglyn/shared-util-http": "1.0.0-beta.195",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.195",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.195",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.195",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.195",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.195",
28
+ "@aglyn/aglyn": "1.0.0-beta.196",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.196",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.196",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.196",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.196",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.196",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.196",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.196",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.196",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.196",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.196",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.196",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.196",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.196",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.196",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.196",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -45,9 +45,17 @@ export declare const AI_USAGE_BY_USER_RETENTION_MONTHS = 13;
45
45
  *
46
46
  * `assist` is a console chat turn; `element`, `blog` and `section` are the
47
47
  * besigner's three assist modes; the rest are generation job kinds, named
48
- * exactly as the job document names them so a reader can join the two.
48
+ * exactly as the job document names them so a reader can join the two — or
49
+ * a purpose another plugin named on the text-generation seam.
49
50
  */
50
- export type AiUsageKind = 'assist' | 'element' | 'blog' | 'section' | AiJobKind;
51
+ export type AiUsageKind = 'assist' | 'element' | 'blog' | 'section' | AiJobKind | PluginAiUsageKind;
52
+ /**
53
+ * A purpose another plugin named on the core's text-generation seam
54
+ * (AGL-3324): the plugin's id and a word, hyphenated — `outreach-curate` —
55
+ * so its spend sits in the same `byKind` map as a job kind and a reader can
56
+ * tell whose it was.
57
+ */
58
+ export type PluginAiUsageKind = `${string}-${string}`;
51
59
  /** One person's month, zero-filled — the shape every reader hands on. */
52
60
  export interface AiUsageByUserMonth {
53
61
  uid: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-usage-by-user.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 { AiJobKind } from './ai-jobs.types'\nimport { assistCreditsFromUsd } from '@aglyn/aglyn/app-utils/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the pure half.\n *\n * The org rollup at `assistUsage/{month}` says what a workspace spent on AI\n * and nothing about who spent it. This module is the shape and the arithmetic\n * of the second rollup — one document per person per month, under the org,\n * keyed by uid — so an agency or a multi-brand workspace can see which of its\n * people, on which site, is generating what. No prose, no PII beyond the uid\n * the document is keyed by.\n *\n * The write lives in the plugin's `usage/ai-usage-by-user.ts`, inside the\n * same batch as the org rollup. Everything here is dependency-free so the\n * plugin's tables and the doors that serve them read one definition of a\n * share, a month key and a retention window.\n */\n\n/** `orgs/{orgId}/{this}/{uid}/{months}/{YYYY-MM}` — the subcollection names. */\nexport const AI_USAGE_BY_USER_COLLECTION = 'aiUsageByUser'\nexport const AI_USAGE_BY_USER_MONTHS_COLLECTION = 'months'\n\n/**\n * How many months a person's usage is kept, counted from the month it\n * describes. Thirteen: a full year of month-over-month comparison for a\n * workspace admin, plus the month in progress. A TTL policy on the month\n * document's `expiresAt` enforces it — see `aiUsageByUserExpiry`.\n */\nexport const AI_USAGE_BY_USER_RETENTION_MONTHS = 13\n\n/**\n * What an AI request was FOR, as the per-user rollup buckets it.\n *\n * `assist` is a console chat turn; `element`, `blog` and `section` are the\n * besigner's three assist modes; the rest are generation job kinds, named\n * exactly as the job document names them so a reader can join the two.\n */\nexport type AiUsageKind = 'assist' | 'element' | 'blog' | 'section' | AiJobKind\n\n/** One person's month, zero-filled — the shape every reader hands on. */\nexport interface AiUsageByUserMonth {\n uid: string\n month: string\n /** Credits drawn, each request rounded up as the meter rounds. */\n credits: number\n /**\n * The measured provider spend behind `credits`. Server-side only: no\n * customer surface renders it, and `aiUsageShare` is how it becomes a\n * figure a customer may see.\n */\n estCostUsd: number\n requests: number\n refusals: number\n /** Credits by `AiUsageKind`. */\n byKind: Record<string, number>\n /** Credits by host id, for requests that named a site. */\n byHost: Record<string, number>\n}\n\nconst MONTH_KEY = /^\\d{4}-(0[1-9]|1[0-2])$/\n\n/** Whether `value` is a `YYYY-MM` month key. */\nexport function isAiUsageMonthKey(value: unknown): value is string {\n return typeof value === 'string' && MONTH_KEY.test(value)\n}\n\n/**\n * The month keys a reader may ask for, newest first: the month in progress\n * and the twelve before it — the retention window, so a picker built from\n * this list never names a month the TTL has reaped.\n */\nexport function aiUsageMonthKeys(\n now: Date = new Date(),\n count: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n): string[] {\n const keys: string[] = []\n for (let back = 0; back < count; back += 1) {\n keys.push(\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - back, 1))\n .toISOString()\n .slice(0, 7),\n )\n }\n return keys\n}\n\n/** Whether `month` is inside the window `aiUsageMonthKeys` describes. */\nexport function aiUsageMonthWithinRetention(\n month: string,\n now: Date = new Date(),\n): boolean {\n return isAiUsageMonthKey(month) && aiUsageMonthKeys(now).includes(month)\n}\n\n/**\n * When a month document expires: the first instant of the month that is\n * `AI_USAGE_BY_USER_RETENTION_MONTHS` after the one it describes has closed.\n *\n * A `Date`, not a number: the TTL policy keys on a Firestore Timestamp and\n * silently governs nothing when the field is a number. Deterministic per\n * month, so every increment into the document re-stamps the same value and\n * the policy never moves under a month that is still being written.\n */\nexport function aiUsageByUserExpiry(month: string): Date {\n if (!isAiUsageMonthKey(month)) {\n throw new Error(`not a month key: ${month}`)\n }\n const [year, monthOfYear] = month.split('-').map(Number)\n return new Date(\n Date.UTC(year, monthOfYear + AI_USAGE_BY_USER_RETENTION_MONTHS, 1),\n )\n}\n\n/**\n * The kind an assist request was, read off the route it arrived on — for the\n * meters that carry a route and no kind of their own. The three besigner\n * modes name themselves in the path; everything else is a chat turn.\n */\nexport function aiUsageKindFromRoute(route: string | null | undefined): AiUsageKind {\n const tail = String(route ?? '').split('/').filter(Boolean).pop() ?? ''\n if (tail === 'element' || tail === 'blog' || tail === 'section') return tail\n return 'assist'\n}\n\n/**\n * One person's share of the workspace's spend for the month, in `[0, 1]`.\n *\n * Measured on the dollar figures, never on credits: the org meter rounds the\n * month's spend up ONCE, while a person's credits round each request up, so\n * a roster's credits can sum to a little more than the pool they drew from.\n * The ratio of measured spends has no such drift. Clamped, because a month\n * document written a moment after the org rollup is still a fraction, not a\n * percentage over one hundred — and rounded to a basis point, which is finer\n * than any surface renders and coarser than floating-point noise.\n */\nexport function aiUsageShare(\n userEstCostUsd: number,\n orgEstCostUsd: number,\n): number {\n const user = Number(userEstCostUsd)\n const org = Number(orgEstCostUsd)\n if (!Number.isFinite(user) || user <= 0) return 0\n if (!Number.isFinite(org) || org <= 0) return 0\n return Math.min(1, Math.round((user / org) * 10_000) / 10_000)\n}\n\nconst finite = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0\n}\n\nconst creditMap = (raw: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n for (const [key, value] of Object.entries(\n (raw ?? {}) as Record<string, unknown>,\n )) {\n const credits = Math.floor(finite(value))\n if (credits > 0) out[key] = credits\n }\n return out\n}\n\n/**\n * A month document as a complete record. Absent and malformed fields read as\n * zero; a `credits` the writer never set is derived from the spend, so a\n * document written before the field existed still ranks.\n */\nexport function aiUsageByUserMonthFrom(\n raw: Record<string, unknown> | null | undefined,\n uid: string,\n month: string,\n): AiUsageByUserMonth {\n const estCostUsd = finite(raw?.['estCostUsd'])\n const stored = Math.floor(finite(raw?.['credits']))\n return {\n uid,\n month,\n credits: stored > 0 ? stored : assistCreditsFromUsd(estCostUsd),\n estCostUsd,\n requests: Math.floor(finite(raw?.['requests'])),\n refusals: Math.floor(finite(raw?.['refusals'])),\n byKind: creditMap(raw?.['byKind']),\n byHost: creditMap(raw?.['byHost']),\n }\n}\n\n/** The kinds a person drew the most credits on, dearest first. */\nexport function topAiUsageKinds(\n byKind: Record<string, number>,\n limit = 3,\n): Array<{ kind: string; credits: number }> {\n return Object.entries(byKind)\n .map(([kind, credits]) => ({ kind, credits }))\n .sort((a, b) => b.credits - a.credits || a.kind.localeCompare(b.kind))\n .slice(0, limit)\n}\n\n/** Two month documents for one person, summed — the roster column's figure. */\nexport function sumAiUsageCredits(months: readonly AiUsageByUserMonth[]): number {\n return months.reduce((sum, entry) => sum + entry.credits, 0)\n}\n"],"names":["assistCreditsFromUsd","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","MONTH_KEY","isAiUsageMonthKey","value","test","aiUsageMonthKeys","now","Date","count","keys","back","push","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiUsageMonthWithinRetention","month","includes","aiUsageByUserExpiry","Error","year","monthOfYear","split","map","Number","aiUsageKindFromRoute","route","String","tail","filter","Boolean","pop","aiUsageShare","userEstCostUsd","orgEstCostUsd","user","org","isFinite","Math","min","round","finite","parsed","creditMap","raw","out","key","Object","entries","credits","floor","aiUsageByUserMonthFrom","uid","estCostUsd","stored","requests","refusals","byKind","byHost","topAiUsageKinds","limit","kind","sort","a","b","localeCompare","sumAiUsageCredits","months","reduce","sum","entry"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-usage-by-user.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 { AiJobKind } from './ai-jobs.types'\nimport { assistCreditsFromUsd } from '@aglyn/aglyn/app-utils/assist-credits'\n\n/**\n * PER-USER AI USAGE (AGL-2928): the pure half.\n *\n * The org rollup at `assistUsage/{month}` says what a workspace spent on AI\n * and nothing about who spent it. This module is the shape and the arithmetic\n * of the second rollup — one document per person per month, under the org,\n * keyed by uid — so an agency or a multi-brand workspace can see which of its\n * people, on which site, is generating what. No prose, no PII beyond the uid\n * the document is keyed by.\n *\n * The write lives in the plugin's `usage/ai-usage-by-user.ts`, inside the\n * same batch as the org rollup. Everything here is dependency-free so the\n * plugin's tables and the doors that serve them read one definition of a\n * share, a month key and a retention window.\n */\n\n/** `orgs/{orgId}/{this}/{uid}/{months}/{YYYY-MM}` — the subcollection names. */\nexport const AI_USAGE_BY_USER_COLLECTION = 'aiUsageByUser'\nexport const AI_USAGE_BY_USER_MONTHS_COLLECTION = 'months'\n\n/**\n * How many months a person's usage is kept, counted from the month it\n * describes. Thirteen: a full year of month-over-month comparison for a\n * workspace admin, plus the month in progress. A TTL policy on the month\n * document's `expiresAt` enforces it — see `aiUsageByUserExpiry`.\n */\nexport const AI_USAGE_BY_USER_RETENTION_MONTHS = 13\n\n/**\n * What an AI request was FOR, as the per-user rollup buckets it.\n *\n * `assist` is a console chat turn; `element`, `blog` and `section` are the\n * besigner's three assist modes; the rest are generation job kinds, named\n * exactly as the job document names them so a reader can join the two — or\n * a purpose another plugin named on the text-generation seam.\n */\nexport type AiUsageKind =\n | 'assist'\n | 'element'\n | 'blog'\n | 'section'\n | AiJobKind\n | PluginAiUsageKind\n\n/**\n * A purpose another plugin named on the core's text-generation seam\n * (AGL-3324): the plugin's id and a word, hyphenated — `outreach-curate` —\n * so its spend sits in the same `byKind` map as a job kind and a reader can\n * tell whose it was.\n */\nexport type PluginAiUsageKind = `${string}-${string}`\n\n/** One person's month, zero-filled — the shape every reader hands on. */\nexport interface AiUsageByUserMonth {\n uid: string\n month: string\n /** Credits drawn, each request rounded up as the meter rounds. */\n credits: number\n /**\n * The measured provider spend behind `credits`. Server-side only: no\n * customer surface renders it, and `aiUsageShare` is how it becomes a\n * figure a customer may see.\n */\n estCostUsd: number\n requests: number\n refusals: number\n /** Credits by `AiUsageKind`. */\n byKind: Record<string, number>\n /** Credits by host id, for requests that named a site. */\n byHost: Record<string, number>\n}\n\nconst MONTH_KEY = /^\\d{4}-(0[1-9]|1[0-2])$/\n\n/** Whether `value` is a `YYYY-MM` month key. */\nexport function isAiUsageMonthKey(value: unknown): value is string {\n return typeof value === 'string' && MONTH_KEY.test(value)\n}\n\n/**\n * The month keys a reader may ask for, newest first: the month in progress\n * and the twelve before it — the retention window, so a picker built from\n * this list never names a month the TTL has reaped.\n */\nexport function aiUsageMonthKeys(\n now: Date = new Date(),\n count: number = AI_USAGE_BY_USER_RETENTION_MONTHS,\n): string[] {\n const keys: string[] = []\n for (let back = 0; back < count; back += 1) {\n keys.push(\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - back, 1))\n .toISOString()\n .slice(0, 7),\n )\n }\n return keys\n}\n\n/** Whether `month` is inside the window `aiUsageMonthKeys` describes. */\nexport function aiUsageMonthWithinRetention(\n month: string,\n now: Date = new Date(),\n): boolean {\n return isAiUsageMonthKey(month) && aiUsageMonthKeys(now).includes(month)\n}\n\n/**\n * When a month document expires: the first instant of the month that is\n * `AI_USAGE_BY_USER_RETENTION_MONTHS` after the one it describes has closed.\n *\n * A `Date`, not a number: the TTL policy keys on a Firestore Timestamp and\n * silently governs nothing when the field is a number. Deterministic per\n * month, so every increment into the document re-stamps the same value and\n * the policy never moves under a month that is still being written.\n */\nexport function aiUsageByUserExpiry(month: string): Date {\n if (!isAiUsageMonthKey(month)) {\n throw new Error(`not a month key: ${month}`)\n }\n const [year, monthOfYear] = month.split('-').map(Number)\n return new Date(\n Date.UTC(year, monthOfYear + AI_USAGE_BY_USER_RETENTION_MONTHS, 1),\n )\n}\n\n/**\n * The kind an assist request was, read off the route it arrived on — for the\n * meters that carry a route and no kind of their own. The three besigner\n * modes name themselves in the path; everything else is a chat turn.\n */\nexport function aiUsageKindFromRoute(route: string | null | undefined): AiUsageKind {\n const tail = String(route ?? '').split('/').filter(Boolean).pop() ?? ''\n if (tail === 'element' || tail === 'blog' || tail === 'section') return tail\n return 'assist'\n}\n\n/**\n * One person's share of the workspace's spend for the month, in `[0, 1]`.\n *\n * Measured on the dollar figures, never on credits: the org meter rounds the\n * month's spend up ONCE, while a person's credits round each request up, so\n * a roster's credits can sum to a little more than the pool they drew from.\n * The ratio of measured spends has no such drift. Clamped, because a month\n * document written a moment after the org rollup is still a fraction, not a\n * percentage over one hundred — and rounded to a basis point, which is finer\n * than any surface renders and coarser than floating-point noise.\n */\nexport function aiUsageShare(\n userEstCostUsd: number,\n orgEstCostUsd: number,\n): number {\n const user = Number(userEstCostUsd)\n const org = Number(orgEstCostUsd)\n if (!Number.isFinite(user) || user <= 0) return 0\n if (!Number.isFinite(org) || org <= 0) return 0\n return Math.min(1, Math.round((user / org) * 10_000) / 10_000)\n}\n\nconst finite = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 0\n}\n\nconst creditMap = (raw: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n for (const [key, value] of Object.entries(\n (raw ?? {}) as Record<string, unknown>,\n )) {\n const credits = Math.floor(finite(value))\n if (credits > 0) out[key] = credits\n }\n return out\n}\n\n/**\n * A month document as a complete record. Absent and malformed fields read as\n * zero; a `credits` the writer never set is derived from the spend, so a\n * document written before the field existed still ranks.\n */\nexport function aiUsageByUserMonthFrom(\n raw: Record<string, unknown> | null | undefined,\n uid: string,\n month: string,\n): AiUsageByUserMonth {\n const estCostUsd = finite(raw?.['estCostUsd'])\n const stored = Math.floor(finite(raw?.['credits']))\n return {\n uid,\n month,\n credits: stored > 0 ? stored : assistCreditsFromUsd(estCostUsd),\n estCostUsd,\n requests: Math.floor(finite(raw?.['requests'])),\n refusals: Math.floor(finite(raw?.['refusals'])),\n byKind: creditMap(raw?.['byKind']),\n byHost: creditMap(raw?.['byHost']),\n }\n}\n\n/** The kinds a person drew the most credits on, dearest first. */\nexport function topAiUsageKinds(\n byKind: Record<string, number>,\n limit = 3,\n): Array<{ kind: string; credits: number }> {\n return Object.entries(byKind)\n .map(([kind, credits]) => ({ kind, credits }))\n .sort((a, b) => b.credits - a.credits || a.kind.localeCompare(b.kind))\n .slice(0, limit)\n}\n\n/** Two month documents for one person, summed — the roster column's figure. */\nexport function sumAiUsageCredits(months: readonly AiUsageByUserMonth[]): number {\n return months.reduce((sum, entry) => sum + entry.credits, 0)\n}\n"],"names":["assistCreditsFromUsd","AI_USAGE_BY_USER_COLLECTION","AI_USAGE_BY_USER_MONTHS_COLLECTION","AI_USAGE_BY_USER_RETENTION_MONTHS","MONTH_KEY","isAiUsageMonthKey","value","test","aiUsageMonthKeys","now","Date","count","keys","back","push","UTC","getUTCFullYear","getUTCMonth","toISOString","slice","aiUsageMonthWithinRetention","month","includes","aiUsageByUserExpiry","Error","year","monthOfYear","split","map","Number","aiUsageKindFromRoute","route","String","tail","filter","Boolean","pop","aiUsageShare","userEstCostUsd","orgEstCostUsd","user","org","isFinite","Math","min","round","finite","parsed","creditMap","raw","out","key","Object","entries","credits","floor","aiUsageByUserMonthFrom","uid","estCostUsd","stored","requests","refusals","byKind","byHost","topAiUsageKinds","limit","kind","sort","a","b","localeCompare","sumAiUsageCredits","months","reduce","sum","entry"],"mappings":"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"}
@@ -0,0 +1,56 @@
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 PluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation';
18
+ import { type AiSystemBlock } from '../runtime/ai-runtime';
19
+ /**
20
+ * THIS PLUGIN AS THE WORKSPACE'S TEXT GENERATOR (AGL-3324), on the core's
21
+ * text-generation seam.
22
+ *
23
+ * Another plugin in the process — Sequences drafting one person's email —
24
+ * asks for text without importing this plugin, and gets it under the rules
25
+ * the generation doors apply, in the order the gate ladder applies them:
26
+ *
27
+ * 404 the generative release flag is off for the org (staff preview through it)
28
+ * 403 the member's role lacks `ai.generate`, on the site named
29
+ * 403 the plan does not include generation
30
+ * 423 the feature is paused — the platform's kill switch, or the workspace's
31
+ * 4xx the site named has AI switched off
32
+ * --- the reservation, which fails CLOSED and refuses with the sentence
33
+ * every door gives — a spent allotment names who can raise it
34
+ *
35
+ * A request that got through is one provider call at the `job.text` route's
36
+ * model — the member's own pick never applies here, since the caller's UI
37
+ * has no model switch — metered on the org's month, the person's month and
38
+ * the purpose the caller named, exactly as a text job is. The provider is
39
+ * never reached without a reservation, and a call that never reached it
40
+ * hands the reservation back.
41
+ *
42
+ * The seam authenticates nobody: the caller has already verified who is
43
+ * asking and that the org is theirs, which is why the token verifier and the
44
+ * rate window are the caller's rungs, not these.
45
+ */
46
+ /** The rate the ledger files a seam call under, beside the doors' routes. */
47
+ export declare const AI_TEXT_GENERATION_ROUTE = "ai/text-generation";
48
+ /** The system block the seam sends: the caller's instructions, cached as a prefix. */
49
+ export declare function aiTextGenerationSystem(system: string): AiSystemBlock[];
50
+ /** The generator, with its reads named so a spec can hand in fakes. */
51
+ export declare function createAiTextGenerator(deps?: {
52
+ firestore(): FirebaseFirestore.Firestore;
53
+ now(): Date;
54
+ }): PluginTextGenerator;
55
+ /** Registers this plugin as the workspace's text generator; idempotent for this plugin. */
56
+ export declare function registerAiTextGenerator(generator?: PluginTextGenerator): void;
@@ -0,0 +1,239 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { normalizePluginTextGenerationPurpose, registerPluginTextGenerator } from "@aglyn/aglyn/plugin-manager/plugin-text-generation";
19
+ import { checkEntitlement } from "@aglyn/aglyn/server";
20
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
21
+ import { featureLockdownRefusal } from "@aglyn/tenant-data-admin/server/lockdown";
22
+ import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-admin/server/organizations";
23
+ import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
24
+ import { AI_PLUGIN_ID } from "../constants.js";
25
+ import { AI_JOB_TEXT_STEP_BUDGET } from "../jobs/ai-job-text-step.js";
26
+ import { aiJobRefusalText } from "../jobs/ai-jobs.js";
27
+ import { AI_OFF_FOR_SITE_COPY, isAiOffForSite } from "../model/ai-site-switch.js";
28
+ import { resolveAiModelChoice } from "../providers/model-choice.js";
29
+ import { AI_ROUTING_TABLE } from "../providers/routing.js";
30
+ import { aiProviderReady, runAiRequest } from "../runtime/ai-runtime.js";
31
+ import { recordUserAiRefusal } from "../usage/ai-usage-by-user.js";
32
+ import { recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
33
+ /**
34
+ * THIS PLUGIN AS THE WORKSPACE'S TEXT GENERATOR (AGL-3324), on the core's
35
+ * text-generation seam.
36
+ *
37
+ * Another plugin in the process — Sequences drafting one person's email —
38
+ * asks for text without importing this plugin, and gets it under the rules
39
+ * the generation doors apply, in the order the gate ladder applies them:
40
+ *
41
+ * 404 the generative release flag is off for the org (staff preview through it)
42
+ * 403 the member's role lacks `ai.generate`, on the site named
43
+ * 403 the plan does not include generation
44
+ * 423 the feature is paused — the platform's kill switch, or the workspace's
45
+ * 4xx the site named has AI switched off
46
+ * --- the reservation, which fails CLOSED and refuses with the sentence
47
+ * every door gives — a spent allotment names who can raise it
48
+ *
49
+ * A request that got through is one provider call at the `job.text` route's
50
+ * model — the member's own pick never applies here, since the caller's UI
51
+ * has no model switch — metered on the org's month, the person's month and
52
+ * the purpose the caller named, exactly as a text job is. The provider is
53
+ * never reached without a reservation, and a call that never reached it
54
+ * hands the reservation back.
55
+ *
56
+ * The seam authenticates nobody: the caller has already verified who is
57
+ * asking and that the org is theirs, which is why the token verifier and the
58
+ * rate window are the caller's rungs, not these.
59
+ */ /** The rate the ledger files a seam call under, beside the doors' routes. */ export const AI_TEXT_GENERATION_ROUTE = 'ai/text-generation';
60
+ /** The route's own answer ceiling, the same table row the text job reads. */ const TEXT_ROUTE = AI_ROUTING_TABLE['job.text'];
61
+ const refusal = (status, reason, error)=>({
62
+ ok: false,
63
+ status,
64
+ reason,
65
+ error
66
+ });
67
+ /** A `Response` a platform helper refused with, read into the seam's shape. */ async function refusedResponse(response, reason, fallback) {
68
+ const body = await response.json().catch(()=>null);
69
+ const error = typeof (body == null ? void 0 : body.error) === 'string' && body.error ? body.error : fallback;
70
+ return refusal(response.status, reason, error);
71
+ }
72
+ /** The system block the seam sends: the caller's instructions, cached as a prefix. */ export function aiTextGenerationSystem(system) {
73
+ return [
74
+ {
75
+ text: system,
76
+ cacheBreakpoint: true
77
+ }
78
+ ];
79
+ }
80
+ /** The generator, with its reads named so a spec can hand in fakes. */ export function createAiTextGenerator(deps = {
81
+ firestore: ()=>firebaseAdmin.app().firestore(),
82
+ now: ()=>new Date()
83
+ }) {
84
+ return {
85
+ async generate (request) {
86
+ var _request_now, _ref, _request_org, _ref1, _ref2;
87
+ var _reservation_allotment, _reservation_allotment1;
88
+ const orgId = request.orgId.trim();
89
+ const uid = request.uid.trim();
90
+ if (!orgId || !uid) return refusal(400, 'failed', 'Name the workspace and the member the text is for.');
91
+ const staff = request.staff === true;
92
+ const now = (_request_now = request.now) != null ? _request_now : deps.now();
93
+ // The flag closes the door, not just the UI: a released-off feature
94
+ // does not exist, and nothing below is disclosed while it is off.
95
+ if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', orgId)) {
96
+ return refusal(404, 'unavailable', 'AI drafting is not available to this workspace yet.');
97
+ }
98
+ if (!aiProviderReady()) {
99
+ return refusal(503, 'unavailable', 'AI drafting is temporarily unavailable.');
100
+ }
101
+ const resolved = await getOrgForUser(uid, orgId);
102
+ if (!resolved || resolved.orgId !== orgId) {
103
+ return refusal(403, 'permission', 'You are not a member of that organization.');
104
+ }
105
+ const org = (_ref = (_request_org = request.org) != null ? _request_org : resolved.org) != null ? _ref : {};
106
+ if (!staff && !await memberHasPermissionOnHost(orgId, request.hostId, resolved.member, 'ai.generate')) {
107
+ return refusal(403, 'permission', 'Your role does not include Generate with AI — ask an organization admin.');
108
+ }
109
+ if (!checkEntitlement(org, 'aiGenerative')) {
110
+ return refusal(403, 'entitlement', "This workspace's plan does not include AI drafting.");
111
+ }
112
+ const paused = await featureLockdownRefusal({
113
+ feature: 'ai-generate',
114
+ staff,
115
+ orgId,
116
+ nowMs: now.getTime()
117
+ });
118
+ if (paused) return refusedResponse(paused, 'off', 'AI drafting is paused.');
119
+ const firestore = deps.firestore();
120
+ if (await isAiOffForSite(firestore, org, request.hostId)) {
121
+ return refusal(403, 'off', AI_OFF_FOR_SITE_COPY);
122
+ }
123
+ // RESERVED before a token is spent, on the member's allotments for the
124
+ // site named, so N concurrent asks cannot all read "under the cap".
125
+ let reservation;
126
+ try {
127
+ var _request_hostId;
128
+ reservation = await reserveAssistMessage(firestore, orgId, true, now, org, {
129
+ uid,
130
+ hostId: (_request_hostId = request.hostId) != null ? _request_hostId : null
131
+ });
132
+ } catch (error) {
133
+ console.error('ai text-generation reservation failed', {
134
+ orgId,
135
+ error
136
+ });
137
+ return refusal(503, 'failed', 'AI drafting is temporarily unavailable.');
138
+ }
139
+ recordUserAiRefusal(firestore, orgId, uid, reservation);
140
+ if (!reservation.allowed) {
141
+ return refusal(reservation.refusedBy === 'cap' ? 402 : 429, 'quota', aiJobRefusalText(org, reservation));
142
+ }
143
+ const choice = resolveAiModelChoice('job.text', null, {
144
+ plan: resolveEffectivePlan(org),
145
+ allotmentModels: (_ref1 = (_reservation_allotment = reservation.allotment) == null ? void 0 : _reservation_allotment.models) != null ? _ref1 : null,
146
+ orgModels: (_ref2 = (_reservation_allotment1 = reservation.allotment) == null ? void 0 : _reservation_allotment1.orgModels) != null ? _ref2 : null
147
+ });
148
+ if (!choice) {
149
+ await releaseAssistMessage(firestore, orgId, reservation).catch(()=>undefined);
150
+ return refusal(503, 'unavailable', 'AI drafting is temporarily unavailable.');
151
+ }
152
+ const model = choice.model;
153
+ const ceiling = AI_JOB_TEXT_STEP_BUDGET.maxTokens(model);
154
+ const maxTokens = typeof request.maxTokens === 'number' && request.maxTokens > 0 ? Math.min(Math.floor(request.maxTokens), ceiling) : ceiling;
155
+ const purpose = normalizePluginTextGenerationPurpose(request.purpose);
156
+ // The ledger's kind for a plugin's purpose: hyphenated, so it never
157
+ // collides with a job kind or an assist mode.
158
+ const kind = purpose.includes('-') ? purpose : `plugin-${purpose}`;
159
+ let result;
160
+ try {
161
+ result = await runAiRequest(_extends({
162
+ model,
163
+ system: aiTextGenerationSystem(request.system),
164
+ messages: [
165
+ {
166
+ role: 'user',
167
+ content: request.prompt
168
+ }
169
+ ],
170
+ maxTokens
171
+ }, TEXT_ROUTE.thinking ? {
172
+ thinking: TEXT_ROUTE.thinking
173
+ } : {}, TEXT_ROUTE.effort ? {
174
+ effort: TEXT_ROUTE.effort
175
+ } : {}, {
176
+ stream: false
177
+ }, request.signal ? {
178
+ signal: request.signal
179
+ } : {}));
180
+ } catch (error) {
181
+ // The provider was never reached, or answered nothing: no usage came
182
+ // back, so nothing is metered and the message goes back to the org.
183
+ await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError)=>console.error('ai text-generation release failed', {
184
+ orgId,
185
+ releaseError
186
+ }));
187
+ console.error('ai text-generation failed', {
188
+ orgId,
189
+ purpose,
190
+ error
191
+ });
192
+ return refusal(503, 'failed', 'The draft could not be written just now. Try again.');
193
+ }
194
+ // Tokens were spent, so the bill is written first and its failure is a
195
+ // log line, never a refusal — the rule every door keeps.
196
+ try {
197
+ var _request_hostId1, _reservation_free;
198
+ await recordAssistCost(firestore, orgId, {
199
+ route: AI_TEXT_GENERATION_ROUTE,
200
+ hostId: (_request_hostId1 = request.hostId) != null ? _request_hostId1 : null,
201
+ model,
202
+ tier: 'entitled',
203
+ usage: result.usage,
204
+ docsPaths: [],
205
+ stopReason: result.stopReason,
206
+ deflected: false,
207
+ free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
208
+ uid,
209
+ kind
210
+ }, now);
211
+ } catch (error) {
212
+ console.error('ai text-generation cost record failed', {
213
+ orgId,
214
+ purpose,
215
+ error
216
+ });
217
+ }
218
+ if (result.kind === 'refusal') {
219
+ return refusal(422, 'refused', 'The model declined to write this draft.');
220
+ }
221
+ return {
222
+ ok: true,
223
+ text: result.text.trim(),
224
+ model,
225
+ usage: {
226
+ inputTokens: result.usage.inputTokens,
227
+ outputTokens: result.usage.outputTokens
228
+ }
229
+ };
230
+ }
231
+ };
232
+ }
233
+ /** Registers this plugin as the workspace's text generator; idempotent for this plugin. */ export function registerAiTextGenerator(generator = createAiTextGenerator()) {
234
+ registerPluginTextGenerator(generator, {
235
+ pluginId: AI_PLUGIN_ID
236
+ });
237
+ }
238
+
239
+ //# sourceMappingURL=plugin-text-generation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/plugin-text-generation.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 { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n normalizePluginTextGenerationPurpose,\n registerPluginTextGenerator,\n type PluginTextGenerationRefusal,\n type PluginTextGenerationRequest,\n type PluginTextGenerationResult,\n type PluginTextGenerator,\n} from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { featureLockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport {\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { AI_PLUGIN_ID } from '../constants'\nimport { AI_JOB_TEXT_STEP_BUDGET } from '../jobs/ai-job-text-step'\nimport { aiJobRefusalText } from '../jobs/ai-jobs'\nimport { AI_OFF_FOR_SITE_COPY, isAiOffForSite } from '../model/ai-site-switch'\nimport { resolveAiModelChoice } from '../providers/model-choice'\nimport { AI_ROUTING_TABLE } from '../providers/routing'\nimport { aiProviderReady, runAiRequest, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { PluginAiUsageKind } from '../model/ai-usage-by-user'\nimport { recordUserAiRefusal } from '../usage/ai-usage-by-user'\nimport {\n recordAssistCost,\n releaseAssistMessage,\n reserveAssistMessage,\n type AssistReservation,\n} from '../usage/assist-usage'\n\n/**\n * THIS PLUGIN AS THE WORKSPACE'S TEXT GENERATOR (AGL-3324), on the core's\n * text-generation seam.\n *\n * Another plugin in the process — Sequences drafting one person's email —\n * asks for text without importing this plugin, and gets it under the rules\n * the generation doors apply, in the order the gate ladder applies them:\n *\n * 404 the generative release flag is off for the org (staff preview through it)\n * 403 the member's role lacks `ai.generate`, on the site named\n * 403 the plan does not include generation\n * 423 the feature is paused — the platform's kill switch, or the workspace's\n * 4xx the site named has AI switched off\n * --- the reservation, which fails CLOSED and refuses with the sentence\n * every door gives — a spent allotment names who can raise it\n *\n * A request that got through is one provider call at the `job.text` route's\n * model — the member's own pick never applies here, since the caller's UI\n * has no model switch — metered on the org's month, the person's month and\n * the purpose the caller named, exactly as a text job is. The provider is\n * never reached without a reservation, and a call that never reached it\n * hands the reservation back.\n *\n * The seam authenticates nobody: the caller has already verified who is\n * asking and that the org is theirs, which is why the token verifier and the\n * rate window are the caller's rungs, not these.\n */\n\n/** The rate the ledger files a seam call under, beside the doors' routes. */\nexport const AI_TEXT_GENERATION_ROUTE = 'ai/text-generation'\n\n/** The route's own answer ceiling, the same table row the text job reads. */\nconst TEXT_ROUTE = AI_ROUTING_TABLE['job.text']\n\nconst refusal = (\n status: number,\n reason: PluginTextGenerationRefusal['reason'],\n error: string,\n): PluginTextGenerationRefusal => ({ ok: false, status, reason, error })\n\n/** A `Response` a platform helper refused with, read into the seam's shape. */\nasync function refusedResponse(\n response: Response,\n reason: PluginTextGenerationRefusal['reason'],\n fallback: string,\n): Promise<PluginTextGenerationRefusal> {\n const body = (await response.json().catch(() => null)) as { error?: unknown } | null\n const error = typeof body?.error === 'string' && body.error ? body.error : fallback\n return refusal(response.status, reason, error)\n}\n\n/** The system block the seam sends: the caller's instructions, cached as a prefix. */\nexport function aiTextGenerationSystem(system: string): AiSystemBlock[] {\n return [{ text: system, cacheBreakpoint: true }]\n}\n\n/** The generator, with its reads named so a spec can hand in fakes. */\nexport function createAiTextGenerator(deps: {\n firestore(): FirebaseFirestore.Firestore\n now(): Date\n} = { firestore: () => firebaseAdmin.app().firestore(), now: () => new Date() }): PluginTextGenerator {\n return {\n async generate(request: PluginTextGenerationRequest): Promise<PluginTextGenerationResult> {\n const orgId = request.orgId.trim()\n const uid = request.uid.trim()\n if (!orgId || !uid) return refusal(400, 'failed', 'Name the workspace and the member the text is for.')\n const staff = request.staff === true\n const now = request.now ?? deps.now()\n\n // The flag closes the door, not just the UI: a released-off feature\n // does not exist, and nothing below is disclosed while it is off.\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', orgId))) {\n return refusal(404, 'unavailable', 'AI drafting is not available to this workspace yet.')\n }\n if (!aiProviderReady()) {\n return refusal(503, 'unavailable', 'AI drafting is temporarily unavailable.')\n }\n\n const resolved = await getOrgForUser(uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return refusal(403, 'permission', 'You are not a member of that organization.')\n }\n const org = (request.org ?? resolved.org ?? {}) as Record<string, unknown>\n if (!staff && !(await memberHasPermissionOnHost(orgId, request.hostId, resolved.member, 'ai.generate'))) {\n return refusal(403, 'permission', 'Your role does not include Generate with AI — ask an organization admin.')\n }\n if (!checkEntitlement(org, 'aiGenerative')) {\n return refusal(403, 'entitlement', \"This workspace's plan does not include AI drafting.\")\n }\n const paused = await featureLockdownRefusal({ feature: 'ai-generate', staff, orgId, nowMs: now.getTime() })\n if (paused) return refusedResponse(paused, 'off', 'AI drafting is paused.')\n const firestore = deps.firestore()\n if (await isAiOffForSite(firestore, org, request.hostId)) {\n return refusal(403, 'off', AI_OFF_FOR_SITE_COPY)\n }\n\n // RESERVED before a token is spent, on the member's allotments for the\n // site named, so N concurrent asks cannot all read \"under the cap\".\n let reservation: AssistReservation\n try {\n reservation = await reserveAssistMessage(firestore, orgId, true, now, org, {\n uid,\n hostId: request.hostId ?? null,\n })\n } catch (error) {\n console.error('ai text-generation reservation failed', { orgId, error })\n return refusal(503, 'failed', 'AI drafting is temporarily unavailable.')\n }\n recordUserAiRefusal(firestore, orgId, uid, reservation)\n if (!reservation.allowed) {\n return refusal(\n reservation.refusedBy === 'cap' ? 402 : 429,\n 'quota',\n aiJobRefusalText(org, reservation),\n )\n }\n\n const choice = resolveAiModelChoice('job.text', null, {\n plan: resolveEffectivePlan(org),\n allotmentModels: reservation.allotment?.models ?? null,\n orgModels: reservation.allotment?.orgModels ?? null,\n })\n if (!choice) {\n await releaseAssistMessage(firestore, orgId, reservation).catch(() => undefined)\n return refusal(503, 'unavailable', 'AI drafting is temporarily unavailable.')\n }\n const model = choice.model\n const ceiling = AI_JOB_TEXT_STEP_BUDGET.maxTokens(model)\n const maxTokens =\n typeof request.maxTokens === 'number' && request.maxTokens > 0\n ? Math.min(Math.floor(request.maxTokens), ceiling)\n : ceiling\n const purpose = normalizePluginTextGenerationPurpose(request.purpose)\n // The ledger's kind for a plugin's purpose: hyphenated, so it never\n // collides with a job kind or an assist mode.\n const kind: PluginAiUsageKind = purpose.includes('-') ? (purpose as PluginAiUsageKind) : `plugin-${purpose}`\n\n let result: Awaited<ReturnType<typeof runAiRequest>>\n try {\n result = await runAiRequest({\n model,\n system: aiTextGenerationSystem(request.system),\n messages: [{ role: 'user', content: request.prompt }],\n maxTokens,\n ...(TEXT_ROUTE.thinking ? { thinking: TEXT_ROUTE.thinking } : {}),\n ...(TEXT_ROUTE.effort ? { effort: TEXT_ROUTE.effort } : {}),\n stream: false,\n ...(request.signal ? { signal: request.signal } : {}),\n })\n } catch (error) {\n // The provider was never reached, or answered nothing: no usage came\n // back, so nothing is metered and the message goes back to the org.\n await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError) =>\n console.error('ai text-generation release failed', { orgId, releaseError }),\n )\n console.error('ai text-generation failed', { orgId, purpose, error })\n return refusal(503, 'failed', 'The draft could not be written just now. Try again.')\n }\n\n // Tokens were spent, so the bill is written first and its failure is a\n // log line, never a refusal — the rule every door keeps.\n try {\n await recordAssistCost(\n firestore,\n orgId,\n {\n route: AI_TEXT_GENERATION_ROUTE,\n hostId: request.hostId ?? null,\n model,\n tier: 'entitled',\n usage: result.usage,\n docsPaths: [],\n stopReason: result.stopReason,\n deflected: false,\n free: reservation.free ?? null,\n uid,\n kind,\n },\n now,\n )\n } catch (error) {\n console.error('ai text-generation cost record failed', { orgId, purpose, error })\n }\n if (result.kind === 'refusal') {\n return refusal(422, 'refused', 'The model declined to write this draft.')\n }\n return {\n ok: true,\n text: result.text.trim(),\n model,\n usage: { inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens },\n }\n },\n }\n}\n\n/** Registers this plugin as the workspace's text generator; idempotent for this plugin. */\nexport function registerAiTextGenerator(generator: PluginTextGenerator = createAiTextGenerator()): void {\n registerPluginTextGenerator(generator, { pluginId: AI_PLUGIN_ID })\n}\n"],"names":["resolveEffectivePlan","normalizePluginTextGenerationPurpose","registerPluginTextGenerator","checkEntitlement","firebaseAdmin","featureLockdownRefusal","getOrgForUser","memberHasPermissionOnHost","isServerReleaseFlagOnForOrg","AI_PLUGIN_ID","AI_JOB_TEXT_STEP_BUDGET","aiJobRefusalText","AI_OFF_FOR_SITE_COPY","isAiOffForSite","resolveAiModelChoice","AI_ROUTING_TABLE","aiProviderReady","runAiRequest","recordUserAiRefusal","recordAssistCost","releaseAssistMessage","reserveAssistMessage","AI_TEXT_GENERATION_ROUTE","TEXT_ROUTE","refusal","status","reason","error","ok","refusedResponse","response","fallback","body","json","catch","aiTextGenerationSystem","system","text","cacheBreakpoint","createAiTextGenerator","deps","firestore","app","now","Date","generate","request","reservation","orgId","trim","uid","staff","resolved","org","hostId","member","paused","feature","nowMs","getTime","console","allowed","refusedBy","choice","plan","allotmentModels","allotment","models","orgModels","undefined","model","ceiling","maxTokens","Math","min","floor","purpose","kind","includes","result","messages","role","content","prompt","thinking","effort","stream","signal","releaseError","route","tier","usage","docsPaths","stopReason","deflected","free","inputTokens","outputTokens","registerAiTextGenerator","generator","pluginId"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -32,6 +32,7 @@ import { registerAiSiteJob } from "./jobs/ai-job-site-step.js";
32
32
  import { registerAiWorkflowJob } from "./jobs/ai-job-workflow-step.js";
33
33
  import { registerAiInsightJob } from "./jobs/ai-job-insight-step.js";
34
34
  import { registerAiFigureReaders } from "./insights/ai-figure-readers.js";
35
+ import { registerAiTextGenerator } from "./server/plugin-text-generation.js";
35
36
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
36
37
  import { ensureFirstPartyAiProviders } from "./providers/registry.js";
37
38
  import { aiAssistHandler } from "./server/ai-assist.js";
@@ -134,6 +135,10 @@ export * from "./runtime/ai-runtime.js";
134
135
  registerAiDeclarations();
135
136
  registerFirstPartyProviders();
136
137
  registerAiJobKinds();
138
+ // This plugin as the workspace's text generator on the core's seam
139
+ // (AGL-3324): another plugin drafts a paragraph through it under the
140
+ // generation doors' own rules, metered as a text job is.
141
+ registerAiTextGenerator();
137
142
  registerPluginApiRoute('assist/chat', {
138
143
  web: assistChat
139
144
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n}\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiWorkflowJob","registerAiInsightJob","registerAiFigureReaders","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","applyAiSeoAudit","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/server.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 { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { registerAiDeclarations } from './declarations'\nimport { AI_JOBS_BEAT_PATH } from './jobs/ai-jobs-beat'\nimport { registerAiJobPlan } from './jobs/ai-job-plan-step'\nimport { registerAiJobsPause } from './jobs/ai-jobs-pause'\nimport { registerAiComponentJob } from './jobs/ai-job-component-step'\nimport { registerAiCrmJob } from './jobs/ai-job-crm-step'\nimport { registerAiLayoutJob } from './jobs/ai-job-layout-step'\nimport { registerAiTemplateJob } from './jobs/ai-job-template-step'\nimport { registerAiExperimentJob } from './jobs/ai-job-experiment-step'\nimport { registerAiFormJob } from './jobs/ai-job-form-step'\nimport { registerAiPageJob } from './jobs/ai-job-page-step'\nimport { registerAiProductsJob } from './jobs/ai-job-products-step'\nimport { registerAiEmailJob } from './jobs/ai-job-email-step'\nimport { registerAiCampaignJob } from './jobs/ai-job-campaign-step'\nimport { registerAiSiteJob } from './jobs/ai-job-site-step'\nimport { registerAiWorkflowJob } from './jobs/ai-job-workflow-step'\nimport { registerAiInsightJob } from './jobs/ai-job-insight-step'\nimport { registerAiFigureReaders } from './insights/ai-figure-readers'\nimport { registerAiTextGenerator } from './server/plugin-text-generation'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { ensureFirstPartyAiProviders } from './providers/registry'\nimport { aiAssistHandler } from './server/ai-assist'\nimport { GET as aiCrmAnswer } from './server/ai-crm-answer'\nimport { POST as runAiJobsBeat } from './server/ai-jobs-beat-route'\nimport { POST as cancelAiJob } from './server/ai-jobs-cancel'\nimport { GET as aiJobEvents } from './server/ai-jobs-events-route'\nimport { POST as createAiSiteBatch } from './server/ai-jobs-batch'\nimport { GET as listAiJobs, POST as createAiJob } from './server/ai-jobs-route'\nimport { POST as resumeAiJob } from './server/ai-jobs-resume'\nimport { POST as aiGenerateComponent } from './server/ai-generate-component'\nimport { POST as applyAiSeoAudit } from './server/ai-seo-apply'\nimport { POST as assistChat } from './server/assist-chat'\nimport { POST as assistEditApplied } from './server/assist-edit-applied'\nimport { POST as assistFeedback } from './server/assist-feedback'\nimport { PATCH as aiHostPermissions } from './server/ai-host-permissions'\nimport { GET as aiAdminOrg } from './server/ai-admin-org'\nimport { GET as aiAdminOrgsSpend } from './server/ai-admin-orgs-spend'\nimport { GET as aiAdminSignals } from './server/ai-admin-signals'\nimport { GET as aiAdminUser } from './server/ai-admin-user'\nimport { POST as aiAdminOverage } from './server/ai-admin-overage'\nimport { GET as aiUsage } from './server/ai-usage'\nimport { GET as aiAllotments } from './server/ai-allotments'\nimport { GET as aiInsightAnswer } from './server/ai-insight-answer'\nimport { POST as runAiInsightsDigest } from './server/ai-insight-digest-route'\nimport { AI_INSIGHTS_DIGEST_PATH } from './insights/ai-insight-digest'\nimport { GET as aiModels } from './server/ai-models'\nimport { GET as billingCredits } from './server/billing-credits'\nimport { POST as billingOverage } from './server/billing-overage'\n\nexport * from './providers/contract'\nexport * from './providers/catalog'\nexport * from './providers/registry'\nexport * from './providers/routing'\nexport * from './runtime/ai-runtime'\n\n/**\n * The two first-party adapters, registered once per process. A marketplace\n * plugin registers its own against `AI_PROVIDER_CONTRACT` from its own\n * entry and never touches this.\n */\nconst registerFirstPartyProviders = ensureFirstPartyAiProviders\n\n/**\n * Every job kind this plugin runs beyond the three the machine registers\n * itself (AGL-3025). Each is a CALL, never an import made for what the\n * module does as it loads: this package declares only this file\n * effect-ful, so a bundler honoring `sideEffects` deletes an import whose\n * exports go unused — and a step registered that way ran in every spec and\n * in neither app. `registrations-survive-a-bundler.spec.ts` holds this.\n *\n * The console surface is the only caller (AGL-3026): its doors run a job's\n * first step inline, and its beat runs every step after it. The tenant app\n * loads no server surface of this plugin at all, so nothing that serves a\n * published site can reach a provider.\n */\nfunction registerAiJobKinds(): void {\n // The workspace AI pause the machine asks before it claims any step, so a\n // paused workspace's queued jobs spend nothing (AGL-3037).\n registerAiJobsPause()\n // The plan step every planned kind runs first (AGL-2935).\n registerAiJobPlan()\n // Components, layouts and templates (AGL-2908, AGL-2909).\n registerAiComponentJob()\n registerAiLayoutJob()\n registerAiTemplateJob()\n // Forms (AGL-2913).\n registerAiExperimentJob()\n registerAiFormJob()\n // Pages, with the least time one pass needs (AGL-2907).\n registerAiPageJob()\n // Email designs and campaigns, whose drafts the email and marketing\n // plugins write on the resource-draft seam (AGL-2912).\n registerAiEmailJob()\n registerAiCampaignJob()\n // Automations drafted from a description, and explained, whose drafts the\n // workflows plugin writes on the resource-draft seam (AGL-2919).\n registerAiWorkflowJob()\n // Product copy, and a store's products, categories and discounts from a\n // brief, as proposals the commerce plugin's surfaces apply (AGL-2916).\n registerAiProductsJob()\n // The site scaffold, which builds a whole site through the steps above\n // (AGL-2911).\n registerAiSiteJob()\n // Insights (AGL-2915): a question about a site's figures, answered from\n // tables the figure readers return, and the weekly digest. The readers for\n // the platform's own records — page views, forms, datasets — are this\n // plugin's; every other plugin registers its own from its console surface.\n registerAiFigureReaders(() => firebaseAdmin.app().firestore())\n registerAiInsightJob()\n // CRM by AI (AGL-2917): a record's summary and next step, an email draft\n // and an import's column matches, read through the CRM's facts readers.\n registerAiCrmJob()\n}\n\n/**\n * The console-side API (AGL-2939): every AI door, under the `ai` and\n * `assist` prefixes the plugin owns. The URLs the panel and the besigner\n * call are unchanged — `/api/assist/chat`, `/api/ai/assist`,\n * `/api/ai/jobs` — because the dispatcher serves them from the registry\n * exactly where the named routes used to; the two billing doors and the\n * per-member usage read (`ai/usage`) live under the plugin's own prefix.\n * The jobs beat is the one path outside them, for the reason\n * `AI_JOBS_BEAT_PATH` gives.\n */\nexport function registerAiConsoleApi(): void {\n registerAiDeclarations()\n registerFirstPartyProviders()\n registerAiJobKinds()\n // This plugin as the workspace's text generator on the core's seam\n // (AGL-3324): another plugin drafts a paragraph through it under the\n // generation doors' own rules, metered as a text job is.\n registerAiTextGenerator()\n registerPluginApiRoute('assist/chat', { web: assistChat })\n registerPluginApiRoute('assist/feedback', { web: assistFeedback })\n // The applied-edit record (AGL-2906): counts of a proposal the author\n // applied in their editor, written to the site's activity log.\n registerPluginApiRoute('assist/edit-applied', { web: assistEditApplied })\n // A collaborator's AI toggles on one site (AGL-2927), set from the site's\n // collaborators card through this plugin's column there.\n registerPluginApiRoute('ai/host-permissions', { web: aiHostPermissions })\n registerPluginApiRoute('ai/assist', aiAssistHandler)\n registerPluginApiRoute('ai/jobs', {\n web: (request) => (request.method === 'GET' ? listAiJobs(request) : createAiJob(request)),\n })\n // The agency batch (AGL-2911): one brief, one `site` job per named site,\n // under one batch id. Before the `:jobId` routes, which it is not one of.\n registerPluginApiRoute('ai/jobs/batch', { web: createAiSiteBatch })\n registerPluginApiRoute('ai/jobs/:jobId/cancel', {\n web: (request, context) =>\n cancelAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/resume', {\n web: (request, context) =>\n resumeAiJob(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // An insight job's answer (AGL-2915), which is kept off the job document\n // and served only to the member who asked, or for a digest to the site's\n // members.\n registerPluginApiRoute('ai/insights/:jobId', {\n web: (request, context) =>\n aiInsightAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // A CRM job's answer (AGL-2917), which is kept off the job document and\n // served only to a member the CRM still lets read the record.\n registerPluginApiRoute('ai/crm/:jobId', {\n web: (request, context) =>\n aiCrmAnswer(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n registerPluginApiRoute('ai/jobs/:jobId/events', {\n web: (request, context) =>\n aiJobEvents(request, { params: Promise.resolve({ jobId: String(context.params['jobId']) }) }),\n })\n // The jobs beat (AGL-3026): every step the doors leave queued, run every\n // minute by the console's scheduler on the cron secret, on the one surface\n // that holds the provider's key.\n registerPluginApiRoute(AI_JOBS_BEAT_PATH, { web: runAiJobsBeat })\n // The weekly insights (AGL-2915): Monday's digests made for the people who\n // asked for them, and delivered once the beat has written them. Beside the\n // beat under `/api/admin/`, where the console's scheduled sweeps live.\n registerPluginApiRoute(AI_INSIGHTS_DIGEST_PATH, { web: runAiInsightsDigest })\n // A site SEO audit's \"Apply all\" (AGL-2910): content fixes as new\n // unpublished versions, listing values staged for their SEO cards.\n registerPluginApiRoute('ai/seo/apply', { web: applyAiSeoAudit })\n // Save the selection as a reusable component, with AI (AGL-2908): the\n // second entry point of the component job, from the besigner's Attributes\n // panel. It proposes, and the person's Apply writes.\n registerPluginApiRoute('ai/generate/component', { web: aiGenerateComponent })\n registerPluginApiRoute('ai/billing/credits', { web: billingCredits })\n registerPluginApiRoute('ai/billing/overage', { web: billingOverage })\n registerPluginApiRoute('ai/usage', { web: aiUsage })\n // The allotments a manager sets, and the options the model switch lists\n // (AGL-2942). One handler answers the allotments' GET and POST.\n registerPluginApiRoute('ai/allotments', { web: aiAllotments })\n registerPluginApiRoute('ai/models', { web: aiModels })\n // The staff doors (AGL-2928, AGL-2930, AGL-2252): one org's AI in full,\n // one account's usage across its workspaces, and the fleet's Assist\n // signals behind the Assist signal staff page.\n registerPluginApiRoute('ai/admin/org', { web: aiAdminOrg })\n registerPluginApiRoute('ai/admin/orgs-spend', { web: aiAdminOrgsSpend })\n registerPluginApiRoute('ai/admin/user', { web: aiAdminUser })\n registerPluginApiRoute('ai/admin/signals', { web: aiAdminSignals })\n // One workspace's overage standing and the four acts staff have over it\n // (AGL-3011): the ceiling override, lifting a pause — the only way a\n // dispute pause comes off — and resetting the step. Every act audited.\n registerPluginApiRoute('ai/admin/overage', { web: aiAdminOverage })\n}\n"],"names":["registerPluginApiRoute","registerAiDeclarations","AI_JOBS_BEAT_PATH","registerAiJobPlan","registerAiJobsPause","registerAiComponentJob","registerAiCrmJob","registerAiLayoutJob","registerAiTemplateJob","registerAiExperimentJob","registerAiFormJob","registerAiPageJob","registerAiProductsJob","registerAiEmailJob","registerAiCampaignJob","registerAiSiteJob","registerAiWorkflowJob","registerAiInsightJob","registerAiFigureReaders","registerAiTextGenerator","firebaseAdmin","ensureFirstPartyAiProviders","aiAssistHandler","GET","aiCrmAnswer","POST","runAiJobsBeat","cancelAiJob","aiJobEvents","createAiSiteBatch","listAiJobs","createAiJob","resumeAiJob","aiGenerateComponent","applyAiSeoAudit","assistChat","assistEditApplied","assistFeedback","PATCH","aiHostPermissions","aiAdminOrg","aiAdminOrgsSpend","aiAdminSignals","aiAdminUser","aiAdminOverage","aiUsage","aiAllotments","aiInsightAnswer","runAiInsightsDigest","AI_INSIGHTS_DIGEST_PATH","aiModels","billingCredits","billingOverage","registerFirstPartyProviders","registerAiJobKinds","app","firestore","registerAiConsoleApi","web","request","method","context","params","Promise","resolve","jobId","String"],"mappings":"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"}